# LSP

URL: https://docs.hanzo.ai/docs/openapi/lsp

> Live semantic code intelligence — definitions, references, types, hover, outline and diagnostics — over a repository AND its resolved dependencies, with no toolchain on the caller's machine.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/lsp) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Live semantic code intelligence — definitions, references, types, hover, outline and diagnostics — over a repository AND its resolved dependencies, with no toolchain on the caller's machine.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 5                                      |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1214 · LSP — Live Code Intelligence** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1214-lsp-live-code-intelligence.md)

`/v1/lsp` is live semantic code intelligence — definitions, references, types,
hover, outline, diagnostics, completion — over a repository and its resolved
dependencies, with no toolchain on the caller's machine. The cloud side,
`hanzoai/cloud` `apps/lsp`, is a proxy: the language servers run in the
`hanzoai/lsp` daemon, jailed on its own deployment, and this side owns the
three things the daemon must never hold — the tenant, the repository and the
ledger (`apps/lsp/lsp.go:14-31`).

### Motivation

Answering a cross-dependency question means running a third-party toolchain
over untrusted bytes, and a git credential that can fetch any repository is
exactly what must not exist next to an unjailed compiler. Splitting the
capability at that line — identity and money here, compilers there — is the
design; this HIP also settles its address, which today nests inside another
capability's root.

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted
as in RFC 2119.

#### The surface

Every address is under `/v1/lsp`, and every operation is typed: `POST /hover`,
`/locate`, `/symbols`, `/diagnostics` and `/complete`
(`apps/lsp/mount.go:67-76`), each taking one `Query` — one position in one file
of one repository at one revision (`apps/lsp/lsp.go:60-84`). `lsp` is on
HIP-0139 §2.5's list, so the name needs no argument. Positions are the LSP's
own — 0-based lines, UTF-16 character units — passed through untouched, because
the callers are agents and editors that already speak the protocol and a
silent re-basing corrupts every multi-byte line (`apps/lsp/lsp.go:33-41`).

Today's router serves this surface at `/v1/code/lsp` (`manifest/apps.go:325`,
`apps/lsp/mount.go:71`), under the code capability's root — nested rather than
top-level, so it is not a line `openapi/misfiled.txt` can carry; `/v1/lsp` is
the target this HIP declares, not yet served. `code` and `lsp` are two reads of
one repository — lexical search and a live language server — and stay
cross-referenced siblings, each at its own root, per §3.1.

#### What this side owns

No store. The tenant is resolved from the validated principal on every request
and is the daemon's isolation key; a caller supplies a repository slug, never
an owner and never a URL, so there is no input from which one tenant could name
another's repository (`apps/lsp/lsp.go:21-25,60-66`). The revision and tree
come from the forge, read as the caller, so the forge's own ACL decides which
repositories answer; the daemon holds no git credential — the tree is pushed to
it, never pulled by it (`apps/lsp/lsp.go:26-30`).

#### Money

Preparing a revision is what costs — a tree write, a dependency fetch, a
language server indexing for seconds to minutes — so the prepare carries the
fee: a flat 2¢, flat because the caller chooses the repository, not the cost of
indexing it (`apps/lsp/meter.go:38-44`). A query against a prepared revision is
a JSON-RPC round trip costing microseconds; it is recorded for attribution and
costs nothing, so callers are taught to reuse revisions rather than re-key
them. The ledger kind is `lsp` with models `prepare` and `query`, which one it
was being the daemon's answer, never a guess (`apps/lsp/meter.go:35-52`). The
gate runs before the work — an out-of-funds caller gets a clean 402 instead of
a dependency fetch nobody can bill — gating the worst case and charging the
real one (`apps/lsp/meter.go:70-77`). The payer is the selected billing org
from the request, never a body field (`apps/lsp/meter.go:54-67`). The plugin
declares `Price: cloud.Metered` (`plugin/lsp/main.go:31`).

#### Events, observability, stage

It publishes no events on the bus, so a customer's webhooks receive nothing
from it, and it emits nothing to observability beyond the request span every
route gets; attribution lives on the ledger rows. The stage is `ga` — the
manifest row declares none — and `ga` here rests on the `hanzoai/lsp` daemon
deployment actually serving; a deployment without the daemon MUST declare the
capability `beta` rather than 503 behind a `ga` door.

#### Upstreams

This package derives from none: it implements the Language Server Protocol's
position semantics as a wire fact and proxies its own daemon. The third-party
language servers themselves run inside `hanzoai/lsp`, jailed, and are that
repository's account.

### Rationale

The alternative argued in the package's own doc was one home — lsp under
`/v1/code`, because an agent searches with code and is certain with lsp
(`apps/lsp/lsp.go:7-12`). The cross-reference survives; the address does not:
HIP-0139 §3.1 makes a second capability under another's root a misfiled route,
and lsp is its own app, its own plugin and its own deployment with its own
meter — everything a capability is except the address. The fold under its own
name costs one route move and removes the mount-order subtlety of a nested
prefix entirely.

### Security Considerations

What an attacker gets from the wrong implementation is a compiler running over
their bytes next to another tenant's code. The design splits the two: the
daemon that runs toolchains holds no tenant mapping, no git credential and no
ledger, and the side that holds those runs no toolchain. Tenancy has no
caller-writable input — org from the principal, repository by slug within that
org, tree by the forge's ACL as the caller — so the cross-tenant read has no
parameter to arrive through. The billing subject is read from the request's
validated identity, never from the body, so a caller cannot bill somebody else
(`apps/lsp/meter.go:60-63`).

## Four surfaces

| Surface  | Reaches this capability as                  | Coverage                                                                  |
| -------- | ------------------------------------------- | ------------------------------------------------------------------------- |
| **REST** | `lsp` at its own prefix                     | 5 operations                                                              |
| **CLI**  | `hanzo lsp …`                               | 5 of 5                                                                    |
| **SDK**  | `LspApi` in every published client          | 5 methods                                                                 |
| **MCP**  | tool `lsp` on `https://api.hanzo.ai/v1/mcp` | 5 operations, 0 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call. This one takes arguments — the placeholders are yours to fill. `POST /v1/lsp/hover`, operation `post_lsp_hover`:

**CLI**

```bash
hanzo lsp hover
```

**SDK**

**.ts**

```ts
import { Configuration, LspApi } from 'hanzoai';

const api = new LspApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postLspHover({ character: 0, line: 0 });
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import LspApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = LspApi(client).post_lsp_hover(character=0, line=0)
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.LspAPI.PostLspHover(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, lsp_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = lsp_api::post_lsp_hover(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.LspApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new LspApi(client).postLspHover();
```

**HTTP**

```bash
curl -X POST https://api.hanzo.ai/v1/lsp/hover \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "character": 0,
       "line": 0
     }'
```

**MCP**

MCP declares no tool for **lsp** — `tools/list` on `https://api.hanzo.ai/v1/mcp` names the products it does reach. Use HTTP or an SDK.

Answers `200` with `object` — ok.

## Endpoints

| Endpoint                                                                                  | What it does                                                                                                                                                                            |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`POST /v1/lsp/complete`](https://docs.hanzo.ai/docs/openapi/lsp/post-lsp-complete)       | Offers the candidates a language server has at a position, typed and resolved through the repository's dependencies rather than guessed from text.                                      |
| [`POST /v1/lsp/diagnostics`](https://docs.hanzo.ai/docs/openapi/lsp/post-lsp-diagnostics) | Reports every problem the language server finds in one file — compile errors, type errors and lints, each with its span and its severity (1 error, 2 warning, 3 information, 4 hint).   |
| [`POST /v1/lsp/hover`](https://docs.hanzo.ai/docs/openapi/lsp/post-lsp-hover)             | Renders the type and documentation of the symbol at a position, as the language server itself renders it.                                                                               |
| [`POST /v1/lsp/locate`](https://docs.hanzo.ai/docs/openapi/lsp/post-lsp-locate)           | Finds where a symbol lives: its definition, its references, its type or its implementations, chosen by relation (definition, reference, type, implementation — empty means definition). |
| [`POST /v1/lsp/symbols`](https://docs.hanzo.ai/docs/openapi/lsp/post-lsp-symbols)         | Outlines one file: every declaration in it, with its kind and its span.                                                                                                                 |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
