API reference
MCP server
Connect an MCP client to the settlement index, gated by the same API key as the REST endpoints.
Roundhouse serves the Model Context Protocol at POST /v0/mcp. It exposes the /v0 read
endpoints as tools, so a client that speaks MCP reads the settlement index without anyone
writing a wrapper for it.
There is no separate account, quota or key. A tool call is forwarded to the endpoint the tool
wraps and answered by that endpoint's own gate — the same API key check, the same per-minute
limit shared across your organisation's keys, the same Query Unit charge on run_sql. Calling
list_flows over MCP and GET /v0/flows over HTTP are the same request as far as access and
metering are concerned.
Connect#
{
"mcpServers": {
"roundhouse": {
"type": "http",
"url": "https://api.roundhouseai.io/v0/mcp",
"headers": { "Authorization": "Bearer rh_live_…" }
}
}
}x-api-key: rh_live_… works in place of the Authorization header if your client sets that
instead.
Keep the key out of the config file#
Claude Code expands ${VAR} in both headers and URLs, so the config can be committed and shared
with no secret in it:
{
"mcpServers": {
"roundhouse": {
"type": "http",
"url": "https://api.roundhouseai.io/v0/mcp",
"headers": { "Authorization": "Bearer ${ROUNDHOUSE_API_KEY}" }
}
}
}Or add it in one line, without editing a file at all:
claude mcp add --transport http roundhouse https://api.roundhouseai.io/v0/mcp \
--header "Authorization: Bearer $ROUNDHOUSE_API_KEY"Variable expansion is a Claude Code feature. Other clients may take the literal string, so use
the plain form above unless you know yours expands it. For a rotating or short-lived credential,
Claude Code's headersHelper runs a script per connection and re-runs it on a 401.
Why the key is not in the URL#
A URL you could paste whole would be an easier setup, and it is not offered on purpose. The MCP
authorization spec is explicit: "Access tokens MUST NOT be included in the URI query string" —
clients must send Authorization: Bearer. A query-string credential also lands in access logs,
shell history and anything anyone screenshots, and unlike a header a URL is the part people paste
into chat. Roundhouse applies the same rule to its own operator endpoints.
Which clients work#
| Client | Works today | How |
|---|---|---|
| Claude Code | Yes | --header, a headers object, headersHelper, or ${VAR} |
| Cursor, Windsurf, MCP SDKs | Yes | A headers object in the client's config |
Anything with fetch | Yes | Send the header yourself |
| Claude app (iOS, Android, web) | Yes, by signing in | Its custom connector runs the OAuth flow below |
Signing in with a Roundhouse login#
Some clients have nowhere to put a header. Claude's Add custom connector form takes a Name and a URL, and authenticates through a sign-in flow rather than a credential you paste. For those, Roundhouse speaks OAuth 2.1: paste the endpoint URL, press connect, and sign in with the same account you use for the dashboard.
Which OAuth client setting to pick#
Newer versions of that form ask how Claude should identify itself to the sign-in service, as an OAuth client choice. It matters, because two of the three options need a feature of the sign-in service rather than of Roundhouse, and picking one that is not offered fails with "Couldn't register with <your connector's name>'s sign-in service" — which reads like a Roundhouse fault and is not one.
| Option | Pick it? |
|---|---|
| No client ID — register one automatically (DCR) | Yes. Dynamic registration is enabled, so leave both fields blank |
| Use your own OAuth client | Also works. Paste the client id and secret you were issued |
| Use Anthropic's hosted client metadata (CIMD) | No. Roundhouse's sign-in service does not resolve hosted client metadata documents |
Leave Authentication on Always required. Required when the server asks does not work
here: it relies on an unauthenticated handshake to detect that no credential is needed, and
Roundhouse answers initialize with a 401 precisely so that detection cannot mislead the
client into never attempting sign-in.
What you are asked, on a Roundhouse consent screen:
- Which organisation the connection should use. Tool calls draw on that organisation's Query Unit balance and share its per-minute rate limit, exactly as one of its API keys would. You are asked because a login can belong to more than one, and the client has no way to know which you meant.
- What it may do. Reading the index and running SQL are separate permissions. The second spends Query Units, so it is off unless you tick it.
The connection then appears under Team → Connected apps with the client that asked for it, the address it sends you back to, and when it was last used. Revoking it there ends the app's session immediately; a request already in flight can still succeed for up to a minute, the same window a revoked API key has.
Two things worth knowing before you rely on it. The name an app shows on that screen is whatever it called itself when it registered, which is why the screen puts the redirect address next to it: the address is the part Roundhouse checked. And the rate limit is the organisation's, shared with its keys, so a connector is not a way to buy throughput.
An API key remains the simpler credential for any client that can set a header, and nothing about it changes.
If your client sends neither a header nor a sign-in#
The connection fails at initialize with a 401, before any tool runs. The response carries a
WWW-Authenticate header naming where to discover the OAuth flow, so a client that can sign in
starts there; one that can do neither cannot use this endpoint.
This is deliberate. initialize used to answer without a credential, so such a connector
reported healthy, listed every tool, and failed only on the first real query — three steps after
the mistake. Worse, Claude's connector form read that successful handshake as
Authentication: None, detected and then never attempted OAuth at all.
Where the key comes from#
There is no MCP-specific credential and no separate MCP quota. The key is the same one the REST endpoints take, from the same two places:
| Organisation key | Created by a person in Team → API keys. Shown once, at creation — that page offers the MCP config block with the key already in it, so you can paste it straight into your client. Rate limit scales with your plan and is shared across your organisation's keys; run_sql is metered at 1 Query Unit per query on a paid plan. Revoke it from the same page. |
| Trial key | GET /v0/test/x402 mints one for $0.01 paid over x402. It registers a wallet identity, expires after 30 days, runs at a low per-minute limit, and does not cover run_sql. Meant for trying the API, not for running on it. |
Both are rh_live_… strings. The endpoint resolves one by hashing it and looking up the key's
organisation, tier and limit, so a revoked key stops working within a minute — the resolution is
cached that long.
A third source is a Roundhouse login itself, through the OAuth flow above, for clients that can only authorise a remote server by opening a login window. That path issues no key: the credential is a short-lived token the client refreshes, and what it may do comes from the consent screen rather than from a string you paste. It draws on the same organisation balance and the same rate-limit bucket as that organisation's keys.
What a tool call is allowed to do#
initialize needs a credential, so every MCP session starts authenticated. Five tools then need
nothing further — get_account, list_capabilities, get_capability,
recommend_capability_offers and list_services — the same rule as REST, where discovering and
pricing services never needs an account. Over REST those five are also reachable with no
credential at all; on MCP the handshake comes first.
get_account is the one to call first. It is unmetered, answers without a key, and reports what
your credential resolves to, which lane a metered call would take, what balance is left, and what
every route costs in both Query Units and USDC. The point is that entitlement is knowable
before a call rather than only from a refusal.
The tools#
Every tool is one /v0 endpoint, named after it, taking exactly the parameters it takes. The
API reference is therefore also the tool reference, and tools/list returns each
tool's arguments, caps and defaults straight from the same registry the
OpenAPI spec is built from.
| Tool | Wraps | Key |
|---|---|---|
get_account | GET /v0/account | No |
list_flows | GET /v0/flows | Yes |
list_transactions | GET /v0/transactions | Yes |
list_entity_settlements | GET /v0/entities/{wallet}/settlements | Yes |
get_agent | GET /v0/agents/{wallet} | Yes |
get_merchant | GET /v0/merchants/{wallet} | Yes |
list_agents | GET /v0/agents | Yes |
list_wallets | GET /v0/wallets | Yes |
get_leaderboard | GET /v0/leaderboard | Yes |
list_agent_feedback | GET /v0/agents/{agentId}/feedback | Yes |
list_facilitators | GET /v0/facilitators | Yes |
get_graph | GET /v0/graph | Yes |
list_services | GET /v0/endpoints | No |
list_capabilities | GET /v0/unified | No |
get_capability | GET /v0/unified/{slug} | No |
recommend_capability_offers | GET /v0/unified/{slug}/recommend | No |
run_sql | POST /v0/sql | Organisation |
run_sql is the one paid tool. Any organisation credential runs it metered against that
organisation's Query Unit balance, on any plan — 1 QU per started five seconds of query time, so
nearly every query costs 1. A paid plan buys the longer 60-second tier, not the route itself.
Anything else gets a refusal that says what to do, and which one depends on what you sent:
- A trial key gets
-32002— it belongs to no organisation, so there is no balance to draw on. The way forward is an organisation key, orPOST /v0/sqlover plain HTTP where an x402 payment can be attached. The message says exactly that and deliberately does not repeat the HTTP route's own "resend this request with anX-PAYMENTheader": a tool call carries no headers you control, so that is an instruction you could only fail at. - A paid organisation that has run out gets
-32002withinsufficient_balanceand a link to top up. It deliberately does not get x402 payment requirements: you have an account, and quietly charging a wallet instead of telling you to top up would be the wrong answer. - No credential at all, or a trial key with no organisation behind it, gets
-32005,no_account. The MCP surface issues no x402 challenge and cannot pay one, so retrying is pointless until you sign in or configure a key.
get_account reports which of those you are in one field. On this surface lane is credit
when metered calls draw down a balance, upgrade_required when an organisation resolved but its
plan excludes a route, and no_account only when nothing resolved at all. Those last two were
one value until September 2026, so a signed-in connection with an organisation and 2,094 Query
Units was told it had no account behind it.
Each of those states the way forward in the error message. data.body carries the route's
full response too, but some MCP clients discard error.data before a model sees it, so nothing
actionable is put only there.
The paywalled GET endpoints (/v0/test/x402, /v0/topup, /v0/queries/{slug}/results) are
deliberately not tools: a tool call carries no X-PAYMENT header, so they would be tools
that can only fail. Use them over HTTP, where the exchange can complete.
What an error means#
A tool that ran and failed is a tool result with isError set, carrying the endpoint's own
message. A caller who cannot proceed gets a JSON-RPC error instead, because the fix is
yours rather than the model's:
| Code | Means |
|---|---|
-32001 | No credential, or one that is unknown, revoked or expired. Sent as a real HTTP 401 with WWW-Authenticate, so an OAuth-capable client can start signing in from it |
-32002 | Your plan does not include this tool, or your organisation is out of Query Units. The message says which, and names the top-up link where that is the answer |
-32003 | Rate limited. The message gives the seconds to wait |
-32004 | This connection was not granted the permission the tool needs. Re-authorise it |
-32005 | No account behind this connection at all. This surface issues no x402 challenge and cannot pay one, so sign in or configure an organisation key — retrying refuses identically |
-32601 | Unknown method. This server offers tools only |
-32602 | Unknown tool, or an argument the endpoint does not take |
Every one of those carries data with the route's own status and full response body, and every
one also puts the way forward in the message. That duplication is deliberate: some MCP
clients discard error.data before a model ever sees it, so a refusal whose only useful content
was in data reads as a dead end.
An unrecognised argument is refused, not ignored: a filter that was silently dropped returns
a list you believe is narrower than it is. Likewise, a failed read never comes back as an empty
one — isError is set and the reason says so, because on an index of other people's payments
"no rows" would otherwise read as "nothing ever happened".
Transport#
The endpoint is versioned with the endpoints it wraps. MCP's own protocolVersion
negotiation covers the wire protocol — whether tools/call exists and what its envelope looks
like. It says nothing about whether list_flows still accepts before, so the tool surface
carries its own version in the path. A /v1 read API would arrive with a /v1/mcp.
Both published bases serve it, since the dashboard rewrite maps /api/v0/* to the worker's
/v0/*:
https://api.roundhouseai.io/v0/mcp | Canonical. Agent traffic belongs on the API host |
https://roundhouseai.io/api/v0/mcp | Through the dashboard rewrite. Same worker |
https://api.roundhouseai.io/mcp — the path most MCP servers use — answers 404 with the real
URL in the body rather than a bare one, so trying it tells you where to go instead of looking
like this service has no MCP server. It is not an alias and not a redirect.
Stateless Streamable HTTP: POST one JSON-RPC 2.0 message per request, no session id, no
server-initiated stream. GET and DELETE answer 405 with Allow: POST. Batched arrays are
refused — batching was removed from the protocol in revision 2025-06-18, and half-serving a
batch where one message in five is rate limited tells you less than a refusal does.
The server implements 2025-06-18 and will echo back 2025-03-26 or 2024-11-05 if your
client negotiates one of those.
Call it by hand#
curl -sL -X POST "https://api.roundhouseai.io/v0/mcp" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'curl -sL -X POST "https://api.roundhouseai.io/v0/mcp" \
-H "authorization: Bearer $ROUNDHOUSE_KEY" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_flows","arguments":{"limit":5}}}' | jq