run-mcp
v4.0.0
Published
An MCP server your coding agent uses to run, test, and diagnose the MCP server it is writing — spawn it, call its tools, restart it after an edit and see what changed, read how it died. Also a headless CLI with persistent sessions for agents that work fro
Maintainers
Readme
run-mcp
An MCP server your coding agent uses to run, test, and diagnose the MCP server it is writing.
An agent building an MCP server needs to run it, call it, restart it after every edit, and find out why it broke, without a human editing mcp.json and restarting the session each time. run-mcp is the harness for that loop. Register it once with your agent and it can spawn any local server, call its tools, restart it and see what changed, and read how it died: the exit code, the stderr from the moment it crashed, the console.log that corrupted stdout, and what the person answered when the server asked.
Other tools show the agent the protocol. This one shows it the process.
Two interfaces, one engine:
- Agent MCP Server, the default.
npx -y run-mcpis an MCP server exposingconnect_to_mcp,call_mcp_primitive,reconnect_to_mcp, and nine other tools. Add it to your agent and ask it to test your server. - Headless CLI, for agents (and people) working from a shell:
run-mcp call,run-mcp list-tools, and friends print JSON to stdout. Add--session <name>and the server stays up between commands, withreconnect,stderr, andvalidateagainst the running instance.
For a human at a keyboard, use the official Inspector: npx @modelcontextprotocol/inspector --tui node my-server.js. run-mcp no longer ships a REPL.
Installation
Nothing to install for a one-off; npx fetches it:
npx -y run-mcp list-tools -- node path/to/my-mcp-server.jsTo give your agent the tools, register npx -y run-mcp as an MCP server in the agent's config. The exact command or file differs per agent; see Add run-mcp to your agent for Claude Code, Claude Desktop, Cursor, VS Code, Codex CLI, Gemini CLI, and Windsurf.
Or install it globally so run-mcp is on your PATH:
npm install -g run-mcpRequires Node.js 20.11 or newer. To work on run-mcp itself, see Development.
Add run-mcp to your agent
Every example below registers the same thing: an MCP server named run-mcp started with npx -y run-mcp. Once it is registered, ask the agent to test the server you are building and it will reach for connect_to_mcp, call_mcp_primitive, and reconnect_to_mcp on its own:
Use run-mcp to connect to
node dist/index.js, list its tools, callsearchwithq=hello, and show me the server's stderr.
Options go in args after the package name: ["-y", "run-mcp", "--out-dir", "./screenshots"] saves intercepted images there, and --max-text 20000 lowers the truncation limit. Everything else (which server to spawn, its env, the protocol era) is passed per call by the agent.
Claude Code
One command. The default scope is local (this project, only you); --scope project writes a shareable .mcp.json, --scope user applies to every project:
claude mcp add run-mcp -- npx -y run-mcp
claude mcp add --scope project run-mcp -- npx -y run-mcpOr add it to .mcp.json in the repo by hand:
{
"mcpServers": {
"run-mcp": {
"command": "npx",
"args": ["-y", "run-mcp"]
}
}
}Check it registered with claude mcp list, then /mcp inside a session shows its tools.
Claude Desktop
Settings → Developer → Edit Config opens claude_desktop_config.json (~/Library/Application Support/Claude/ on macOS, %APPDATA%\Claude\ on Windows). Add the server and restart the app:
{
"mcpServers": {
"run-mcp": {
"command": "npx",
"args": ["-y", "run-mcp"]
}
}
}Cursor
.cursor/mcp.json in the project (or ~/.cursor/mcp.json for every project):
{
"mcpServers": {
"run-mcp": {
"command": "npx",
"args": ["-y", "run-mcp"]
}
}
}VS Code (Copilot agent mode)
.vscode/mcp.json uses a servers key and a type:
{
"servers": {
"run-mcp": {
"type": "stdio",
"command": "npx",
"args": ["-y", "run-mcp"]
}
}
}Or from a shell:
code --add-mcp '{"name":"run-mcp","command":"npx","args":["-y","run-mcp"]}'Codex CLI
codex mcp add run-mcp -- npx -y run-mcpEquivalent to this block in ~/.codex/config.toml (or a project's .codex/config.toml):
[mcp_servers.run-mcp]
command = "npx"
args = ["-y", "run-mcp"]Gemini CLI
gemini mcp add takes the command and its arguments positionally. The default scope is project; -s user applies everywhere:
gemini mcp add run-mcp npx -y run-mcpEquivalent to this in .gemini/settings.json or ~/.gemini/settings.json:
{
"mcpServers": {
"run-mcp": {
"command": "npx",
"args": ["-y", "run-mcp"]
}
}
}Windsurf
~/.codeium/windsurf/mcp_config.json, same mcpServers shape as Cursor:
{
"mcpServers": {
"run-mcp": {
"command": "npx",
"args": ["-y", "run-mcp"]
}
}
}Anything else
Any client that can launch a stdio MCP server can launch this one: the command is npx, the arguments are ["-y", "run-mcp"], and no environment variables are needed. If you installed it globally, run-mcp with no arguments does the same thing.
What the agent gets
| Tool | Description |
| :--- | :--- |
| connect_to_mcp | Spawn and connect (use include to get tools/resources/prompts; name it to keep several open) |
| call_mcp_primitive | Call a tool, read a resource, or get a prompt (auto-connects) |
| list_mcp_primitives | List tools, resources, and/or prompts |
| get_server_notifications | Inspect notifications the target emitted (list_changed, updates, logs) |
| subscribe_to_resource | Exercise a server's resource-subscription support |
| reconnect_to_mcp | Restart the target after a code edit and diff what changed |
| read_result | Page through an oversized result spilled to disk |
| disconnect_from_mcp | Stop the target without restarting it |
| mcp_server_status | Connection status, how the server died, what the host forwards |
| get_mcp_server_stderr | The target's stderr, timestamped, with a cursor |
| validate_mcp_server | Spawn a server command, check it, report stderr and exit |
| list_available_mcp_servers | The servers registered with the clients on this machine |
The loop
connect_to_mcp command=node args=[dist/index.js] include=[tools] summary=true
call_mcp_primitive type=tool name=search arguments={q: "hello"}
... edit the server ...
reconnect_to_mcp → "Tools: +1 added (search_images)"
get_mcp_server_stderr since=42 → only what the server said since the last lookWhen the server breaks
A server that fails to start is the most common event in this loop, and its stderr is the only evidence of why. Every connect failure carries it inline, with how the process ended:
Failed to connect: Connection closed
Command: node dist/index.js
The server exited with code 1.
--- Target server stderr (this is almost certainly the cause) ---
14:02:11.240 #1 Error: Cannot find module './db-config.js'
14:02:11.241 #2 at loadConfig (src/index.js:12:9)The same wording appears when a running server drops (was killed by SIGSEGV, exited with code 0 (it chose to stop)), in mcp_server_status, in reconnect_to_mcp failures, in a call the server died in the middle of, and in validate.
A server that starts but never completes the handshake (waiting on a database, a port, or stdin, or writing its protocol to the wrong stream) is given up on after 30 seconds, torn down, and reported with that diagnosis and its stderr. connect_timeout_ms on connect_to_mcp and --connect-timeout on headless commands change the budget for a slow but healthy server.
stdout is the protocol channel
The single most common stdio bug is a console.log in the server: stdout carries JSON-RPC, so any other line corrupts the channel. The SDK's transport skips such lines silently, which means the server "works" under a forgiving client and breaks under a strict one. run-mcp watches the child's stdout itself and reports every non-JSON line: a section in mcp_server_status, get_mcp_server_stderr, and connect replies, stdout_noise in call --raw plus a Warning: on stderr in headless mode, and a FAIL in validate (stdout_protocol_channel). Transport-level errors the SDK only reports through a callback (a message that parsed as JSON but isn't valid JSON-RPC, a buffer overflow) are captured the same way (transport_errors).
Stderr as data
- What is kept: the first 100 lines and the last 1,000. A chatty server's crash diagnosis is usually in the first lines, which a plain tail would drop, so the head is kept and a marker line (
[... 137 of 1,237 lines elided ...]) makes the gap visible instead of silent. - Timestamps and a cursor:
get_mcp_server_stderrprefixes each line with its arrival time and stream position (12:01:03.412 #42 …) and ends withnext: since=N; pass that assinceto see only what the server said after your last look.timestamps: falsegives bare lines. - Relayed to the host in batches: the agent server forwards stderr as logging messages, at most 20 lines per message every 250 ms, with a
get_mcp_server_stderr since=Npointer for the rest, so a chatty startup does not flood the agent's context. - From a shell,
run-mcp call <tool> --rawincludes astderrarray in the result envelope, andrun-mcp stderrprints what a fresh spawn writes at startup.
Testing your server's client-facing behavior
Some MCP features depend on what the client provides. run-mcp exposes these so an agent can exercise them:
- Roots: pass
rootstoconnect_to_mcp(orreconnect_to_mcp) and your server'sroots/listcalls get a real answer.run-mcpadvertises the roots capability, so without this your server correctly sees an empty list. Roots persist across reconnects. - Log level: pass
log_levelto raise your server's logging verbosity. - Notifications:
get_server_notificationsshows what your server emitted (tools/list_changed,resources/updated, log messages). These travel outside the request/response flow, so a tool result will never reveal them. - Subscriptions:
subscribe_to_resource, then trigger a change and confirm withget_server_notifications(method='resources/updated'). On a 2026-07-28 connection this opens asubscriptions/listenstream and reports what the server honored. - What your host forwards: every connect, reconnect, and
mcp_server_statusreply carries one line naming what a server's requests to the client will actually reach:Client input forwarded to your host: elicitation forms yes, URL mode no, sampling no; roots answered by run-mcp (2 configured).run-mcpadvertises elicitation, sampling, and roots to every target; elicitation and sampling are relayed to the host it runs under, so whether they get a real answer depends on what that host declared. - What the person answered: with
include_metadata: true, each call reports one line per client-input round: the question's first sentence, the outcome (accepted,declined,cancelled,answered,refused), which fields differ from the form's defaults (names only; the values are the person's), and how long the round took. The JSON metadata carries the same underinput_requests.rounds, andlatency_msis split intoserver_msandinput_wait_ms, so a slow form is not read as a slow server. - Progress: every call offers the server a progress token, so its streaming path is exercised on every call. With
include_metadata: truethe reply says what it did with it:Progress: 4 update(s) streamed; last 4/4 "step 4" at 0.3s.orProgress: none — a progress token was offered and the server sent no notifications/progress, with the events underprogressin the JSON. The updates are also listed byget_server_notifications(method='progress'), andcall --rawcarries the sameprogressfield. A host like Claude Code shows streamed progress and falls back to polling otherwise; this is the only place the difference is visible. - Timeouts are server time:
timeout_ms(and--timeoutin headless mode) pauses while a request to the client is open. A form nobody answers for 45 seconds cannot trip a 30-second timeout; a timeout message says how much client wait was excluded. - Several servers at once: give
connect_to_mcpanameand pass it asconnectionto the other tools. With one live connection nothing needs a name; with several, a tool called without one lists them instead of guessing (naming the most recently used one), andmcp_server_statusshows all of them. A disconnected connection keeps its record, so its stderr andreconnect_to_mcpstill work and it never makes the one live server ambiguous;disconnect_from_mcpwithforget: truedrops the record. Forwarded notifications carry the connection name in_meta["run-mcp/connection"]. - Protocol era: pass
protocol: "2026-07-28"toconnect_to_mcpto test the modern path of a server that serves both eras; the reply names the era it got. See Protocol revisions.
Finding the server you are working on
list_available_mcp_servers reads every MCP config the clients on this machine keep: Claude Code in all three scopes (user, project .mcp.json, and the per-project local scope that claude mcp add writes by default), Claude Desktop, Cursor, VS Code (mcp.json and settings.json), Cline, Copilot CLI, Codex CLI (config.toml), Gemini CLI, Windsurf, and Zed. Files are parsed as JSON with comments and trailing commas, or as Codex's TOML. Each entry reports its name, source, file, type, command, args, cwd, env_keys, header names, and whether the client has it disabled; values of env and headers are never shown. A file that exists but cannot be parsed, or an entry that cannot be launched, is reported with its reason instead of silently skipped, so "my server isn't listed" always has an answer. filter narrows by name, command, source, or file; file reads one explicit config instead of scanning; --scan on the run-mcp command also walks up from the working directory for any JSON file with an mcpServers block.
Interception
To protect the agent's context, tool results are processed before they are returned:
| Feature | Behavior |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Image extraction | type: "image" responses with base64 data are saved to disk. Replaced with [Image saved to /path/to/img.png (24KB)] |
| Audio extraction | type: "audio" responses with base64 data are saved to disk. Replaced with [Audio saved to /path/to/audio.wav (12KB)] |
| Base64 detection | Text responses that are entirely base64-encoded (1000+ chars) are also saved as images |
| Timeouts | Tool calls are wrapped in a configurable timeout (default 5 minutes, --timeout to change), counting server time only |
| Truncation | Text exceeding the limit (default 50K chars, --max-text to change) is saved in full to disk; the reply keeps the head plus a result id, navigable via the read_result tool |
A well-behaved tool result under the limits passes through untouched. Saved media and spilled results older than 7 days are pruned from the output directory when the agent server starts.
One message is capped at 64 MB on the wire. A result that large costs the SDK's read buffer many times its size in transient memory (a 20 MB result peaks near 1.4 GB, measured), so past the cap the transport closes the connection and the call reports that it did, with the size, rather than letting the process grow without bound. A tool that has that much to say should write it to a file and return the path.
Usage
run-mcp [options] the agent server, on stdio
run-mcp <subcommand> [options] -- <cmd> one headless call| Option | Description |
| :--- | :--- |
| -V, --version | output the version number |
| -o, --out-dir <path> | Directory to save intercepted images, audio, and spilled results |
| -t, --timeout <ms> | Default tool call timeout in milliseconds (default: 300000) |
| --max-text <chars> | Max text response length before truncation (default: 50000) |
| -m, --media-threshold <kb> | Media size threshold in KB to save to disk (0 to always save, -1 to keep inline) |
| --scan | Let list_available_mcp_servers also walk up from the working directory for any JSON file containing mcpServers |
| --protocol <mode> | Handshake to open with: legacy (default, the 2025 initialize), auto (probe for 2026-07-28, fall back to legacy), or a revision to pin such as 2026-07-28 (no fallback) |
| -h, --help | display help for command |
Headless mode (from a shell)
run-mcp exposes subcommands that print clean JSON to stdout and keep status messages on stderr. Without --session each command spawns the server fresh, which is fine for CI and jq one-liners. For a dev loop, use sessions: the server stays up, and reconnect/stderr/validate work against the running instance.
If you're an agent driving run-mcp through a shell tool that merges stdout and stderr: use
--session. A sessioned call prints nothing but the JSON result, and the server's stderr is reachable as data (run-mcp stderr --session <name>, or thestderrfield ofcall --raw) instead of interleaving with your output.
call [options] <tool> [json_args] [target_command...]list-tools [options] [target_command...]list-resources [options] [target_command...]list-prompts [options] [target_command...]read [options] <uri> [target_command...]describe [options] <tool> [target_command...]get-prompt [options] <name> [json_args] [target_command...]stderr [options] [count] [target_command...]reconnect [options] [target_command...]sessionsclose-session <session_name>validate [options] [target_command...]With no subcommand, run-mcp is an MCP server for your agent (stdio). Register it once:{"mcpServers": {"run-mcp": {"command": "npx","args": ["-y", "run-mcp"]}}}
Use run-mcp <subcommand> --help for specific command options.
The -- separator
Separate the target command with -- whenever it has flags of its own:
run-mcp list-tools -- node my-server.js --verboseWithout a separator, flags after the target command belong to the target and cannot be checked. With one, a misspelled run-mcp flag is an error (exit 64, with a "did you mean") rather than something silently ignored: --sesion does not quietly run without a session.
Shorthand arguments
Instead of escaping JSON on the command line, pass arguments HTTPie-style. Every word after the tool name is an argument:
run-mcp call greet name=Alice count:=5 -- node my-server.jskey=valueis a stringkey:=jsonis parsed as JSON (number, boolean, array, object, null)- a bare word with no
=is an error (exit 65), not a silent drop - prompt arguments must be strings, so
get-promptrefuseskey:=json
Persistent sessions (the dev loop from a shell)
Pass --session <name> and the first call spawns a background daemon that keeps the server running; every later command with the same name attaches to it, needs no target command, and prints nothing but the result:
# First call spawns the session (and, say, launches the browser)
run-mcp call browser_launch headless:=true --session main -- node browser-server.js
# Later calls reuse the running server — no cold start, no progress lines
run-mcp call browser_navigate url=https://google.com --session main
run-mcp list-tools --session main
# The server's stderr, as a JSON array of lines: the first 100 and the last 1,000 since it
# started (a marker line names anything elided between them), or the last N
run-mcp stderr --session main
run-mcp stderr 20 --session main
# Edit your server's code, then restart it and see what your edit changed
run-mcp reconnect --session main
# { "reconnected": true, "pid": 4242, "command": "node browser-server.js",
# "changes": ["Changes since last connection:", " Tools: +1 added (browser_pdf)"] }
# If the new code fails to start, the result carries the exit and the crash output inline
# ({ "reconnected": false, "error": ..., "exit": "The server exited with code 1.", "stderr": [...] });
# the old process is gone, `stderr --session` still shows why, and `reconnect` again once it's fixed.
# Checks against the running instance
run-mcp validate --deep --session main
# Stop the server and the daemon
run-mcp close-session main--show-stderr on a sessioned call replays the stderr the server wrote during that call (the daemon holds the pipe, so it can't stream live). --out-dir, --timeout, and --media-threshold apply per call, exactly as without a session. --env and --protocol are fixed when the session is created.
Keeping track of sessions. run-mcp sessions lists what's running (name, pid, command, working directory, env keys, uptime, idle timeout) as JSON. A session remembers the command, directory, and env it was started with: if you pass a different command (or the same relative command from a different directory, or a different --env) with an existing session name, the call is refused with the difference shown, rather than quietly answered by the wrong server. Omit the command to attach, close-session to replace. A reconnect that lands while another call is in flight fails that call with a message saying a reconnect interrupted it.
The daemon leaves evidence. Its own stderr goes to <name>.log beside the session file, so a daemon that dies is explained by the next call rather than reported as "not running". A daemon that accepts a request but never answers fails the call after the operation's timeout plus a margin, with a message that says whether to suspect the server or the daemon.
Nothing leaks. A session lives until you close-session it, which means a forgotten one keeps its server (and whatever the server holds, like a browser) alive until reboot. Pass --idle-timeout <minutes> on any sessioned call to have it close itself after that long without a command; the value shows up in sessions. If the server fails to start on the first sessioned call, the call exits 69 with the server's stderr, and no session is left behind. The daemon listens on a Unix socket (a named pipe on Windows) inside an owner-only directory under your temp dir, so no other user on the machine can reach your server through it. A session record whose daemon is not answering is pruned, even if its pid has been reused.
Exit codes
| Code | Meaning |
| ---- | ------- |
| 0 | success |
| 1 | the tool reported isError, or a session/daemon error |
| 64 | usage error: bad flag, missing target command, unknown tool |
| 65 | malformed input: invalid JSON or shorthand arguments |
| 66 | the target command was not found |
| 69 | the target server failed to start or connect |
| 70 | an internal error in run-mcp itself (RUN_MCP_DEBUG=1 for the stack) |
Client input from a shell
There is no human behind a headless call, so a server that asks for input gets a deterministic answer instead of a hang: elicitation is declined, sampling is refused. The call still completes, and call --raw reports what was asked under input_requests (counts, wait time, and one entry per round).
Environment variables
The target server does not inherit your shell's environment. Like every MCP client, run-mcp starts it with only a small whitelist (PATH, HOME, SHELL, USER, and their Windows equivalents), so API_KEY=… run-mcp … does not reach it. Pass what your server reads explicitly:
# repeat --env (or -e) per variable; the first "=" splits key from value
run-mcp call search q=hello --env API_KEY=sk-123 -- node my-server.js
# Sessions remember the env they were started with; attach without repeating it
run-mcp list-tools --session dev --env API_KEY=sk-123 -- node my-server.js
run-mcp call search q=hello --session devFrom the agent server, pass env to connect_to_mcp (or auto_connect.env); it is kept across reconnect_to_mcp. list_available_mcp_servers reports each configured server's env_keys so an agent knows what to pass.
HTTP targets
A target that starts with http:// or https:// is connected over Streamable HTTP instead of being spawned:
run-mcp list-tools -- http://localhost:3000/mcpEverything that depends on owning the process is stdio-only and says so: there is no stderr, no exit code, no stdout-pollution check, and reconnect cannot restart a server run-mcp did not start. The deprecated HTTP+SSE transport is not supported.
Protocol revisions: 2025 vs 2026-07-28
MCP has two eras. Every revision through 2025-11-25 opens with an initialize handshake, pushes notifications unsolicited, and lets a server send elicitation/create / sampling/createMessage / roots/list requests to the client. Revision 2026-07-28 starts the modern era: a server/discover probe instead of initialize, change notifications only over a subscriptions/listen stream the client opens, and client input requested in-band by returning input_required from a tool. A server built on SDK v2 with serveStdio serves both from one factory, and a client that connects the old way gets the old behaviour, including the SDK's legacy shim for input_required, without any sign that the modern path was never exercised.
run-mcp makes the era explicit and lets you choose it:
run-mcp list-tools -- node my-server.js # legacy handshake (default)
run-mcp --protocol auto list-tools -- node my-server.js # probe; modern if the server offers it, else legacy
run-mcp --protocol 2026-07-28 list-tools -- node my-server.js # modern only; fails loudly if the server can't
run-mcp validate --deep --protocol 2026-07-28 -- node my-server.js- The era shows up everywhere: headless
Connectedlines and the--rawenvelope'sprotocolfield,connect_to_mcp/mcp_server_status, andvalidate(protocolEra,protocolVersion). - Sessions remember it:
--protocolis fixed when the session is created; a later call asking for a different one is refused, like a different command or--env. - Subscriptions follow the era. On a modern connection
run-mcpopens alistenstream for everylistChangedtype your server advertises, andsubscribe_to_resourceopens a per-URI stream. What the server honored is reported, so a filter it accepted but will never deliver on is visible. - The default stays legacy on purpose: a spawn-per-invocation tool must not pay a probe on every connect, and a probe would change what a legacy server sees. Pass
autoor a pin when you mean it.
Sampling, roots, and the logging capability are deprecated as of 2026-07-28 but stay in the spec for at least twelve months; run-mcp keeps exercising them.
validate
run-mcp validate -- node my-server.js (or validate_mcp_server from the agent) spawns the server, connects, and reports the mistakes the SDK forgives and a stricter client would not: stdout pollution, transport errors, crash signatures in stderr, capabilities advertised but not served (and served but not advertised), tool and prompt entries a client would reject (a required property never defined under properties, duplicate tool names, non-object schemas), and, on a 2026-07-28 connection, whether the server honored the subscriptions/listen filter it advertised. --deep prints every check; --json prints the report; a dead server's report carries its exit and stderr. It is deliberately not a wire-format conformance suite; the official MCP conformance runner is the place for that.
Development
npm install # Node.js 20.11+
npm run build # one-time build (also regenerates the README's help tables)
npm run dev # rebuild on save
npm test # build, then the full suite
node dist/index.js list-tools -- node --import tsx tests/fixtures/mock-server.tsArchitecture and conventions are in AGENTS.md.
License
MIT
