mcp-stdio-guard
Catch stdout pollution and handshake failures in MCP stdio servers before clients do.
Install / Use
claude mcp add 1Utkarsh1 -- npx -y github:1Utkarsh1/mcp-stdio-guardIf the server publishes to npm under a different name, use that package instead — check the repo README.
MCP Server
Model Context Protocol server
Quality Score
Category
Development & EngineeringSupported Platforms
Skill content
View source on GitHubMCP stdio servers use stdout as their protocol channel. Debug text, banners, progress logs, console.log, Python print, or any other stray stdout output can corrupt the stream and make clients fail in confusing ways.
mcp-stdio-guard starts your server, performs a real MCP initialize handshake, probes advertised tools, resources, and prompts list capabilities, optionally sends a real post-initialize MCP request such as tools/list, validates every stdout frame, checks returned tool metadata, and scans source for risky stdout calls.
Why This Exists
The latest MCP docs say stdio servers must send JSON-RPC messages on stdout, may log to stderr, and must complete the initialize then notifications/initialized lifecycle before normal operation.
That is easy to get wrong in real servers. This guard turns that fragile process boundary into a fast local check and a CI gate.
<p align="center"> <img src="assets/protocol-flow.svg" alt="Protocol flow tested by mcp-stdio-guard" width="100%" /> </p>Install
From npm:
npx mcp-stdio-guard -- node ./server.js
From this repo:
git clone https://github.com/1Utkarsh1/mcp-stdio-guard.git
cd mcp-stdio-guard
npm ci
npm test
Quickstart
Run your MCP server behind the guard:
mcp-stdio-guard -- node ./server.js
Use a deterministic profile for common workflows:
mcp-stdio-guard --profile registry --json -- node ./server.js
Use a config file for registry runs that need environment names, request lists, or explicitly safe tool calls:
mcp-stdio-guard --config mcp-stdio-guard.config.json
Exercise a real MCP operation after initialization:
mcp-stdio-guard --request tools/list -- node ./server.js
Scan source for obvious stdout writes too. Findings are warnings unless --fail-on-static is set:
mcp-stdio-guard --scan src --fail-on-static --request tools/list -- node ./server.js
JSON output for CI:
mcp-stdio-guard --json --request tools/list -- node ./server.js
Repeat the same guard to catch cold/warm startup behavior:
mcp-stdio-guard --repeat 2 --request tools/list -- node ./server.js
What It Catches
<p align="center"> <img src="assets/terminal-demo.svg" alt="Passing and failing terminal output examples" width="100%" /> </p>| Problem | Runtime check | Static scan |
| --- | --- | --- |
| console.log("starting") before server startup | Yes | Yes |
| Dependency/import-time stdout pollution | Yes with --repeat | No |
| Python print("debug") in a stdio server | Yes | Yes |
| Late stdout logs after initialize | Yes | Partial |
| Invalid JSON-RPC frames | Yes | No |
| Server crash after notifications/initialized | Yes | No |
| Missing initialize or operation response | Yes | No |
| Duplicate tool names or invalid inputSchema.required | Yes with --request tools/list | No |
| Cold/warm protocol, capability, or tool-list drift | Warning with --repeat | No |
| stderr diagnostics | Allowed | Allowed |
Live MCP Coverage
The test suite creates real servers with @modelcontextprotocol/sdk@1.29.0 and verifies:
| Scenario | Expected result |
| --- | --- |
| clean SDK stdio server through initialize and tools/list | Pass |
| SDK server with startup stdout pollution | Fail |
| SDK server with stderr diagnostics | Pass |
| SDK server with late stdout pollution after connection | Fail |
| hand-rolled server that ignores post-initialize requests | Fail |
| server that crashes after initialized notification | Fail |
Commands
mcp-stdio-guard [options] -- <command> [args...]
| Option | Description |
| --- | --- |
| --config <path> | read a JSON config file for registry runs and explicitly safe tool calls |
| --profile <name> | apply a deterministic guard profile: custom, smoke, registry, ci, or strict |
| --protocol <version> | MCP protocol version to send, default 2025-11-25 |
| --timeout <ms> | initialize and request timeout, default 5000 |
| --max-stdout-bytes <n> | total stdout byte limit, default 1048576 |
| --max-stdout-line-bytes <n> | single stdout line byte limit, default 262144 |
| --max-stderr-bytes <n> | retained stderr byte limit, default 1048576 |
| --repeat <count> | run the same guard multiple times to catch cold/warm startup behavior |
| --request <method> | send one MCP request after initialization, for example tools/list |
| --params <json> | JSON params for --request |
| --adversarial-probe <name> / --adversarial-probes <list> | opt into strict protocol probes: invalid-method, invalid-params, notification, malformed-json, all, or none |
| --scan <path> | scan source for risky stdout writes and visible startup-output risks |
| --fail-on-static | make static scan findings fail the command |
| --json | print machine-readable output |
| --cwd <path> | run the server command from a specific directory |
| --help | show help |
Profiles
Profiles are deterministic presets for common workflows. Existing CLI behavior remains the default custom profile, so current commands keep working unless --profile is provided.
| Profile | Behavior |
| --- | --- |
| custom | preserve explicit CLI flags and legacy defaults |
| smoke | initialize only unless --request is provided; skip advertised tools/list, resources/list, and prompts/list probes |
| registry | run advertised list probes and repeat twice by default for cold/warm consistency |
| ci | emit JSON output and make static scan findings fail when --scan is used |
| strict | combine CI-style output/static failures, registry-style repeat depth, and built-in adversarial protocol probes |
Explicit flags can still narrow or deepen a profile. For example, --profile registry --repeat 1 keeps registry capability probing but disables the repeat preset. Use --profile strict --adversarial-probes none if you want strict JSON/static behavior without adversarial inputs.
Config Files
Config files let registries run repeatable checks without hiding what was executed. The file is JSON, and CLI flags still override matching config defaults. Parsing happens before the server process starts, so invalid config does not launch the target command.
Supported fields:
| Field | Meaning |
| --- | --- |
| command | command as either ["node", "./server.js"] or "node" with args |
| args | arguments used only when command is a string |
| cwd | working directory, resolved relative to the config file |
| env | environment variables to pass; values are redacted in JSON output |
| profile, protocol, timeoutMs, repeat, json | same meaning as CLI options |
| maxStdoutBytes, maxStdoutLineBytes, maxStderrBytes | byte limits for untrusted child output |
| scan or scanPath | source scan path, resolved relative to the config file |
| failOnStatic | make static scan findings fail |
| request | one explicit post-initialize request: { "method": "tools/list" } |
| requests | list of explicit post-initialize requests |
| safeToolCalls | opt-in tools/call recipes; no tool is called unless listed here or explicitly requested |
| adversarialProbes | opt-in built-in probes as true, "all", "none", or a list of probe names |
| adversarialToolCalls | opt-in invalid-argument tools/call probes for configured safe tools |
Example:
{
"profile": "registry",
"command": ["node", "./server.js"],
"cwd": ".",
"json": true,
"env": {
"API_TOKEN": "set-in-runner"
},
"requests": [
{ "method": "tools/list" }
],
"adversarialProbes": ["invalid-method", "notification"],
"safeToolCalls": [
{ "name": "echo", "arguments": { "text": "hello" } }
],
"adversarialToolCalls": [
{ "name": "echo", "arguments": { "unexpected": true } }
]
}
The guard does not discover and call arbitrary tools from tools/list. Tool execution only happens through an explicit safeToolCalls entry or an explicit tools/call request you provide.
Adversarial probes are off by default because they intentionally send unusual inputs. Built-in probes check that unknown methods return structured errors, invalid params return structured errors, notifications do not receive responses, and malformed JSON does not crash the process. adversarialToolCalls is separate because it calls a named tool with intentionally invalid arguments; only use it for tools you control and consider safe/idempotent.
JSON Contract
--json is intended for CI, registries, and badge ingestion. The current contract is schemaVersion: 1; new fields may be added, but these fields are stable for consumers:
| Field | Meaning |
| --- | --- |
| schemaVersion | JSON contract version, currently 1 |
| ok | true when no error-severity issue was found |
| config | config file metadata and checks used, or { "enabled": false, ... } |
| profile | selected guard profile, for example custom, smoke, registry, ci, or strict |
| command | command and arguments that were validated |
| protocol | MCP protocol version sent by the guard |
| negotiatedProtocol | protocol version returned by the server, when available |
| initialized | whether the server completed the initialize handshake |
| operation | post-initialize request result, or null when --request was not used |
| operations | all explicit post-initialize requests, including config requests and safe tool calls |
| adversarial | opt-in adversarial probe results, including status, risk text, and per-probe issue codes |
| toolSchema | summary of tools/list metadata validation when that operation was requested or probed from an advertised tools capability |
| capabilityProbes | whether advertised capability list probes were enabled for this run |
| capabilityKeys | sorted capability keys returned by initialize for a single run; repeat mode exposes this inside each runs entry |
| capabilityChecks | advertised capability probes observed during a single run; repeat mode exposes this inside each runs entry |
| drift | repeat-run comparison summary for negotiated protocol, advertised capabilities, tool names/counts, and resource/prompt list counts |
| process | startup, timeout, exit code, signal, and guard-termination metadata for a single run; repeat mode exposes this inside each runs entry |
| checks | badge-friendly per-class statuses |
| issueClasses | registry-friendly summary grouped by installRuntime, stdioTransport, and mcpProtocol |
| summary | badge-friendly aggregate status, primary issue, issue counts, a
Truncated for display — read the full file on GitHub.
Related Skills
Agent-Reach
84.2kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
headroom
73.4kCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
ruflo
73.0k🌊 The original agent harness. Deploy intelligent multi-player swarms, coordinate autonomous workflows, and build conversational AI systems. Features adaptive memory, self-learning intelligence, federation, vector RAG integration, and native Claude Code / Codex / Hermes and many more Integrated
career-ops
72.3kOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)
