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 RH's sign-in service" — which reads like a Roundhouse fault and is not one.
| Option | Pick it? |
|---|---|
| Use your own OAuth client | Yes. Paste the client id and secret you were issued. This always works |
| No client ID — register one automatically (DCR) | Only if dynamic registration is enabled on the deployment you are pointing at |
| Use Anthropic's hosted client metadata (CIMD) | No. Roundhouse's sign-in service does not resolve hosted client metadata documents |
Authentication can be left on Always required. Required when the server asks also works and is a slightly better fit: the handshake, the tool list and the capability tools answer without a credential, so you can see what is on offer before signing 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.
Availability: the OAuth flow is enabled per deployment. If your client reports that this server does not support sign-in, it is not yet on for the host you are pointing at — use an API key, which always works.
The trap if your client has no header field and no sign-in#
Pasting the URL into a connector that sends neither looks like it works, which is the part worth knowing:
- The connector POSTs
initialize. That needs no credential, so it succeeds. - It calls
tools/list. Also credential-free — it gets all sixteen tools. - The connector reports healthy and shows the full tool list.
- The first real query —
list_flows,get_agent, anything touching the index — fails with-32001, because those need the credential that never arrived.
So the failure surfaces three steps after the mistake, on a connector that looks correctly set
up. The credential-free handshake is deliberate (a client should be able to connect and see
what it needs before signing in or buying a key), and this is its cost. A client that can do
either of those two things will not hit it: the refusal at step 4 is a real 401 carrying a
WWW-Authenticate header, which is what tells an OAuth-capable client where to send you.
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#
The handshake needs no key: initialize, ping and tools/list all answer without one, so you
can connect first and see what each tool asks for. Five tools work with no key at all —
get_account, list_capabilities, get_capability, recommend_capability_offers and
list_services — which is the same rule as REST, where discovering and pricing services never
needs an account.
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 | Paid plan |
run_sql is the one paid tool. A paid-plan organisation key runs it metered at 1 Query Unit per
query. Anything else gets a refusal that says what to do, and which one depends on what you sent:
- A trial key or a free-plan organisation gets
-32002— SQL is not included in either, so the way forward is a paid plan, orPOST /v0/sqlover plain HTTP where an x402 payment can be attached. - 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 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.
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://www.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