# MCP server

Connect an MCP client to the settlement index, gated by the same API key as the REST endpoints.

Section: API reference
Source: https://app.roundhouseai.io/docs/api/mcp

---

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

| 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:

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**](https://app.roundhouseai.io/dashboard/team) 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 key** | Created by a person in [**Team → API keys**](https://app.roundhouseai.io/dashboard/team). 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](https://app.roundhouseai.io/docs/api) 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](https://app.roundhouseai.io/docs/api/openapi.yaml) 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, 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:

| 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

```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
```

---

Every page in these docs is available as markdown at its own URL plus `.md`.
Full index: https://app.roundhouseai.io/docs.md
