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.jsonand 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.
MCP discovery and health
Section titled “MCP discovery and health”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/discoveryAuthorization: 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.
MCP HTTP reverse proxy
Section titled “MCP HTTP reverse proxy”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" }Gateway proxy routes for MCP
Section titled “Gateway proxy routes for MCP”| Method | Path | Proxies to | Notes |
|---|---|---|---|
GET | /api/mcp/health | MCP GET /health | Convenience passthrough; 5 s timeout |
POST | /api/mcp/session | MCP POST /session | Body forwarded verbatim; 10 s timeout |
POST | /api/mcp/call | MCP POST /mcp (JSON-RPC 2.0) | Forwards Authorization + X-Revka-Session; 120 s timeout |
GET | /api/mcp/session/{session_id}/events | MCP GET /session/{id}/events | SSE 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.
Create a session
Section titled “Create a session”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/sessionAuthorization: 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.
Call a tool (JSON-RPC)
Section titled “Call a tool (JSON-RPC)”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/callAuthorization: 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.
Stream session events (SSE)
Section titled “Stream session events (SSE)”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>/eventsAuthorization: Bearer <gateway-token>X-Revka-Session: <session_id>Accept: text/event-streamThe 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.
MCP server test endpoint
Section titled “MCP server test endpoint”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/testAuthorization: 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}| Field | Type | Notes |
|---|---|---|
name | string | Required; must be non-empty |
transport | string | "stdio", "http", or "sse" |
command | string | Required when transport is "stdio" |
args | string[] | Optional stdio arguments |
env | object | Optional environment variables for the stdio child |
url | string | Required when transport is "http" or "sse" |
headers | object | Optional headers for http/sse |
timeout_ms | number | Per-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
errorfield rather than as HTTP errors: an emptyname, an unknowntransport, a missingcommandfor stdio, or a missingurlfor http/sse all come back asok: false. timeout_msis converted to whole seconds and floored at1— a sub-second value such as500clamps to a1 sper-tool timeout so the downstream client never receives0.
WebSocket MCP events proxy
Section titled “WebSocket MCP events proxy”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 param | Purpose |
|---|---|
token | Gateway bearer token (same auth as /ws/terminal) |
session_id | MCP session id returned by POST /api/mcp/session |
mcp_token | MCP 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.
MCP Daemon Discovery file
Section titled “MCP Daemon Discovery file”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" }| Field | Type | Meaning |
|---|---|---|
url | string | Full MCP endpoint, e.g. http://127.0.0.1:<port>/mcp |
pid | number (optional) | Daemon process id |
started_at | string (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/discoveryreturns{ "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, andstarted_atappear. Onlyurlis guaranteed present;pidandstarted_atmay be omitted.
Related pages
Section titled “Related pages”- Revka as an MCP server — the in-process daemon, native endpoints, and tool registry tiers.
- Connecting to external MCP servers — the
[mcp]config section, transports, and deferred tool loading. - Pairing & authentication — how to obtain the gateway bearer token.
- Realtime: WebSocket, SSE & Live Canvas — the terminal, chat, and event WebSocket endpoints the Code tab uses alongside MCP events.
- Gateway API overview — shared conventions for all
/api/*routes.