> ## 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 — **192 tools** on this deploy today, from 228 routes less the open ones and the 20 [withheld](#withheld-routes). 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`. A sub-resource keeps its word: the [browser](/docs/api/browser) on a computer is `computers_browser_run_action`, `computers_browser_login`, `computers_browser_save_context`. 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` when the call changes nothing, `false` when it does. That is the method in almost every case — `GET` reads, the rest write — with named exceptions in both directions: a POST whose body is only the question (pricing a payment or a card, minting a file link) is `true`; the one `GET` that returns a live card credential is `false`. This is what lets a caller allow reads without maintaining a list.
</ResponseField>

<ResponseField name="annotations.destructiveHint" type="boolean">
  `true` when the call is one a person should see before it runs: every `DELETE`, and any call that spends money or acts outside your account (a payment or transfer, a card order or its credential reveal, a credits charge, paid media generation, a company formation, a social post, an outbound email or SMS, an account created in a third party's system), changes the organization's security posture (vaults, webhooks, the organization and its members, domains, app secrets and login links, saved browser logins), rewrites data (a schema migration, raw SQL through `apps_query_db`, a path removal), decides what an agent may do (creating, updating or rolling back an agent — its version is the model, toolset, permissions and budget every later session runs under), or runs code (a command on a computer, a browser action that acts on a page, a browser login or sign-up). Creating a session, an identity, an inbox, a skill, a file or a deployment is `false`. A read-only tool is never destructive. This is what lets a caller allow benign writes and `ask` only on the dangerous ones.
</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, "destructiveHint": false }
}
```

## 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

**Some 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`                                 | A 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.                                                                          |
| `GET /v1/computers/{id}/browser/live-view`                         | Answers a [dashboard session](/docs/api/browser#live-view) only, and its answer is a bearer URL for a person's eyes, not a transcript.                                                                                           |
| `POST /v1/browser/contexts/{name}/grants`                          | Dashboard session only: a [saved login](/docs/api/browser#grant-a-saved-login) is default-deny and a model must not grant itself one.                                                                                            |
| `DELETE /v1/browser/contexts/{name}`                               | Dashboard session only, like the grant it revokes.                                                                                                                                                                          |
| `POST /v1/browser/credentials`                                     | **Accepts a password value**, like the vault write above; the `login` action reads what a person sealed.                                                                                                                    |
| `POST /v1/proxy/openai/v1/chat/completions`                        | The same proxy in its second dialect.                                                                                                                                                                                       |
| `POST /v1/models/auto_select`                                      | A credentialed, billed model-side call like the proxy, and its answer is an id for the next line of the same program — an SDK-only add-on.                                                                                  |
| `POST /v1/sandbox/mcp`                                             | The session tool bridge — a transport for tools a model already has.                                                                                                                                                        |
| `POST /v1/sandbox/handoffs`                                        | The door behind [`send_to_agent`](/docs/capabilities/tools#handoffs) with `wait: false`. It answers only a bearer the session runtime mints for the session it is running; a model that may hand work on already holds the tool. |
| `POST /v1/sandbox/channel/messages`                                | The door behind `post_to_channel`: a room's member speaks into its root. Same per-session bearer; a member already holds the tool, and anyone else has no room to post into.                                                |
| `GET` `HEAD` `POST` `PATCH` `PUT` `DELETE /v1/apps/{id}/db/rest/*` | The [database REST passthrough](/docs/api/database#rest): its arguments *are* the rest of the URL — any path, any filter, a body no flat schema describes. The `apps` tool's `db_rest` action hands it to an agent whole.        |

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; the browser live-view URL is a bearer credential for the same reason. 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` and `POST /v1/browser/credentials` are the routes whose body carries a secret *value*. [Vault](/docs/api/vaults) reads never return values, so the rest of the vault family is published; the writes are not.
  </Card>
</CardGroup>

The rest are not security exclusions at all — they are shape exclusions. A never-ending stream, a byte body with no JSON schema, the catalog itself, a billed model call that also streams, and a REST passthrough whose arguments are an arbitrary URL cannot be expressed as a flat tool call that returns. The database one is not lost to agents: the built-in `apps` tool's `db_rest` action carries the whole request.

<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` and `annotations.destructiveHint` to allow reads and benign writes while asking on the dangerous ones.
