MCP proxy
graphmind mcp-proxy is the one integration that needs nothing from you but a change to how the
server is spawned. It speaks stdio JSON-RPC to your MCP host on one side and to your server on
the other, relays every frame byte for byte, and reports the conversation to GraphMind as a
live run you can watch, hold and steer.
Because it sits at the protocol boundary rather than inside a process, it works on a server written in any language — TypeScript, Python, Go, Rust, a binary somebody handed you — and on servers you did not write and cannot modify.
It ships in graphmind-ai, so there is nothing to install beyond the CLI. It is new, so make
sure you are on a release that has it — npx graphmind-ai@latest, or check that
graphmind mcp-proxy --help prints something.
graphmind mcp-proxy -- node build/server.jsgraphmind mcp-proxy -- python -m my_servergraphmind mcp-proxy -- ./target/debug/my-server --flaggraphmind mcp-proxy --trace -- npx -y @modelcontextprotocol/server-everythingEverything after -- is your server’s command line, run unchanged as a child process.
Wiring it into a host
Section titled “Wiring it into a host”Move your existing command inside the proxy. The host still spawns one process; that process just happens to be the proxy now.
claude mcp add my-server-debug -- npx -y graphmind-ai mcp-proxy -- node my-server.jsAdding it under a second name lets you keep the direct one and switch by enabling whichever you want.
// before{ "command": "node", "args": ["my-server.js"] }
// after{ "command": "npx", "args": ["-y", "graphmind-ai", "mcp-proxy", "--", "node", "my-server.js"] }Use absolute paths — the host does not spawn the child in your project directory.
Then run graphmind in another terminal and restart the host so it re-spawns the server.
Options
Section titled “Options”| Flag | Meaning |
|---|---|
--trace | One line per frame on stderr. Works with no GraphMind running at all — a tee for the protocol. |
--wait-for-attach | Hold the first frame until the debugger attaches (up to 3s), so even initialize can be gated. |
--inherit-stderr | Give the server the real stderr fd instead of piping it. Keeps isatty(2) true for servers that care; the server’s log then does not appear on the session node. |
--max-frame-bytes <n> | Frame-assembly ceiling, default 64 MiB. Above it the proxy stops parsing and becomes a raw byte pipe. |
--port <n> | GraphMind port to report to (default 4747). |
Run graphmind mcp-proxy with no command and it prints a working recipe rather than a synopsis.
What it reports
Section titled “What it reports”One session node, and a node per request under it:
| Method | nodeId | Kind |
|---|---|---|
tools/call | tool:<name> | tool |
resources/read | resource:<uri> | resource |
prompts/get | prompt:<name> | prompt |
sampling/createMessage | llm:sampling | llm |
| the session | mcp:session | server |
everything else (initialize, tools/list, ping, logging/*, notifications) | mcp:<method> | custom |
nodeId is stable per logical node, so ten calls to one tool are ten instances of one node on
the canvas rather than ten boxes — the same rule every GraphMind adapter follows. instanceId is
a per-session counter (mcp_1, mcp_2, …) rather than the JSON-RPC request id, because
notifications have no id at all and two connections would otherwise collide.
Two behaviours are worth knowing:
- An unanswered request keeps its node open. That is a real server bug and the graph should show it as one. It only becomes an error when the child process exits, which is the first moment it is honest to say no answer is coming.
- The server’s stderr is captured and streamed onto the session node, so the logging you are
allowed to write finally sits in the timeline next to the request it was about. Turn it off
with
--inherit-stderr.
Gate points and what the actions mean
Section titled “Gate points and what the actions mean”The proxy holds frames, so the four actions map onto protocol actions:
Held request (before) | Held response (after, or error) | |
|---|---|---|
continue | Forward unchanged | Forward unchanged |
inject | Do not forward; answer the sender with your value as the JSON-RPC result | Forward a rewritten frame carrying your value |
retry | Same as continue — nothing has run yet | Drop it and re-send the original request bytes; the node stays open until the new answer |
abort | Do not forward; answer the sender with a JSON-RPC error (-32099) | Drop it and answer the requester with -32099 |
A notification has no id and no answer, so it only has a before gate: continue forwards,
inject forwards your object in its place, abort swallows it.
The error gate fires on a JSON-RPC error and on an MCP tool result carrying
isError: true — the second is the common one, because that is how a failing tool reports
itself. Both are armed by default by graphmind serve, so a failing tool call pauses without
any configuration.
If your injected object has its own jsonrpc field it is sent as the whole frame; otherwise it
becomes the result. That is the escape hatch for hand-crafting an error response.
Inject the result shape, not the payload
Section titled “Inject the result shape, not the payload”Fail-open
Section titled “Fail-open”The proxy is in the path of your editor’s tool traffic, so this matters more here than anywhere else in GraphMind:
- No GraphMind running: it is a plain pipe. Frames are relayed, nothing is held, the session behaves exactly as it did before you added the proxy.
- A debugger that disconnects mid-hold releases every held frame with
continue. - If the reporter throws, the frame is forwarded unchanged. Observation is best-effort; delivery is not.
- A frame larger than
--max-frame-bytesdegrades the relay to a raw byte pipe for the rest of that stream. You lose the debugger, never the session. - Nothing is ever written to stdout except the protocol, including the proxy’s own
diagnostics.
--tracegoes to stderr. - Order is preserved. Frames go through a single FIFO queue, and while one is held the source is paused — so a held request means execution really is stopped, and the server cannot see frame N+1 before frame N.
Bytes, exactly
Section titled “Bytes, exactly”A forwarded frame is written back as it arrived. Nothing is re-serialised, so key order, number
formatting, unicode escaping and stray \rs all survive the round trip. This is deliberate: a
debugger that silently normalises the protocol is a debugger that hides protocol bugs.
Limits
Section titled “Limits”See also
Section titled “See also”- Debugging MCP servers — the workflow, end to end.
@graphmind-ai/mcp— instrument a TypeScript server from the inside.