# MCP Server

> Reach the catalogue, the gateway and the queue from an MCP client as tools.

Section: Guides. Canonical page: https://routehook.ai/docs/mcp

`POST`, `GET` and `DELETE` on `/v1/mcp` implement the Model Context Protocol over the Streamable HTTP transport: JSON-RPC 2.0, protocol version `2025-06-18`, authenticated by OAuth 2.1 or the same API key as every other route. Sixteen tools cover the catalogue, live rankings, provider routing, documentation, account data, feedback, the gateway and the queue.

## POST /v1/mcp

JSON-RPC 2.0 over Streamable HTTP: initialise a session and call the tools.

Status: Available. Authentication: any live API key on the account.

> **Warning: An MCP client holding a live key can spend the balance**
>
> **`send-message`, `generate-image` and `submit_job` charge the account that owns the key**, and a model decides on its own when to call them. Keys carry no per-tool scopes, so there is nothing to withhold at the key level. Issue the client its own key with a spend cap (`GET /v1/key` reports the cap as `limit` and what is left as `limit_remaining`), so the ceiling is set before the model reaches for a tool, and revoke that one key without touching anything else you run.

## OAuth. The default for interactive MCP clients

Give a compatible client only `https://api.routehook.ai/v1/mcp`. An unauthenticated request advertises protected-resource metadata, dynamic client registration and an authorization-code flow with mandatory S256 PKCE. The browser signs in to Routehook, access tokens last one hour, and refresh access expires after seven days. OAuth is accepted only by this MCP endpoint; it does not turn into a general-purpose `/v1` API credential.

> **Note: OAuth uses the same account balance**
>
> OAuth changes the login ceremony, not billing. Billable tools reserve and settle against the signed-in account's balance and use its routing and rate limits. If an automation needs an independently revocable secret or a separate operational ceiling, configure it with a dedicated API key instead.

## Claude Code

A static-key setup is still supported. This command stores the header with the server, so the key is not repeated per call.

**Adding the server**

```bash
claude mcp add --transport http routehook https://api.routehook.ai/v1/mcp \
  --header "Authorization: Bearer sk_live_..."
```

## Claude Desktop, and anything reading a config file

The same server as JSON. `type: "http"` selects the Streamable HTTP transport. This server has no stdio entry point, so a `command`/`args` block will not reach it.

**claude_desktop_config.json**

```json
{
  "mcpServers": {
    "routehook": {
      "type": "http",
      "url": "https://api.routehook.ai/v1/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_..."
      }
    }
  }
}
```

## A generic Streamable HTTP client

Any client that speaks JSON-RPC 2.0 over HTTP can connect directly. Complete OAuth discovery or send an API key as a bearer token, accept both `application/json` and `text/event-stream`, and start with `initialize`.

**The handshake, by hand**

```bash
curl -i -X POST https://api.routehook.ai/v1/mcp \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "capabilities": {},
      "clientInfo": { "name": "your-client", "version": "1.0.0" }
    }
  }'
```

## The session

1. **initialize**

   The reply carries `protocolVersion`, `capabilities` and `serverInfo`, and an `Mcp-Session-Id` **response header**. `-i` above is there so you can see it.

2. **notifications/initialized**

   A notification, so no `id` and no result: it answers `202` with an empty body. Skipping it leaves a client that some servers will refuse to serve.

3. **Send Mcp-Session-Id on everything after that**

   Every later `POST`, the `GET` stream and the `DELETE` all carry the session header.

4. **Discover**

   `tools/list`, `resources/list`, `resources/templates/list` and `prompts/list`. `ping` is answered too, for clients that keepalive.

5. **Call, and close**

   `tools/call` runs a tool. `GET /v1/mcp` opens the stream for server-initiated messages; `DELETE /v1/mcp` ends the session and drops it. Ending a session does nothing to the API key.

## Read-only tools

| TOOL | ARGUMENTS | RETURNS |
| --- | --- | --- |
| list-models | category, vendor, search, limit (default 50, max 200) | Models with published price, category and context length. |
| get-model | model (required) | One model: price on every metered axis, context length, and the parameters it honours. |
| list-model-endpoints | model (required) | Eligible providers with published pricing, context limits, supported parameters, latency and health. |
| list-providers | none | Connected providers with customer-safe aggregate health and model counts. |
| list-daily-model-rankings | days (1-30), limit (1-50), category | Per-day rankings by live token volume, including token share. |
| search-docs | query (required), limit (1-10) | Relevant Routehook documentation pages and summaries. |
| get_job | job_id (required), logs | request_id, status, outcome, queue_position, output and error. Read outcome. A failed job still reports COMPLETED. |
| get-credits | none | balance, held, available, credit_limit, total_credits, total_usage, currency. |
| get_usage | range (24h, 7d, 30d, 90d), model, limit | Spend by model: request counts, tokens, what was charged, and the same traffic at the vendors' list prices. |
| get-generation | id (required) | One request: tokens, latency, amount charged, provider attribution and vendor list-price comparison. |
| ping | none | Connection health status. |

## Tools that change something

| TOOL | ARGUMENTS | RETURNS | BILLING |
| --- | --- | --- | --- |
| send-message | model (required), messages or prompt, max_tokens, temperature | text, model, finish_reason, cost, usage, request_id. | Charges the account. |
| generate-image | model, prompt (both required), n (1-10), size | images[].url, cost, request_id. | Charges once per image. N multiplies the bill. |
| submit_job | model, input (both required), webhook_url, idempotency_key | The job id and its queue position. Poll it with get_job. | Reserves credit at submit, charges on completion. |
| cancel_job | job_id (required) | The job's terminal state. | Releases unspent credit; work already done is still charged. |
| send-feedback | generation_id, category (both required), comment | A support reference after verifying the generation belongs to this account. | Free; writes a support record. |

## How a tool answers

Every result is `{ content: [{ type: "text", text: <JSON> }] }`, with `isError: true` on a failure. `content[0].text` is a JSON document **as a string**. A client that hands it straight to a model is doing the right thing, but code reading it has to parse it. Where a tool declares an output schema (`send-message`, `generate-image`, `get_job` and `get-credits`) the same object arrives parsed alongside as `structuredContent`, and that is the one to read from code.

## A worked example

An agent asked to summarise something cheaply has no business guessing a model slug. It lists first, then spends, which is why the read-only tools say so in their own descriptions: a model with a budget will call `list-models` three times to get `send-message` right once.

**1: tools/call, list-models**

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "list-models",
    "arguments": { "category": "text", "search": "mini", "limit": 1 }
  }
}
```

**2: the answer, with the payload as a string**

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"data\":[{\"id\":\"openai/gpt-5.4-mini\",\"category\":\"text\",\"unit\":\"per 1M tokens\",\"price\":0.15,\"context_length\":128000}]}"
      }
    ]
  }
}
```

**3: tools/call, send-message. This one charges the account.**

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "send-message",
    "arguments": {
      "model": "openai/gpt-5.4-mini",
      "prompt": "Summarise this changelog in two sentences.",
      "max_tokens": 200
    }
  }
}
```

**4: the answer, parsed as well as rendered**

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "Two sentences of summary." }],
    "structuredContent": {
      "text": "Two sentences of summary.",
      "model": "openai/gpt-5.4-mini",
      "finish_reason": "stop",
      "cost": "0.000103",
      "usage": {
        "prompt_tokens": 412,
        "completion_tokens": 58,
        "total_tokens": 470
      },
      "request_id": "req_7c41d9be"
    }
  }
}
```

`cost` is a decimal string and `request_id` is the same id `get-generation` takes, so an agent can report what it just spent without being told.

## Resources

- `routehook://models`: the catalogue, as `GET /v1/models` serves it.
- `routehook://credits`: the calling account's balance, held amount and headroom.
- `routehook://models/{slug}`: one model. A **template**, served by `resources/templates/list` rather than `resources/list`, so read it with a slug substituted in and never with the literal braces.

## Prompt

One prompt, `pick_model`. It takes `task` (required. What the model has to do, in plain words) and `budget` (optional), and returns a message listing candidate models with their prices. It exists beside `list-models` because a person wants one command that produces a shortlist, where a model is happy to list and reason for itself.

## Protocol details worth knowing

- **Version negotiation.** State `2025-06-18`. `2025-03-26` is still accepted for clients pinned there. A version we do not speak is answered with ours rather than refused. The spec makes continuing the client's decision, and refusing at the transport turns a soft downgrade into a connection nobody can debug.
- **Two error channels.** An unknown method is JSON-RPC `-32601` inside a `200`. Transport failures stay HTTP: an absent, expired or revoked credential is a real `401`, carrying a `WWW-Authenticate` header that points to OAuth protected-resource metadata.
- **Streaming.** A `POST` answers `application/json` for a single response, or `text/event-stream` when the client accepts it and the call streams. The `GET` stream carries server-initiated messages only. Tool calls always go over `POST`.
- **No SDK behind it.** The server is hand-rolled JSON-RPC framing rather than a vendored MCP library, so the wire behaviour is exactly what is documented here.

## Next

- [Authentication](/docs/authentication): How keys authenticate, and how to cap what one can spend.
- [Cost Tracking](/docs/cost-tracking): What an agent just spent, per request and per window.
- [Queue](/docs/queue): Where submit_job, get_job and cancel_job actually land.
