Skip to content
docs/mcpGuides

MCP Server

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

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/mcpAvailable

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

AUTHENTICATION
Bearer token, Authorization header
REQUIRED SCOPE
api-key

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.

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.

cURLAdding the server
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.

JSONclaude_desktop_config.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.

cURLThe handshake, by hand
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. 01
    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. 02
    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. 03
    Send Mcp-Session-Id on everything after that

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

  4. 04
    Discover

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

  5. 05
    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

TOOLARGUMENTSRETURNS
list-modelscategory, vendor, search, limit (default 50, max 200)Models with published price, category and context length.
get-modelmodel (required)One model: price on every metered axis, context length, and the parameters it honours.
list-model-endpointsmodel (required)Eligible providers with published pricing, context limits, supported parameters, latency and health.
list-providersnoneConnected providers with customer-safe aggregate health and model counts.
list-daily-model-rankingsdays (1-30), limit (1-50), categoryPer-day rankings by live token volume, including token share.
search-docsquery (required), limit (1-10)Relevant Routehook documentation pages and summaries.
get_jobjob_id (required), logsrequest_id, status, outcome, queue_position, output and error. Read outcome. A failed job still reports COMPLETED.
get-creditsnonebalance, held, available, credit_limit, total_credits, total_usage, currency.
get_usagerange (24h, 7d, 30d, 90d), model, limitSpend by model: request counts, tokens, what was charged, and the same traffic at the vendors' list prices.
get-generationid (required)One request: tokens, latency, amount charged, provider attribution and vendor list-price comparison.
pingnoneConnection health status.

Tools that change something

TOOLARGUMENTSRETURNSBILLING
send-messagemodel (required), messages or prompt, max_tokens, temperaturetext, model, finish_reason, cost, usage, request_id.Charges the account.
generate-imagemodel, prompt (both required), n (1-10), sizeimages[].url, cost, request_id.Charges once per image. N multiplies the bill.
submit_jobmodel, input (both required), webhook_url, idempotency_keyThe job id and its queue position. Poll it with get_job.Reserves credit at submit, charges on completion.
cancel_jobjob_id (required)The job's terminal state.Releases unspent credit; work already done is still charged.
send-feedbackgeneration_id, category (both required), commentA 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.

JSON1: tools/call, list-models
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "list-models",
    "arguments": { "category": "text", "search": "mini", "limit": 1 }
  }
}
JSON2: the answer, with the payload as a string
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"data\":[{\"id\":\"openai/gpt-4o-mini\",\"category\":\"text\",\"unit\":\"per 1M tokens\",\"price\":0.15,\"context_length\":128000}]}"
      }
    ]
  }
}
JSON3: tools/call, send-message. This one charges the account.
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "send-message",
    "arguments": {
      "model": "openai/gpt-4o-mini",
      "prompt": "Summarise this changelog in two sentences.",
      "max_tokens": 200
    }
  }
}
JSON4: the answer, parsed as well as rendered
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "Two sentences of summary." }],
    "structuredContent": {
      "text": "Two sentences of summary.",
      "model": "openai/gpt-4o-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.