npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Readme

run-mcp

An MCP server your coding agent uses to run, test, and diagnose the MCP server it is writing.

CI npm 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 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 <name> 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:

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 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-mcp

Requires 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, 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:

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:

{
  "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-mcp

Equivalent 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-mcp

Equivalent 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 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 <tool> --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.

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 the stderr field of call --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...]
  • sessions
  • close-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 --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:

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 <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 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:

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:

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

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.

License

MIT