# run-mcp ### An MCP server your coding agent uses to run, test, and diagnose the MCP server it is writing. [![CI](https://github.com/funkyfunc/run-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/funkyfunc/run-mcp/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/run-mcp)](https://www.npmjs.com/package/run-mcp) [![license](https://img.shields.io/npm/l/run-mcp)](./LICENSE) 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: 1. **Agent MCP Server**, the default. `npx -y run-mcp` is an MCP server exposing `connect_to_mcp`, `call_mcp_primitive`, `reconnect_to_mcp`, and nine other tools. [Add it to your agent](#add-run-mcp-to-your-agent) and ask it to test your server. 2. **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 ` and the server stays up between commands, with `reconnect`, `stderr`, and `validate` against 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: ```bash npx -y run-mcp list-tools -- node path/to/my-mcp-server.js ``` To 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](#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: ```bash npm install -g run-mcp ``` Requires Node.js 20.11 or newer. To work on `run-mcp` itself, see [Development](#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, call `search` with `q=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: ```bash claude mcp add run-mcp -- npx -y run-mcp claude mcp add --scope project run-mcp -- npx -y run-mcp ``` Or add it to `.mcp.json` in the repo by hand: ```json { "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: ```json { "mcpServers": { "run-mcp": { "command": "npx", "args": ["-y", "run-mcp"] } } } ``` ### Cursor `.cursor/mcp.json` in the project (or `~/.cursor/mcp.json` for every project): ```json { "mcpServers": { "run-mcp": { "command": "npx", "args": ["-y", "run-mcp"] } } } ``` ### VS Code (Copilot agent mode) `.vscode/mcp.json` uses a `servers` key and a `type`: ```json { "servers": { "run-mcp": { "type": "stdio", "command": "npx", "args": ["-y", "run-mcp"] } } } ``` Or from a shell: ```bash code --add-mcp '{"name":"run-mcp","command":"npx","args":["-y","run-mcp"]}' ``` ### Codex CLI ```bash codex mcp add run-mcp -- npx -y run-mcp ``` Equivalent to this block in `~/.codex/config.toml` (or a project's `.codex/config.toml`): ```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: ```bash gemini mcp add run-mcp npx -y run-mcp ``` Equivalent to this in `.gemini/settings.json` or `~/.gemini/settings.json`: ```json { "mcpServers": { "run-mcp": { "command": "npx", "args": ["-y", "run-mcp"] } } } ``` ### Windsurf `~/.codeium/windsurf/mcp_config.json`, same `mcpServers` shape as Cursor: ```json { "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 look ``` ### When 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_stderr` prefixes each line with its arrival time and stream position (`12:01:03.412 #42 …`) and ends with `next: since=N`; pass that as `since` to see only what the server said after your last look. `timestamps: false` gives 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=N` pointer for the rest, so a chatty startup does not flood the agent's context. - From a shell, `run-mcp call --raw` includes a `stderr` array in the result envelope, and `run-mcp stderr` prints 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 `roots` to `connect_to_mcp` (or `reconnect_to_mcp`) and your server's `roots/list` calls get a real answer. `run-mcp` advertises the roots capability, so without this your server correctly sees an empty list. Roots persist across reconnects. - **Log level**: pass `log_level` to raise your server's logging verbosity. - **Notifications**: `get_server_notifications` shows 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 with `get_server_notifications(method='resources/updated')`. On a 2026-07-28 connection this opens a `subscriptions/listen` stream and reports what the server honored. - **What your host forwards**: every connect, reconnect, and `mcp_server_status` reply 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-mcp` advertises 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 under `input_requests.rounds`, and `latency_ms` is split into `server_ms` and `input_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: true` the reply says what it did with it: `Progress: 4 update(s) streamed; last 4/4 "step 4" at 0.3s.` or `Progress: none — a progress token was offered and the server sent no notifications/progress`, with the events under `progress` in the JSON. The updates are also listed by `get_server_notifications(method='progress')`, and `call --raw` carries the same `progress` field. 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 `--timeout` in 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_mcp` a `name` and pass it as `connection` to 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), and `mcp_server_status` shows all of them. A disconnected connection keeps its record, so its stderr and `reconnect_to_mcp` still work and it never makes the one live server ambiguous; `disconnect_from_mcp` with `forget: true` drops the record. Forwarded notifications carry the connection name in `_meta["run-mcp/connection"]`. - **Protocol era**: pass `protocol: "2026-07-28"` to `connect_to_mcp` to test the modern path of a server that serves both eras; the reply names the era it got. See [Protocol revisions](#protocol-revisions-2025-vs-2026-07-28). ### 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 [options] -- one headless call ``` | Option | Description | | :--- | :--- | | `-V, --version` | output the version number | | `-o, --out-dir ` | Directory to save intercepted images, audio, and spilled results | | `-t, --timeout ` | Default tool call timeout in milliseconds (default: 300000) | | `--max-text ` | Max text response length before truncation (default: 50000) | | `-m, --media-threshold ` | 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 ` | 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](#persistent-sessions-the-dev-loop-from-a-shell)**: 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 `, or the `stderr` field of `call --raw`) instead of interleaving with your output. - `call [options] [json_args] [target_command...]` - `list-tools [options] [target_command...]` - `list-resources [options] [target_command...]` - `list-prompts [options] [target_command...]` - `read [options] [target_command...]` - `describe [options] [target_command...]` - `get-prompt [options] [json_args] [target_command...]` - `stderr [options] [count] [target_command...]` - `reconnect [options] [target_command...]` - `sessions` - `close-session ` - `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 --help` for specific command options. ### The `--` separator Separate the target command with `--` whenever it has flags of its own: ```bash run-mcp list-tools -- node my-server.js --verbose ``` Without 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: ```bash run-mcp call greet name=Alice count:=5 -- node my-server.js ``` - `key=value` is a string - `key:=json` is 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-prompt` refuses `key:=json` ### Persistent sessions (the dev loop from a shell) Pass `--session ` 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: ```bash # 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 `.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 ` 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: ```bash # 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 dev ``` From 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: ```bash run-mcp list-tools -- http://localhost:3000/mcp ``` Everything 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: ```bash 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 `Connected` lines and the `--raw` envelope's `protocol` field, `connect_to_mcp` / `mcp_server_status`, and `validate` (`protocolEra`, `protocolVersion`). - **Sessions remember it:** `--protocol` is 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-mcp` opens a `listen` stream for every `listChanged` type your server advertises, and `subscribe_to_resource` opens 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 `auto` or 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 ```bash 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.ts ``` Architecture and conventions are in [AGENTS.md](./AGENTS.md). ## License MIT