Skip to content

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.

Terminal window
graphmind mcp-proxy -- node build/server.js
graphmind mcp-proxy -- python -m my_server
graphmind mcp-proxy -- ./target/debug/my-server --flag
graphmind mcp-proxy --trace -- npx -y @modelcontextprotocol/server-everything

Everything after -- is your server’s command line, run unchanged as a child process.

Move your existing command inside the proxy. The host still spawns one process; that process just happens to be the proxy now.

Terminal window
claude mcp add my-server-debug -- npx -y graphmind-ai mcp-proxy -- node my-server.js

Adding it under a second name lets you keep the direct one and switch by enabling whichever you want.

Then run graphmind in another terminal and restart the host so it re-spawns the server.

FlagMeaning
--traceOne line per frame on stderr. Works with no GraphMind running at all — a tee for the protocol.
--wait-for-attachHold the first frame until the debugger attaches (up to 3s), so even initialize can be gated.
--inherit-stderrGive 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.

One session node, and a node per request under it:

MethodnodeIdKind
tools/calltool:<name>tool
resources/readresource:<uri>resource
prompts/getprompt:<name>prompt
sampling/createMessagellm:samplingllm
the sessionmcp:sessionserver
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.

The proxy holds frames, so the four actions map onto protocol actions:

Held request (before)Held response (after, or error)
continueForward unchangedForward unchanged
injectDo not forward; answer the sender with your value as the JSON-RPC resultForward a rewritten frame carrying your value
retrySame as continue — nothing has run yetDrop it and re-send the original request bytes; the node stays open until the new answer
abortDo 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.

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-bytes degrades 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. --trace goes 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.

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.