mcp-down-wrapper
v1.0.1
Published
Wraps an MCP server command so that a failed/missing server reports zero tools instead of a load failure.
Readme
mcp-down-wrapper
A transparent wrapper for MCP server commands. When the wrapped server fails to start — bad binary, missing dependency, crash during boot, whatever — Claude sees a server that loaded successfully with zero tools, instead of a "failed to load" warning.
The problem
Claude's MCP client shows a hard failure warning any time a configured server process exits or errors before completing its handshake. That's the right default, but it means a single flaky/optional MCP server (e.g. one that depends on a local service like ArangoDB being up) throws a scary warning every time that dependency isn't running — even if you don't care about that server's tools right now.
mcp-down-wrapper sits between Claude and the real server command. If the
real command starts fine, the wrapper is invisible — it proxies stdio
byte-for-byte. If the real command fails before producing any output, the
wrapper answers the MCP handshake itself and reports an empty tool list, so
Claude treats it as a normal, healthy, toolless server.
How it works
The wrapper is invoked as:
mcp-down-wrapper <command> [args...]<command> [args...] is exactly what used to be your server's own
command/args — the wrapper spawns it verbatim, inheriting env from its
own process (which Claude already populated from your MCP config).
- Child starts and talks normally: stdin/stdout are proxied directly between Claude and the child process. The wrapper does not parse or alter any protocol messages in this path — it's a transparent pipe.
- Child fails to spawn (e.g.
ENOENT) or exits before writing a single byte to stdout: the wrapper switches into a minimal built-in MCP responder. It replays whatever Claude had already sent (so theinitializerequest isn't lost) and answers:initialize→ normal handshake response, notoolscapability advertisedtools/list→{ "tools": [] }ping→{}- anything else → JSON-RPC "method not found" (
-32601)
- Child crashes after already producing real output: this is treated as a genuine runtime crash, not a failed start, and the wrapper just exits with the child's exit code. Masking only applies to startup failures.
The child's stderr is always inherited straight through, so real crash
logs and error messages are still visible for debugging — only the
protocol-level "did the server fail to load" signal is smoothed over.
Usage
Wrap any existing MCP server config by moving its command/args inside a
mcp-down-wrapper invocation. env stays exactly as it was.
Before:
{
"command": "npx",
"args": ["arango-server"],
"env": {
"ARANGO_URL": "http://localhost:8529",
"ARANGO_USERNAME": "root",
"ARANGO_PASSWORD": "root"
}
}After (published to npm):
{
"command": "npx",
"args": ["mcp-down-wrapper", "npx", "arango-server"],
"env": {
"ARANGO_URL": "http://localhost:8529",
"ARANGO_USERNAME": "root",
"ARANGO_PASSWORD": "root"
}
}Note that args is a real array — "mcp-down-wrapper" and "npx" are
separate elements, not one string. The wrapper is intentionally generic: the
first argument is whatever command needs to run (npx, uvx, node,
python, ...), and everything after it is that command's own arguments.
Running it locally (not published)
Two options, since this package isn't on the npm registry:
Option A — point npx at the project folder directly:
{
"command": "npx",
"args": [
"/absolute/path/to/mcp-down-wrapper",
"npx",
"arango-server"
],
"env": {
"ARANGO_URL": "http://localhost:8529",
"ARANGO_USERNAME": "root",
"ARANGO_PASSWORD": "root"
}
}npm 7+ resolves a local folder path directly to its bin, so this works
without installing or publishing anything.
Option B — call node on the built file directly (most deterministic):
{
"command": "node",
"args": [
"/absolute/path/to/mcp-down-wrapper/dist/index.js",
"npx",
"arango-server"
],
"env": {
"ARANGO_URL": "http://localhost:8529",
"ARANGO_USERNAME": "root",
"ARANGO_PASSWORD": "root"
}
}Option C — npm link for a plain npx mcp-down-wrapper ... invocation:
cd /absolute/path/to/mcp-down-wrapper
npm linkThis registers mcp-down-wrapper globally, so you can use the exact same
config shape as the "published to npm" example above, with no path or
version to keep track of.
How to build
Prerequisites: Node.js 20+ and npm.
git clone <this-repo>
cd mcp-down-wrapper
npm install # installs devDependencies and builds via the `prepare` script
npm run build # compiles src/index.ts -> dist/index.jsdist/ is a build artifact and is gitignored — rebuild it after pulling
changes or editing src/index.ts. There's no test suite, so validate a build
by pointing one of the local usage options
at your freshly built dist/index.js.
Development
There's no test suite; the behavior was verified manually against three
scenarios: a well-behaved child (pure passthrough), a nonexistent command
(ENOENT), and a child that exits immediately without writing to stdout
(both fall back to the empty-tools response).
Limitations
- Only startup failures are masked. A server that starts fine and crashes later behaves normally (wrapper exits with the child's code) — Claude already knows about its tools by that point, so there's nothing sensible to hide.
- The fallback responder does not advertise
resourcesorpromptscapabilities, but still answersresources/list/prompts/listwith empty arrays defensively in case a client calls them anyway. - No shell is used to spawn the child (
child_process.spawnwithoutshell: true), so shell syntax inargswon't be interpreted — pass a real command and argv, same as you would in the original unwrapped config.
