# MCP gateway

The MCP gateway puts your MCP servers behind one RouterPlus endpoint. Your agents call the
servers with their RouterPlus inference key. The gateway adds the server's credentials,
applies your tool policy and logs each tool call.

The gateway uses the MCP Streamable HTTP transport (JSON-RPC 2.0). It accepts JSON and
`text/event-stream` responses from the server. In the console, open Build > MCP servers.

## Register a server

Send `POST /v1/mcp/servers` with a management key (`Authorization: Bearer tm_mk_...`):

```json
{
  "slug": "github",
  "url": "https://mcp.example.com/mcp",
  "auth_header": "Authorization",
  "auth_value": "Bearer ghp_...",
  "allowed_tools": ["search_*", "get_issue"],
  "blocked_tools": ["search_admin*"],
  "workspace_ids": []
}
```

| Field | Rule |
|---|---|
| `slug` | Required. 1 to 40 of `a-z`, `0-9` and `-`, starting with a letter or digit. Unique in your org. `servers` and `calls` are reserved. |
| `url` | Required. An `https` URL. `http` works only for `localhost`. No user name or password in the URL. |
| `auth_header` | Optional. The header name that the server expects, for example `Authorization` or `X-Api-Key`. |
| `auth_value` | Required with `auth_header`. Up to 3,000 bytes. RouterPlus stores it encrypted and never returns it. |
| `allowed_tools` | Optional. Tool names or globs. Empty (the default) allows every tool. |
| `blocked_tools` | Optional. Tool names or globs. A blocked tool stays blocked even when `allowed_tools` matches it. |
| `workspace_ids` | Optional. The workspaces whose keys can use the server. Empty (the default) means every workspace. |
| `enabled` | Optional. Default `true`. A disabled server returns 404. |

In a glob, `*` matches any run of characters. Every other character matches itself. Names are
case-sensitive.

The response is `201`, with the server. It shows `auth_header` and `has_auth_value`. It never
shows the value.

## Manage servers

| Route | What it does |
|---|---|
| `GET /v1/mcp/servers` | Lists your servers. |
| `GET /v1/mcp/servers/{id}` | Reads one server. |
| `PATCH /v1/mcp/servers/{id}` | Changes the fields you send. To rotate the secret, send `auth_value` alone. To remove the auth header and its value, send `"auth_header": null`. |
| `DELETE /v1/mcp/servers/{id}` | Deletes the server. Its call log stays. |
| `GET /v1/mcp/calls` | Reads the tool call log. |

Every route also works under `/api/v1`. Each change writes a row to the audit log
(`GET /v1/audit`).

## Call a server

Point your MCP client at `https://<gateway>/v1/mcp/{slug}` and send your inference key:

```
Authorization: Bearer tm_vk_...
```

| Method | Use |
|---|---|
| `POST` | Sends a JSON-RPC message or batch. The response is JSON or an SSE stream, in the form that the server sends. |
| `GET` | Opens the server's SSE stream. |
| `DELETE` | Ends the session. |

- The gateway passes `mcp-session-id`, `mcp-protocol-version` and `last-event-id` to the
  server. It returns the server's `mcp-session-id` to you.
- It sends the server's `auth_header` with the stored value. It never sends your
  RouterPlus key to the server.

## Tool policy

- `tools/list`: the gateway removes the tools that your policy does not allow, in JSON and in
  SSE.
- `tools/call` for a tool that your policy does not allow: the gateway returns this error and
  does not call the server:

```json
{ "jsonrpc": "2.0", "id": 3, "error": { "code": -32601, "message": "tool not allowed by RouterPlus policy" } }
```

- The gateway checks a batch one message at a time. Allowed messages go to the server. Refused
  calls get the error above, in the same response.
- Other methods (`initialize`, `resources/*`, `prompts/*`, notifications) pass through.

## Tool call log

The gateway records each `tools/call`: the tool name, the key, the server, the status and the
duration. It does not store arguments or results.

| Status | Meaning |
|---|---|
| `ok` | The server returned a result. |
| `error` | The server returned a JSON-RPC error or a result with `isError: true`. |
| `blocked` | Your policy refused the call. The gateway did not call the server. |
| `upstream_error` | The server was unreachable or returned an HTTP error. |
| `timeout` | The server did not answer in time. |
| `no_answer` | The server ended its response without an answer to the call. |

`GET /v1/mcp/calls` takes these query parameters: `server_id`, `key_id`, `tool`, `status`,
`from` and `to` (ISO 8601 times), and `limit` (1 to 500, default 100). The newest calls come
first.

## Limits and errors

| Limit | Value |
|---|---|
| Request body | 4 MiB. A larger body gets `413`. |
| Server timeout | 120 s until the server starts its response, and 120 s between parts of a stream. When the server does not start in time, the response is `504`. |
| JSON response from the server | 16 MiB. |

| Status | Cause |
|---|---|
| `401` | The inference key is missing or not valid. |
| `404` | No enabled server has this slug in your org, or the server is not open to your key's workspace. |
| `400` with JSON-RPC code `-32700` | The body is not valid JSON. |
| `400` `upstream_refused` | The server URL resolves to a private, loopback or link-local address. The gateway does not call it. |
| `502` `upstream_auth_failed` | The server refused the stored credentials (HTTP 401 or 403). |
| `502` `upstream_error` | The server was unreachable, or sent a redirect or invalid JSON. |

Other HTTP errors from the server (for example `404` for an expired session) reach you
unchanged.
