Skip to content

MCP proxy & discovery API

Browser-facing MCP discovery, HTTP reverse proxy, server testing, and the MCP events WebSocket.

Revka runs a Model Context Protocol (MCP) server as a background task inside the main daemon. It binds an ephemeral port and is meant to be reached directly by external CLI tools (Claude Code, Codex, Claude Desktop) that read a discovery file. The browser dashboard, however, lives on the gateway’s origin and can’t talk to a different port without hitting CORS — so the gateway exposes a set of same-origin /api/mcp/* reverse-proxy routes plus a /ws/mcp/events WebSocket.

This page covers the browser-facing surface: how to discover whether the MCP server is up, how to create sessions and call tools through the proxy, how to test an external MCP server config before saving it, how to stream tool-progress events over WebSocket, and the format of the ~/.revka/mcp.json discovery file.

For the MCP server itself (the in-process daemon, its native endpoints, and the tool registry), see Revka as an MCP server. To configure Revka as a client of external MCP servers, see Connecting to external MCP servers. All routes here require a bearer token from the pairing flow.

All /api/* routes share a 64 KiB request body cap and a configurable timeout (default 30 s, env REVKA_GATEWAY_TIMEOUT_SECS), with per-route overrides noted below.

When to use the proxy vs. the discovery file

Section titled “When to use the proxy vs. the discovery file”

There are two ways to reach the MCP server, and they are not interchangeable:

  • External CLIs and desktop clients (Claude Code, Codex, Claude Desktop) read ~/.revka/mcp.json and connect to the MCP port directly. They never use the /api/mcp/* routes.
  • The browser dashboard uses the gateway’s /api/mcp/* reverse proxy so it stays same-origin and reuses the gateway’s bearer-auth middleware. The proxy forwards each request to the in-process MCP server’s ephemeral port on your behalf.

The two share the same backend, so a session created through the proxy and a session created directly behave identically.

GET /api/mcp/discovery reports whether the in-process MCP server is running and reachable. It reads the ~/.revka/mcp.json discovery file and then probes the server’s /health endpoint with a 500 ms timeout, returning a uniform shape that drives the dashboard’s MCP status badge.

GET /api/mcp/discovery
Authorization: Bearer <token>

When the server is up and healthy:

{
"available": true,
"url": "http://127.0.0.1:54500/mcp",
"health": {
"status": "ok",
"pid": 12345,
"uptime_seconds": 3600,
"started_at": "2026-06-18T00:00:00Z",
"protocol_version": "2024-11-05"
}
}

When the daemon has not written the discovery file yet, or the health probe fails, you get available: false with a reason:

{ "available": false, "reason": "discovery file missing" }
{ "available": false, "reason": "health check failed" }

The health object is exactly what the MCP server’s own /health endpoint returns. protocol_version is 2024-11-05. The discovery endpoint always returns 200 OK — read the available field to decide whether MCP is usable.

These routes proxy HTTP requests to the in-process MCP server so the browser can stay same-origin. Each one requires the gateway bearer token. If the MCP server failed to bind during daemon startup, every proxy route returns 503 Service Unavailable with a uniform body:

{ "available": false, "reason": "mcp server not bound" }
MethodPathProxies toNotes
GET/api/mcp/healthMCP GET /healthConvenience passthrough; 5 s timeout
POST/api/mcp/sessionMCP POST /sessionBody forwarded verbatim; 10 s timeout
POST/api/mcp/callMCP POST /mcp (JSON-RPC 2.0)Forwards Authorization + X-Revka-Session; 120 s timeout
GET/api/mcp/session/{session_id}/eventsMCP GET /session/{id}/eventsSSE passthrough; long-lived, no request timeout
POST/api/mcp/servers/test(handshake, not a passthrough)Test an external MCP server config; 10 s ceiling

All routes require Authorization: Bearer <token>. The status code, Content-Type, and body from the upstream MCP server are passed back unchanged.

POST /api/mcp/session forwards your body verbatim to the MCP server’s POST /session and returns whatever it returns — on success, the session id and a per-session token.

POST /api/mcp/session
Authorization: Bearer <gateway-token>
Content-Type: application/json
{ "cwd": "/home/user/project", "label": "code-tab" }
{ "session_id": "<session-id>", "token": "<mcp-session-token>", "cwd": "/home/user/project" }

Keep both values. The session_id and token are issued by the MCP server, are opaque to the gateway, and are required for the JSON-RPC and SSE routes below.

POST /api/mcp/call proxies to the MCP server’s JSON-RPC 2.0 endpoint (POST /mcp). The MCP server’s supported methods are initialize, tools/list, and tools/call.

POST /api/mcp/call
Authorization: Bearer <gateway-token>
X-Revka-Session: <session_id>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}

The 120 s timeout gives long-running tools/call invocations headroom while still bounding a runaway call so it can’t tie up a worker.

GET /api/mcp/session/{session_id}/events is an SSE passthrough to the MCP server’s per-session progress stream. It is deliberately long-lived: the proxy disables the request-level timeout so the server-push stream is never severed mid-flight, and it sets x-accel-buffering: no so nginx and similar reverse proxies don’t buffer the stream.

GET /api/mcp/session/<session_id>/events
Authorization: Bearer <gateway-token>
X-Revka-Session: <session_id>
Accept: text/event-stream

The response carries Content-Type: text/event-stream and Cache-Control: no-cache. Each event’s data: payload is a ProgressEvent emitted by an in-flight tool call on that session. For browser clients, prefer the WebSocket form described under WebSocket MCP events proxy — it carries the same data over a single auth surface.

POST /api/mcp/servers/test test-connects to a user-supplied external MCP server configuration before it is saved. It runs the same initialize + tools/list handshake a real client would, then reports success, the discovered tool count and names, and the round-trip latency. This is the backend for the “Test” button in the config editor’s MCP section.

POST /api/mcp/servers/test
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "filesystem",
"transport": "stdio",
"command": "/usr/local/bin/mcp-filesystem",
"args": ["--read-only"],
"env": { "MCP_LOG": "debug" },
"url": null,
"headers": {},
"timeout_ms": 30000
}
FieldTypeNotes
namestringRequired; must be non-empty
transportstring"stdio", "http", or "sse"
commandstringRequired when transport is "stdio"
argsstring[]Optional stdio arguments
envobjectOptional environment variables for the stdio child
urlstringRequired when transport is "http" or "sse"
headersobjectOptional headers for http/sse
timeout_msnumberPer-tool timeout hint; converted to whole seconds

On a successful handshake:

{ "ok": true, "tool_count": 5, "tools": ["read_file", "write_file", "..."], "latency_ms": 234 }

On a validation or connection failure, the endpoint still returns 200 OK with ok: false:

{ "ok": false, "error": "command is required for stdio transport", "latency_ms": 0 }
{ "ok": false, "error": "timed out after 10s", "latency_ms": 10000 }

Notes on behavior:

  • The full handshake is bounded by a hard 10 s ceiling (TEST_HANDSHAKE_TIMEOUT_SECS); a misconfigured server cannot tie up a request thread.
  • Validation errors are returned in the error field rather than as HTTP errors: an empty name, an unknown transport, a missing command for stdio, or a missing url for http/sse all come back as ok: false.
  • timeout_ms is converted to whole seconds and floored at 1 — a sub-second value such as 500 clamps to a 1 s per-tool timeout so the downstream client never receives 0.

GET /ws/mcp/events is a WebSocket that proxies the MCP server’s per-session progress SSE stream to the browser. The dashboard’s Code tab opens it while a CLI coding agent runs in the terminal, so it can display live tool-progress as the agent works. Using a gateway-side proxy keeps all browser traffic on one auth surface with no CORS friction, consistent with /ws/terminal.

ws://host:port/ws/mcp/events?session_id=<mcp-session>&mcp_token=<mcp-token>&token=<bearer>
Query paramPurpose
tokenGateway bearer token (same auth as /ws/terminal)
session_idMCP session id returned by POST /api/mcp/session
mcp_tokenMCP session token returned by POST /api/mcp/session

The gateway verifies its own bearer independently (via Authorization: Bearer, the bearer.<token> WebSocket subprotocol, or the ?token= query param). The mcp_token is used only to open the in-process MCP server’s event stream — it is never validated as a gateway credential.

Each event is forwarded as a single JSON text frame matching the server’s ProgressEvent serialization:

{
"token": 7,
"progress": 4,
"total": 10,
"message": "...",
"tool": "notion",
"timestamp": "2026-04-17T10:20:33+00:00"
}

When the in-process MCP server is unreachable, the proxy sends an error frame and closes:

{ "error": "daemon-unreachable", "detail": "..." }

The stream is read-only: any client frames other than a close are ignored. Multiple data: lines within a single SSE event are joined with \n, per the SSE spec.

The typical browser lifecycle is: POST /api/mcp/session to mint a session → open /ws/mcp/events with that session_id and mcp_token → issue tool calls via POST /api/mcp/call → watch progress frames arrive on the WebSocket. See Realtime: WebSocket, SSE & Live Canvas for the other WebSocket endpoints.

The daemon writes ~/.revka/mcp.json at startup so callers can find the MCP server’s ephemeral port without hardcoding it. The gateway reads this file for the proxy and WebSocket-events routes; external CLIs read the same file directly.

The payload shape is a frozen contract:

{ "url": "http://127.0.0.1:54500/mcp", "pid": 12345, "started_at": "2026-06-18T00:00:00Z" }
FieldTypeMeaning
urlstringFull MCP endpoint, e.g. http://127.0.0.1:<port>/mcp
pidnumber (optional)Daemon process id
started_atstring (optional)RFC 3339 start timestamp

Behavior to rely on:

  • Absent until bound. The file does not exist until the daemon has fully started its MCP task. Callers must handle absence gracefully — GET /api/mcp/discovery returns { "available": false, "reason": "discovery file missing" } in that window.
  • mtime-cached. The gateway caches the parsed file keyed by its modification time. Because the daemon may restart between requests (changing the ephemeral port), the cache re-reads automatically on the next request after the mtime changes, so a restart never serves a stale port.
  • Stable shape. Only url, pid, and started_at appear. Only url is guaranteed present; pid and started_at may be omitted.