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.
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.
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.
{
"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.
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
- 01initialize
The reply carries
protocolVersion,capabilitiesandserverInfo, and anMcp-Session-Idresponse header.-iabove is there so you can see it. - 02notifications/initialized
A notification, so no
idand no result: it answers202with an empty body. Skipping it leaves a client that some servers will refuse to serve. - 03Send Mcp-Session-Id on everything after that
Every later
POST, theGETstream and theDELETEall carry the session header. - 04Discover
tools/list,resources/list,resources/templates/listandprompts/list.pingis answered too, for clients that keepalive. - 05Call, and close
tools/callruns a tool.GET /v1/mcpopens the stream for server-initiated messages;DELETE /v1/mcpends 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.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list-models",
"arguments": { "category": "text", "search": "mini", "limit": 1 }
}
}
{
"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}]}"
}
]
}
}
{
"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
}
}
}
{
"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, asGET /v1/modelsserves it.routehook://credits: the calling account's balance, held amount and headroom.routehook://models/{slug}: one model. A template, served byresources/templates/listrather thanresources/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-26is 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
-32601inside a200. Transport failures stay HTTP: an absent, expired or revoked credential is a real401, carrying aWWW-Authenticateheader that points to OAuth protected-resource metadata. - Streaming. A
POSTanswersapplication/jsonfor a single response, ortext/event-streamwhen the client accepts it and the call streams. TheGETstream carries server-initiated messages only. Tool calls always go overPOST. - 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.