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#

json
{
  "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:

json
{
  "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:

bash
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#

ClientWorks todayHow
Claude CodeYes--header, a headers object, headersHelper, or ${VAR}
Cursor, Windsurf, MCP SDKsYesA headers object in the client's config
Anything with fetchYesSend the header yourself
Claude app (iOS, Android, web)Yes, by signing inIts 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.

OptionPick it?
Use your own OAuth clientYes. 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:

  1. 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.
  2. 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:

  1. The connector POSTs initialize. That needs no credential, so it succeeds.
  2. It calls tools/list. Also credential-free — it gets all sixteen tools.
  3. The connector reports healthy and shows the full tool list.
  4. 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 keyCreated 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 keyGET /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.

ToolWrapsKey
get_accountGET /v0/accountNo
list_flowsGET /v0/flowsYes
list_transactionsGET /v0/transactionsYes
list_entity_settlementsGET /v0/entities/{wallet}/settlementsYes
get_agentGET /v0/agents/{wallet}Yes
get_merchantGET /v0/merchants/{wallet}Yes
list_agentsGET /v0/agentsYes
list_walletsGET /v0/walletsYes
get_leaderboardGET /v0/leaderboardYes
list_agent_feedbackGET /v0/agents/{agentId}/feedbackYes
list_facilitatorsGET /v0/facilitatorsYes
get_graphGET /v0/graphYes
list_servicesGET /v0/endpointsNo
list_capabilitiesGET /v0/unifiedNo
get_capabilityGET /v0/unified/{slug}No
recommend_capability_offersGET /v0/unified/{slug}/recommendNo
run_sqlPOST /v0/sqlPaid 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, or POST /v0/sql over plain HTTP where an x402 payment can be attached.
  • A paid organisation that has run out gets -32002 with insufficient_balance and 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:

CodeMeans
-32001No 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
-32002Your 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
-32003Rate limited. The message gives the seconds to wait
-32004This connection was not granted the permission the tool needs. Re-authorise it
-32005No 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
-32601Unknown method. This server offers tools only
-32602Unknown 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/mcpCanonical. Agent traffic belongs on the API host
https://www.roundhouseai.io/api/v0/mcpThrough 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#

bash
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'
bash
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