> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP server

> The Vetta API published to a model as an MCP tool catalog, generated from the route table.

`POST /v1/mcp` publishes **this API itself** as a [Model Context Protocol](/docs/capabilities/tools#mcp-connector) tool catalog. Point an agent at it and the agent can operate a Vetta account — create agents, start sessions, read events, exec on a computer, check credits — the same way it operates any other connected system.

It is a JSON-RPC 2.0 endpoint. It is **not** a REST resource: there is no object, no list, no `GET`. Everything it can do, it does by dispatching one of the routes documented elsewhere in this reference.

```jsonc theme={"system"}
{
  "mcp_servers": [
    { "type": "url", "name": "vetta", "url": "https://api.vetta.sh/v1/mcp" }
  ]
}
```

<Note>
  There is no `client.mcp` in the [TypeScript SDK](/docs/sdk/typescript) and no `vetta mcp` command, on purpose. Every operation behind this endpoint already has a typed method and a command; a second, weaker way to call the same routes would be a surface to keep in sync for no gain. This endpoint exists for **MCP clients** — an agent's runtime, or your own.
</Note>

## Authentication

The same organization-scoped API key as every other endpoint, in the same header. See [Authentication](/docs/api/authentication).

```bash theme={"system"}
Authorization: Bearer sk_live_...
```

Two properties follow from how a tool call is executed, and both are the point of the design:

* **A tool call is an ordinary API call.** It is re-dispatched through the full middleware chain — authentication, scope check, plan gate, rate limit, strict request validation — as a real request carrying **the caller's own bearer**. There is no second authorization path, because there is no second path.
* **A tool can never reach further than the key that invoked it.** A key scoped `agents:read` calling `agents_create` gets `403 forbidden` back as a tool error, in the control plane's own words. Scopes are not re-declared here and cannot be widened here.

The endpoint itself declares no scope of its own — any authenticated principal may open the catalog — because every tool inside it is authorized by the route it actually is. The [plan gate](/docs/platform/billing) applies as usual.

## Handshake

The server speaks MCP revision `2025-06-18` and exactly the methods a tool catalog needs: `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`. Any other method answers JSON-RPC error `-32601`.

<CodeGroup>
  ```bash initialize theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/mcp \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}'
  ```

  ```json Response theme={"system"}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "protocolVersion": "2025-06-18",
      "capabilities": { "tools": {} },
      "serverInfo": { "name": "vetta", "version": "2026-09-01" }
    }
  }
  ```
</CodeGroup>

## The tool catalog

`tools/list` returns one tool per served route — **119 tools** on this deploy today. The catalog is **generated from the server's own route table**, the same table [`GET /v1/openapi.json`](/docs/api/overview#machine-readable-description) is generated from — so it describes exactly what the deploy you are talking to serves. A tool cannot exist without a route, and a route cannot acquire a tool by being described twice.

<ResponseField name="name" type="string">
  `resource_verb`, derived from the route's resource and its handler — `agents_create`, `agents_list`, `sessions_list_events`, `computers_exec_command`, `credits_get_balance`. Namespaced by the server name in an agent's tool config, a tool is `vetta.agents_create`.
</ResponseField>

<ResponseField name="description" type="string">
  What the operation does, the `METHOD /v1/…` it maps to, one sentence about the resource, and the scope it requires.
</ResponseField>

<ResponseField name="inputSchema" type="object">
  JSON Schema. Path parameters, declared query filters and body fields arrive as **one flat object** — a model fills a flat schema reliably and a nested one it does not. The body fields are the same zod schema the server validates with, so the arguments offered are the arguments enforced. `additionalProperties` is `false`.
</ResponseField>

<ResponseField name="annotations.readOnlyHint" type="boolean">
  `true` for every `GET`, `false` for every write. This is what lets a caller allow reads and `ask` on writes without maintaining a list.
</ResponseField>

Listing routes additionally accept `limit` (1–100) and `after`; a read addressed to a single id accepts neither. See [Pagination](/docs/api/pagination).

```json theme={"system"}
{
  "name": "sessions_list_events",
  "description": "List events — GET /v1/sessions/{id}/events — a session is one metered, resumable run of an agent, with a gap-free event log. Requires the `sessions:read` scope.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "id": { "type": "string", "description": "the `id` path segment" },
      "after_seq": { "type": "string", "description": "filter" },
      "limit": { "type": "string", "description": "page size, 1–100. Ask for a small one: a tool result is truncated at 8000 characters." },
      "after": { "type": "string", "description": "cursor: the id of the last row of the previous page." }
    },
    "required": ["id"],
    "additionalProperties": false
  },
  "annotations": { "readOnlyHint": true }
}
```

## Calling a tool

<CodeGroup>
  ```bash tools/call theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/mcp \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
         "params":{"name":"agents_get","arguments":{"id":"agt_01H8XK..."}}}'
  ```

  ```json Response theme={"system"}
  {
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
      "content": [{ "type": "text", "text": "{\"id\":\"agt_01H8XK...\",\"object\":\"agent\",\"name\":\"support\"}" }]
    }
  }
  ```
</CodeGroup>

The response body of the underlying call is returned verbatim as the tool's text content.

### Failures

A failed call is a **tool error**, not a transport error. The JSON-RPC envelope stays `200` and the result carries `isError: true` with the API's own [error envelope](/docs/api/errors) as its text — a non-200 here would make an MCP client conclude the *transport* is broken and drop the whole catalog for the turn.

```json theme={"system"}
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{\"error\":{\"type\":\"invalid_request\",\"code\":\"not_found\",\"message\":\"agent not found\",\"request_id\":\"req_01H...\"}}" }],
    "isError": true
  }
}
```

Naming a tool that is not in the catalog answers `no such tool: <name>` with `isError: true`, rather than guessing a route.

### Results are capped at 8,000 characters

A tool result is read by a model, on a budget, so a result longer than 8,000 characters is cut and the cut says so — terminally. The notice states that the reply was cut, that **paging will not fix it** (the rows themselves are large, so the next page is cut too), and that the model should report what it can see rather than call again.

This wording is deliberate and was measured. An earlier version suggested narrowing the call, and models then walked the cursor to their iteration limit — 24 model calls to count sixteen rows, every page truncated. Ask for a small `limit`, or filter, before you call.

## Withheld routes

**Nine served routes are deliberately absent from the catalog.** They answer normally over HTTP with the same key; they are simply never handed to a model. Each exclusion is a property you can rely on, not an oversight — the reason is recorded next to the route in the server and enforced by a test that fails if a withheld route disappears.

| Route                                  | Why it is withheld                                                                                                                                 |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/mcp`                         | This endpoint. A tool that re-enters the catalog is a loop with a credential in it.                                                                |
| `GET /v1/sessions/{id}/stream`         | An SSE body that never ends — a tool call has to return. `sessions_list_events` reads the same log.                                                |
| `POST /v1/files`                       | Multipart upload: the body is bytes, not JSON, so there is no argument schema to publish.                                                          |
| `POST /v1/api_keys`                    | **Mints a live credential and returns it in the response body** — i.e. into the transcript.                                                        |
| `POST /v1/api_keys/{id}/rotate`        | Same: the response carries a new secret.                                                                                                           |
| `GET /v1/api_keys`                     | Withheld with the rest of the key family; nothing an agent does needs to enumerate credentials.                                                    |
| `DELETE /v1/api_keys/{id}`             | Revoking the key the caller is authenticated with is a foot-gun, not a task.                                                                       |
| `POST /v1/vaults/{id}/credentials`     | The only route that **accepts a secret value**. A model typing one writes it into a `tool.started` event.                                          |
| `POST /v1/proxy/anthropic/v1/messages` | The [model proxy](/docs/api/proxy). Handing a model a credentialed, billed model call is a loop, and its streaming half is an SSE body that never ends. |

Two rules generate that table, and they are worth stating plainly because they are the security properties the endpoint offers:

<CardGroup cols={2}>
  <Card title="No route that returns a secret" icon="key">
    The four `/v1/api_keys` routes are withheld as a family because two of them return live credentials into the model's transcript and the log that records it. A [key](/docs/api/authentication) is issued by a person, in the dashboard or the CLI — never by an agent.
  </Card>

  <Card title="No route that accepts a secret" icon="lock">
    `POST /v1/vaults/{id}/credentials` is the one route on the whole surface whose body carries a secret *value*. [Vault](/docs/api/vaults) reads never return values, so the rest of the vault family is published; the write is not.
  </Card>
</CardGroup>

The other four are not security exclusions at all — they are shape exclusions. A never-ending stream, a byte body with no JSON schema, the catalog itself, and a billed model call that also streams cannot be expressed as a tool call that returns.

<Warning>
  Withholding is not an access control. It keeps these operations out of a model's *reach*; it does not stop the key from performing them. If an agent must not be able to touch a resource at all, scope its key — the tool call is checked against that key like any other request.
</Warning>

## Permissions

Because the catalog arrives as an ordinary external MCP server, an agent's own tool policy governs it. `vetta.agents_create` is filtered by the same allow / ask / deny mechanism as `bash`, and the default permission for MCP tools is **`ask`** — a newly exposed tool never auto-runs. See [Tools](/docs/capabilities/tools#mcp-connector) for the `tools.configs` shape, and pair it with `annotations.readOnlyHint` to allow reads while asking on writes.
