Skip to content

Repository files navigation

mcp-surgeon

eslint + vitest for your MCP server. Catches silent failures before your users do.

MCP servers fail quietly. A tool returns isError: false with a body that says "Context added" — but nothing was written. The model believes it succeeded. The user believes it succeeded. Nobody finds out until the data is missing.

Anthropic's Inspector is a great manual debugger, but it can't catch this: it transports the response, it doesn't read intent out of it. mcp-surgeon does — in your terminal, in CI, in seconds.

Quick start

npx mcp-surgeon lint -- node dist/index.js         # static pass
npx mcp-surgeon lint --call -- node dist/index.js  # + probe each tool live
  ✖ [error-semantics] sync_data: Tool "sync_data" returned isError=false but its
    body is a self-declared error object ("...object":"error","message":"..."").
    Programmatic callers will treat this as success. Set isError:true when the
    operation fails.
  ⚠ [error-semantics] save_note: Tool "save_note" returned isError=false but its
    text reads like a failure ("...record not found..."). If this is an error,
    set isError:true; if it is normal output, make the success text unambiguous.

mcp-surgeon: 1 error(s), 1 warning(s).

--call invokes every tool once with minimal arguments derived from its schema and lints the live responses. It is opt-in because calling tools can have side effects on a real server.

No Docker. No service. Just Node.

Remote servers (HTTP)

Point it at a URL instead of a command and mcp-surgeon connects over Streamable HTTP. Use --insecure for a self-signed test server:

npx mcp-surgeon lint -- https://host/mcp
npx mcp-surgeon lint --header 'Authorization: Bearer TOKEN' -- https://host/mcp
npx mcp-surgeon lint --insecure -- https://test-host/mcp   # skips TLS checks

Auth from .env. For an HTTP target, set MCP_AUTH_TOKEN (in your shell or a gitignored .env) and it is sent as Authorization: Bearer <token>. Use MCP_AUTHORIZATION to set the header verbatim for non-Bearer schemes. An explicit --header 'Authorization: …' overrides both.

# .env  →  MCP_AUTH_TOKEN=your-real-token
npx mcp-surgeon lint --read-only -- https://host/mcp

--insecure disables TLS certificate verification — only for testing, never a public server.

Servers that need credentials

mcp-surgeon launches the target server as a subprocess and forwards your shell environment to it — exactly how an MCP host (Claude Desktop, Cursor) starts a server. The variable name is whatever that server documents (TOKEN, API_KEY, …); you set it, mcp-surgeon passes it through:

# set whatever variable the target server reads, then lint it:
TOKEN=your-token npx mcp-surgeon lint --call -- npx -y your-mcp-server

Or keep the keys in a local .env file — mcp-surgeon loads .env from the current directory and forwards it to the target. Copy .env.example to .env (already gitignored) and fill in the variables the server documents:

cp .env.example .env      # then edit .env
npx mcp-surgeon lint --call -- npx -y your-mcp-server

No credentials are needed for the static pass (it only reads tools/list). They matter for --call, so tools run against a real, authenticated backend.

Safety. --call invokes every tool, including ones that write or delete. With valid credentials those calls hit your real data. Use --read-only to probe only tools that are safe to call (a readOnlyHint annotation, or failing that a read-only name like get_/list_/search_); the rest are skipped and listed. Point --call at a throwaway or minimally-scoped account, never production.

# safe against a real server: only reads, never writes:
npx mcp-surgeon lint --read-only -- https://host/mcp

Probing with real data

By default the probe fills required arguments with schema-derived placeholders ("probe", 0), so it mostly exercises a server's not-found / invalid-input branches. Those findings are real — the error path lies about success — but they are not the happy path. To probe with your data, map tool names to real arguments in a JSON file and pass --args-file:

{
  "get_page":  { "page_id": "263fc4a7-…-real-id" },
  "get_user":  { "user_id": "b32d84e2-…-real-id" }
}
npx mcp-surgeon lint --read-only --args-file probes.json -- npx -y your-mcp-server

Listed tools are called with your arguments; unlisted tools keep placeholders. Every finding prints the exact call in its header — get_page({"page_id":"probe"}) — so you can always tell which branch a finding came from.

Rules (v0)

Rule Severity Catches
error-semantics error / warning A success envelope (isError:false) over a failure body. error when the body is a self-declared error object ({"object":"error"}, HTTP-ish status ≥ 400); warning when it only reads like a failure in prose. Severity tracks how sure the rule is.
tool-schema-valid error Missing / non-object inputSchema
tool-has-description warning Tools the model can't reason about

Use it from your AI agent

mcp-surgeon ships Claude Code skills — mcp-audit (lint a server + interpret) and mcp-fix (apply the fixes + re-verify). Install the package and just ask your agent to audit your MCP server.

Status

Early. Static rules + the dynamic call harness (--call) work today — error-semantics fires on live silent failures. mcp-surgeon test snapshots and a GitHub Action are next.

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages