[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81064":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":13,"contributorsCount":12,"subscribersCount":12,"size":12,"stars1d":12,"stars7d":12,"stars30d":12,"stars90d":12,"forks30d":12,"starsTrendScore":12,"compositeScore":12,"rankGlobal":9,"rankLanguage":9,"license":14,"archived":15,"fork":15,"defaultBranch":16,"hasWiki":17,"hasPages":15,"topics":18,"createdAt":9,"pushedAt":9,"updatedAt":30,"readmeContent":31,"aiSummary":32,"trendingCount":12,"starSnapshotCount":12,"syncStatus":33,"lastSyncTime":34,"discoverSource":35},81064,"mcp-stdio-guard","1Utkarsh1\u002Fmcp-stdio-guard","1Utkarsh1","Catch stdout pollution and handshake failures in MCP stdio servers before clients do.",null,"JavaScript",14,0,1,"MIT License",false,"main",true,[19,20,21,22,23,24,25,26,27,28,29],"cli","debugging","developer-tools","json-rpc","mcp","mcp-tools","model-context-protocol","nodejs","npm-package","stdio","testing-tools","2026-06-12 02:04:10","\u003Cp align=\"center\">\n  \u003Cimg src=\"assets\u002Flogo.svg\" alt=\"mcp-stdio-guard logo\" width=\"120\" \u002F>\n\u003C\u002Fp>\n\n\u003Ch1 align=\"center\">mcp-stdio-guard\u003C\u002Fh1>\n\n\u003Cp align=\"center\">\n  Catch stdout pollution and handshake failures in MCP stdio servers before clients do.\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002F1Utkarsh1\u002Fmcp-stdio-guard\u002Factions\u002Fworkflows\u002Fci.yml\">\u003Cimg alt=\"CI\" src=\"https:\u002F\u002Fgithub.com\u002F1Utkarsh1\u002Fmcp-stdio-guard\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg\" \u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmcp-stdio-guard\">\u003Cimg alt=\"npm\" src=\"https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002Fmcp-stdio-guard?color=0b6bcb\" \u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fbadge.socket.dev\u002Fnpm\u002Fpackage\u002Fmcp-stdio-guard\u002F1.0.0\">\u003Cimg alt=\"Socket\" src=\"https:\u002F\u002Fbadge.socket.dev\u002Fnpm\u002Fpackage\u002Fmcp-stdio-guard\u002F1.0.0\" \u002F>\u003C\u002Fa>\n  \u003Cimg alt=\"runtime dependencies\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fruntime%20deps-0-1f8f4c\" \u002F>\n  \u003Cimg alt=\"node\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fnode-%3E%3D18-2f855a\" \u002F>\n  \u003Ca href=\"LICENSE\">\u003Cimg alt=\"license\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-MIT-111827\" \u002F>\u003C\u002Fa>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"assets\u002Fhero.svg\" alt=\"mcp-stdio-guard hero showing a clean MCP stdio pipeline\" width=\"100%\" \u002F>\n\u003C\u002Fp>\n\nMCP 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.\n\n`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\u002Flist`, validates every stdout frame, checks returned tool metadata, and scans source for risky stdout calls.\n\n## Why This Exists\n\nThe latest MCP docs say [stdio servers must send JSON-RPC messages on stdout](https:\u002F\u002Fmodelcontextprotocol.io\u002Fspecification\u002F2025-11-25\u002Fbasic\u002Ftransports), may log to stderr, and must complete the [`initialize` then `notifications\u002Finitialized` lifecycle](https:\u002F\u002Fmodelcontextprotocol.io\u002Fspecification\u002F2025-11-25\u002Fbasic\u002Flifecycle) before normal operation.\n\nThat is easy to get wrong in real servers. This guard turns that fragile process boundary into a fast local check and a CI gate.\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"assets\u002Fprotocol-flow.svg\" alt=\"Protocol flow tested by mcp-stdio-guard\" width=\"100%\" \u002F>\n\u003C\u002Fp>\n\n## Install\n\nFrom npm:\n\n```bash\nnpx mcp-stdio-guard -- node .\u002Fserver.js\n```\n\nFrom this repo:\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002F1Utkarsh1\u002Fmcp-stdio-guard.git\ncd mcp-stdio-guard\nnpm ci\nnpm test\n```\n\n## Quickstart\n\nRun your MCP server behind the guard:\n\n```bash\nmcp-stdio-guard -- node .\u002Fserver.js\n```\n\nUse a deterministic profile for common workflows:\n\n```bash\nmcp-stdio-guard --profile registry --json -- node .\u002Fserver.js\n```\n\nUse a config file for registry runs that need environment names, request lists, or explicitly safe tool calls:\n\n```bash\nmcp-stdio-guard --config mcp-stdio-guard.config.json\n```\n\nExercise a real MCP operation after initialization:\n\n```bash\nmcp-stdio-guard --request tools\u002Flist -- node .\u002Fserver.js\n```\n\nScan source for obvious stdout writes too. Findings are warnings unless `--fail-on-static` is set:\n\n```bash\nmcp-stdio-guard --scan src --fail-on-static --request tools\u002Flist -- node .\u002Fserver.js\n```\n\nJSON output for CI:\n\n```bash\nmcp-stdio-guard --json --request tools\u002Flist -- node .\u002Fserver.js\n```\n\nRepeat the same guard to catch cold\u002Fwarm startup behavior:\n\n```bash\nmcp-stdio-guard --repeat 2 --request tools\u002Flist -- node .\u002Fserver.js\n```\n\n## What It Catches\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"assets\u002Fterminal-demo.svg\" alt=\"Passing and failing terminal output examples\" width=\"100%\" \u002F>\n\u003C\u002Fp>\n\n| Problem | Runtime check | Static scan |\n| --- | --- | --- |\n| `console.log(\"starting\")` before server startup | Yes | Yes |\n| Dependency\u002Fimport-time stdout pollution | Yes with `--repeat` | No |\n| Python `print(\"debug\")` in a stdio server | Yes | Yes |\n| Late stdout logs after `initialize` | Yes | Partial |\n| Invalid JSON-RPC frames | Yes | No |\n| Server crash after `notifications\u002Finitialized` | Yes | No |\n| Missing `initialize` or operation response | Yes | No |\n| Duplicate tool names or invalid `inputSchema.required` | Yes with `--request tools\u002Flist` | No |\n| Cold\u002Fwarm protocol, capability, or tool-list drift | Warning with `--repeat` | No |\n| stderr diagnostics | Allowed | Allowed |\n\n## Live MCP Coverage\n\nThe test suite creates real servers with `@modelcontextprotocol\u002Fsdk@1.29.0` and verifies:\n\n| Scenario | Expected result |\n| --- | --- |\n| clean SDK stdio server through `initialize` and `tools\u002Flist` | Pass |\n| SDK server with startup stdout pollution | Fail |\n| SDK server with stderr diagnostics | Pass |\n| SDK server with late stdout pollution after connection | Fail |\n| hand-rolled server that ignores post-initialize requests | Fail |\n| server that crashes after initialized notification | Fail |\n\n## Commands\n\n```bash\nmcp-stdio-guard [options] -- \u003Ccommand> [args...]\n```\n\n| Option | Description |\n| --- | --- |\n| `--config \u003Cpath>` | read a JSON config file for registry runs and explicitly safe tool calls |\n| `--profile \u003Cname>` | apply a deterministic guard profile: `custom`, `smoke`, `registry`, `ci`, or `strict` |\n| `--protocol \u003Cversion>` | MCP protocol version to send, default `2025-11-25` |\n| `--timeout \u003Cms>` | initialize and request timeout, default `5000` |\n| `--max-stdout-bytes \u003Cn>` | total stdout byte limit, default `1048576` |\n| `--max-stdout-line-bytes \u003Cn>` | single stdout line byte limit, default `262144` |\n| `--max-stderr-bytes \u003Cn>` | retained stderr byte limit, default `1048576` |\n| `--repeat \u003Ccount>` | run the same guard multiple times to catch cold\u002Fwarm startup behavior |\n| `--request \u003Cmethod>` | send one MCP request after initialization, for example `tools\u002Flist` |\n| `--params \u003Cjson>` | JSON params for `--request` |\n| `--adversarial-probe \u003Cname>` \u002F `--adversarial-probes \u003Clist>` | opt into strict protocol probes: `invalid-method`, `invalid-params`, `notification`, `malformed-json`, `all`, or `none` |\n| `--scan \u003Cpath>` | scan source for risky stdout writes and visible startup-output risks |\n| `--fail-on-static` | make static scan findings fail the command |\n| `--json` | print machine-readable output |\n| `--cwd \u003Cpath>` | run the server command from a specific directory |\n| `--help` | show help |\n\n## Profiles\n\nProfiles are deterministic presets for common workflows. Existing CLI behavior remains the default `custom` profile, so current commands keep working unless `--profile` is provided.\n\n| Profile | Behavior |\n| --- | --- |\n| `custom` | preserve explicit CLI flags and legacy defaults |\n| `smoke` | initialize only unless `--request` is provided; skip advertised `tools\u002Flist`, `resources\u002Flist`, and `prompts\u002Flist` probes |\n| `registry` | run advertised list probes and repeat twice by default for cold\u002Fwarm consistency |\n| `ci` | emit JSON output and make static scan findings fail when `--scan` is used |\n| `strict` | combine CI-style output\u002Fstatic failures, registry-style repeat depth, and built-in adversarial protocol probes |\n\nExplicit 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\u002Fstatic behavior without adversarial inputs.\n\n## Config Files\n\nConfig 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.\n\nSupported fields:\n\n| Field | Meaning |\n| --- | --- |\n| `command` | command as either `[\"node\", \".\u002Fserver.js\"]` or `\"node\"` with `args` |\n| `args` | arguments used only when `command` is a string |\n| `cwd` | working directory, resolved relative to the config file |\n| `env` | environment variables to pass; values are redacted in JSON output |\n| `profile`, `protocol`, `timeoutMs`, `repeat`, `json` | same meaning as CLI options |\n| `maxStdoutBytes`, `maxStdoutLineBytes`, `maxStderrBytes` | byte limits for untrusted child output |\n| `scan` or `scanPath` | source scan path, resolved relative to the config file |\n| `failOnStatic` | make static scan findings fail |\n| `request` | one explicit post-initialize request: `{ \"method\": \"tools\u002Flist\" }` |\n| `requests` | list of explicit post-initialize requests |\n| `safeToolCalls` | opt-in `tools\u002Fcall` recipes; no tool is called unless listed here or explicitly requested |\n| `adversarialProbes` | opt-in built-in probes as `true`, `\"all\"`, `\"none\"`, or a list of probe names |\n| `adversarialToolCalls` | opt-in invalid-argument `tools\u002Fcall` probes for configured safe tools |\n\nExample:\n\n```json\n{\n  \"profile\": \"registry\",\n  \"command\": [\"node\", \".\u002Fserver.js\"],\n  \"cwd\": \".\",\n  \"json\": true,\n  \"env\": {\n    \"API_TOKEN\": \"set-in-runner\"\n  },\n  \"requests\": [\n    { \"method\": \"tools\u002Flist\" }\n  ],\n  \"adversarialProbes\": [\"invalid-method\", \"notification\"],\n  \"safeToolCalls\": [\n    { \"name\": \"echo\", \"arguments\": { \"text\": \"hello\" } }\n  ],\n  \"adversarialToolCalls\": [\n    { \"name\": \"echo\", \"arguments\": { \"unexpected\": true } }\n  ]\n}\n```\n\nThe guard does not discover and call arbitrary tools from `tools\u002Flist`. Tool execution only happens through an explicit `safeToolCalls` entry or an explicit `tools\u002Fcall` request you provide.\n\nAdversarial 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\u002Fidempotent.\n\n## JSON Contract\n\n`--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:\n\n| Field | Meaning |\n| --- | --- |\n| `schemaVersion` | JSON contract version, currently `1` |\n| `ok` | `true` when no error-severity issue was found |\n| `config` | config file metadata and checks used, or `{ \"enabled\": false, ... }` |\n| `profile` | selected guard profile, for example `custom`, `smoke`, `registry`, `ci`, or `strict` |\n| `command` | command and arguments that were validated |\n| `protocol` | MCP protocol version sent by the guard |\n| `negotiatedProtocol` | protocol version returned by the server, when available |\n| `initialized` | whether the server completed the initialize handshake |\n| `operation` | post-initialize request result, or `null` when `--request` was not used |\n| `operations` | all explicit post-initialize requests, including config requests and safe tool calls |\n| `adversarial` | opt-in adversarial probe results, including status, risk text, and per-probe issue codes |\n| `toolSchema` | summary of `tools\u002Flist` metadata validation when that operation was requested or probed from an advertised tools capability |\n| `capabilityProbes` | whether advertised capability list probes were enabled for this run |\n| `capabilityKeys` | sorted capability keys returned by `initialize` for a single run; repeat mode exposes this inside each `runs` entry |\n| `capabilityChecks` | advertised capability probes observed during a single run; repeat mode exposes this inside each `runs` entry |\n| `drift` | repeat-run comparison summary for negotiated protocol, advertised capabilities, tool names\u002Fcounts, and resource\u002Fprompt list counts |\n| `process` | startup, timeout, exit code, signal, and guard-termination metadata for a single run; repeat mode exposes this inside each `runs` entry |\n| `checks` | badge-friendly per-class statuses |\n| `issueClasses` | registry-friendly summary grouped by `installRuntime`, `stdioTransport`, and `mcpProtocol` |\n| `summary` | badge-friendly aggregate status, primary issue, issue counts, and display guidance |\n| `fingerprint` | redacted reproducibility metadata for debugging registry and CI runs |\n| `issues` | machine-readable diagnostics with `class`, `severity`, `code`, and `message`; repeat mode also adds `run` |\n| `staticScan` | whether source scanning was enabled and whether findings fail the command |\n| `staticFindings` | source scan findings with language, file, line, reason, and message |\n| `runs` | per-run results when `--repeat` is used |\n\n`process.output` records observed stdout\u002Fstderr byte counts, configured output limits, whether retained stderr was truncated, and the issue code that stopped the run when a limit was exceeded.\n\n`summary` is the preferred badge and listing entry point. It is additive to `ok`, `checks`, and `issueClasses`; consumers that already read those fields can keep doing so.\n\n| Summary field | Shape |\n| --- | --- |\n| `summary.status` | `pass`, `warning`, `needs_inspection`, or `fail` |\n| `summary.issueClass` | `\"\"`, `installRuntime`, `stdioTransport`, `mcpProtocol`, or `mixed` |\n| `summary.primaryIssueCode` | first display-worthy issue code, or `\"\"` when none |\n| `summary.primaryIssueClass` | class for the primary issue, or `\"\"` when none |\n| `summary.issueCounts` | `{ \"error\": number, \"warning\": number, \"total\": number }` |\n| `summary.badge` | `{ \"label\": \"mcp stdio\", \"message\": string, \"color\": string }` |\n| `summary.display` | short human display guidance for registry listings |\n\nRegistry summary display guidance:\n\n| Status | Meaning | Suggested display |\n| --- | --- | --- |\n| `pass` | no issues were found | verified |\n| `warning` | warning-only result, such as static stdout risk or metadata quality advisory | verified with warnings |\n| `needs_inspection` | error-severity issue is limited to install\u002Fruntime health | runtime or install issue; inspect before presenting as protocol failure |\n| `fail` | stdio transport or MCP protocol error was observed | stdio transport or MCP protocol failure |\n\nCheck statuses are `pass`, `fail`, `warning`, or `skipped`. The `checks` object separates the signal into `initialize`, `stdout`, `jsonRpc`, `operation`, `capabilities`, `toolSchema`, `adversarial`, `process`, `pythonBuffering`, `staticScan`, and `repeat`, each with stable `status` and `issueCodes` fields. When `--repeat` is used, `checks.repeat` also includes `runs`, `passedRuns`, and `failedRuns`; each entry in `runs` is a normal schema-versioned result for that individual guard run.\n\n`issueClasses` is additive to `checks`. It groups issue codes by the kind of problem a registry or client should display:\n\n| Issue class | Meaning | Display guidance |\n| --- | --- | --- |\n| `installRuntime` | the command could not start, timed out, exited, crashed, exceeded stderr limits, or hit a runtime advisory | show as \"needs inspection\" or \"runtime\u002Finstall issue\"; do not present it as an MCP protocol violation |\n| `stdioTransport` | stdout was not a clean newline-delimited JSON-RPC channel, exceeded output limits, or source scan found risky stdout writes | show as stdio hygiene failure; ask maintainers to keep diagnostics on stderr |\n| `mcpProtocol` | the server emitted invalid JSON-RPC\u002FMCP responses, mismatched request ids, or returned initialize\u002Foperation errors | show as MCP\u002FJSON-RPC conformance issue |\n\nCurrent issue-code mapping:\n\n| Issue class | Issue codes |\n| --- | --- |\n| `installRuntime` | `initialize-timeout`, `operation-missing-response`, `operation-timeout`, `python-buffered-stdio`, `server-crashed`, `server-exited`, `spawn-failed`, `stderr-output-limit-exceeded` |\n| `stdioTransport` | `static-stdout-write`, `stdout-content-length-framing`, `stdout-empty-line`, `stdout-line-too-large`, `stdout-non-json`, `stdout-output-limit-exceeded`, `stdout-without-newline` |\n| `mcpProtocol` | `adversarial-invalid-method-result`, `adversarial-invalid-params-result`, `adversarial-malformed-json-result`, `adversarial-notification-response`, `adversarial-probe-crash`, `adversarial-probe-invalid-stdout`, `adversarial-probe-timeout`, `adversarial-tool-call-result`, `capability-list-error`, `capability-list-missing-response`, `capability-list-timeout`, `capability-list-unsupported`, `initialize-error`, `initialize-invalid-capabilities`, `initialize-invalid-protocol-version`, `initialize-invalid-result`, `initialize-invalid-server-info`, `initialize-missing-capabilities`, `initialize-missing-protocol-version`, `initialize-missing-server-info`, `notification-response`, `operation-error`, `repeat-capability-drift`, `repeat-list-shape-drift`, `repeat-protocol-drift`, `repeat-tool-drift`, `response-id-mismatch`, `response-id-type-mismatch`, `stdout-invalid-json-rpc`, `stdout-unexpected-request-id`, `tool-description-missing`, `tool-input-schema-invalid`, `tool-input-schema-required-missing`, `tool-name-duplicate`, `tool-name-invalid`, `tools-list-invalid-result` |\n\nInitialize lifecycle checks are part of the MCP protocol class. Missing or invalid `protocolVersion` and `capabilities` fail the run before the guard sends `notifications\u002Finitialized` or any normal request. Missing or invalid `serverInfo` is warning-level so registries can surface incomplete metadata without confusing it with a broken transport.\n\nJSON-RPC invariant checks distinguish wrong response ids from id type round-trip problems and fail servers that respond to `notifications\u002Finitialized`. JSON-RPC error frames must be structured with numeric `code` and string `message` fields.\n\nTool schema checks run when `tools\u002Flist` receives a successful result, either from `--request tools\u002Flist` or from the advertised tools capability probe. Duplicate or invalid tool names, missing `inputSchema`, invalid schema shapes, and `required` entries that are absent from `properties` are MCP protocol failures. Missing tool descriptions are warning-level so registries can show quality guidance without marking the server broken.\n\nCapability honesty checks are additive. If `initialize` advertises `capabilities.tools`, `capabilities.resources`, or `capabilities.prompts`, the guard probes the matching `tools\u002Flist`, `resources\u002Flist`, or `prompts\u002Flist` method after `notifications\u002Finitialized`. Unadvertised capabilities are `skipped`, not failed. `capability-list-unsupported` means an advertised list method returned method-not-found; `capability-list-error`, `capability-list-timeout`, and `capability-list-missing-response` mean the advertised list method existed in the contract but failed at runtime.\n\nAdversarial probes are additive and opt-in. Their failures are classified as `mcpProtocol`, not install\u002Fruntime failures, so registries can distinguish \"the package cannot start\" from \"the server started but mishandled strict JSON-RPC\u002FMCP inputs.\" `malformed-json` accepts either a structured parse error or silence after a short observation window; a crash is a protocol failure. `notification` expects no response.\n\nRepeat drift checks compare successful initialized runs against the first initialized run. Negotiated protocol changes, advertised capability key changes, added or removed tool names, tool count changes, and resource\u002Fprompt list count changes are warning-level `repeat-*` issues. Tool order is normalized before comparison, so order-only changes do not warn.\n\nThe repeat `drift` object has stable `status`, `issueCodes`, `baselineRun`, and `comparedRuns` fields. Its nested `negotiatedProtocol`, `capabilities`, `tools`, `lists.resources`, and `lists.prompts` sections include `changedRuns` so registries can show exactly what changed between cold and warm starts.\n\nRuntime issue codes remain backward-compatible. For finer registry display, runtime issues may also include a stable `detailCode`:\n\n| Existing issue code | Detail codes |\n| --- | --- |\n| `spawn-failed` | `spawn-failed-before-startup` |\n| `server-exited` | `clean-exit-before-initialize`, `nonzero-exit-before-initialize`, `signal-exit-before-initialize` |\n| `initialize-timeout` | `startup-timeout` |\n| `operation-timeout` | `request-timeout` |\n| `operation-missing-response` | `clean-exit-during-operation`, `nonzero-exit-during-operation`, `signal-exit-during-operation` |\n| `server-crashed` | `nonzero-exit-after-initialize`, `signal-exit-after-initialize` |\n\nSchema version policy:\n\n| Change type | Versioning |\n| --- | --- |\n| Add a field, check, issue code, detail code, or optional metadata | keep `schemaVersion: 1` |\n| Rename, remove, or change the type or meaning of a stable field | bump `schemaVersion` |\n| Change the `summary.status` enum or the meaning of an existing status | bump `schemaVersion` |\n| Add a new issue code under an existing issue class | keep `schemaVersion: 1`; consumers should fall back to `class`, `severity`, and `summary.status` |\n\nMigration notes from the first `schemaVersion: 1` contract:\n\n| Existing consumer behavior | Recommended migration |\n| --- | --- |\n| Read `ok` for pass\u002Ffail | keep reading `ok`; use `summary.status` when a registry needs to distinguish runtime inspection from protocol failure |\n| Read `checks.*.status` and `checks.*.issueCodes` | keep reading `checks`; use `summary.badge` for badges and compact listing UI |\n| Read `issueClasses` for display buckets | keep reading `issueClasses`; use `summary.issueClass` and `summary.primaryIssueCode` for the first-line message |\n| Treat every `ok: false` as a broken MCP server | prefer `summary.status`; `needs_inspection` means install\u002Fruntime health failed, not stdio or MCP protocol conformance |\n\n`process` records the observed lifecycle even when the run passes. `outcome` is one of `starting`, `running`, `exited`, `timeout`, `spawn-failed`, or `guard-terminated`; `starting` is the transient initial value while the child is being created, not an expected terminal outcome. `phase` is `startup`, `initialize`, `operation`, `adversarial`, or `post-initialize`. `exitCode` and `signal` are included when the process exits before the guard finishes; timeout runs include `timedOut`, `timeoutCode`, `timeoutMs`, and guard kill metadata. `spawnError` is either `null` or an object with `code` and `message`; the matching `spawn-failed` issue also exposes `spawnErrorCode`.\n\nSpawn failure shape:\n\n| Field | Shape |\n| --- | --- |\n| `process.spawnError` | `null` or `{ \"code\": \"ENOENT\", \"message\": \"spawn missing-command ENOENT\" }` |\n| `issues[].spawnErrorCode` | short platform error code such as `ENOENT`, or `\"\"` when unavailable |\n\n`fingerprint` helps explain why a result reproduced in one runner but not another. It includes the guard version, redacted command argv, cwd details, protocol, timeout, repeat count, requested operation, platform\u002Farch, relevant runtime versions, package metadata when detectable, static-scan context, and startup\u002Ftotal duration. Environment variable values are always emitted as `\u003Credacted>` and only explicitly provided env names are listed.\n\nRegistry display flow:\n\n| Step | Use |\n| --- | --- |\n| 1 | Use `summary.status`, `summary.badge`, and `summary.display` for compact badges and listing rows |\n| 2 | Use `fingerprint.command`, `fingerprint.cwd`, and `fingerprint.package` to show what was actually run |\n| 3 | Show `issueClasses` details so install\u002Fruntime, stdio transport, and MCP protocol failures stay distinct |\n| 4 | Show `checks.capabilities` as advertised MCP surface honesty |\n| 5 | Show `checks.toolSchema` as tool metadata quality, separate from startup and stdio transport health |\n| 6 | Show `drift` warnings as stability advisories, not hard failures, unless another check failed |\n| 7 | Compare `fingerprint.system`, `fingerprint.runtimes`, and `fingerprint.timings` before marking a package broken |\n| 8 | Show `fingerprint.env.names` only when debugging; never ask users to paste secret values |\n\nExample:\n\n```json\n{\n  \"schemaVersion\": 1,\n  \"ok\": true,\n  \"config\": {\n    \"enabled\": false,\n    \"path\": \"\",\n    \"resolvedPath\": \"\",\n    \"checks\": {\n      \"command\": false,\n      \"cwd\": false,\n      \"envNames\": [],\n      \"requests\": [],\n      \"safeToolCalls\": [],\n      \"adversarialProbes\": [],\n      \"adversarialToolCalls\": []\n    }\n  },\n  \"profile\": \"custom\",\n  \"fingerprint\": {\n    \"guard\": { \"name\": \"mcp-stdio-guard\", \"version\": \"1.0.0\" },\n    \"command\": {\n      \"executable\": \"node\",\n      \"args\": [\".\u002Fserver.js\"],\n      \"argv\": [\"node\", \".\u002Fserver.js\"]\n    },\n    \"cwd\": {\n      \"requested\": \"\u002Frepo\u002Fserver\",\n      \"resolved\": \"\u002Frepo\u002Fserver\",\n      \"exists\": true\n    },\n    \"protocol\": \"2025-11-25\",\n    \"config\": {\n      \"enabled\": false,\n      \"path\": \"\",\n      \"resolvedPath\": \"\",\n      \"checks\": {\n        \"command\": false,\n        \"cwd\": false,\n        \"envNames\": [],\n        \"requests\": [],\n        \"safeToolCalls\": [],\n        \"adversarialProbes\": [],\n        \"adversarialToolCalls\": []\n      }\n    },\n    \"profile\": \"custom\",\n    \"timeoutMs\": 5000,\n    \"repeat\": 1,\n    \"capabilityProbes\": true,\n    \"adversarialProbes\": [],\n    \"operation\": { \"method\": \"tools\u002Flist\", \"hasParams\": false, \"source\": \"cli-request\", \"safeToolCallName\": \"\" },\n    \"operations\": [{ \"method\": \"tools\u002Flist\", \"hasParams\": false, \"source\": \"cli-request\", \"safeToolCallName\": \"\" }],\n    \"system\": { \"platform\": \"darwin\", \"arch\": \"arm64\", \"osRelease\": \"25.0.0\" },\n    \"runtimes\": {\n      \"node\": { \"version\": \"v24.0.0\", \"role\": \"guard-and-target\" }\n    },\n    \"package\": null,\n    \"env\": {\n      \"inherited\": true,\n      \"names\": [\"API_TOKEN\"],\n      \"values\": { \"API_TOKEN\": \"\u003Credacted>\" }\n    },\n    \"staticScan\": { \"enabled\": false, \"path\": \"\", \"failOnFindings\": false },\n    \"timings\": { \"startupMs\": 42, \"totalMs\": 96 }\n  },\n  \"process\": {\n    \"started\": true,\n    \"pid\": 12345,\n    \"outcome\": \"guard-terminated\",\n    \"phase\": \"post-initialize\",\n    \"exitCode\": null,\n    \"signal\": null,\n    \"timedOut\": false,\n    \"timeoutCode\": \"\",\n    \"timeoutMs\": 5000,\n    \"killedByGuard\": true,\n    \"killSignal\": \"SIGTERM\",\n    \"killReason\": \"guard-finished\",\n    \"spawnError\": null\n  },\n  \"capabilityProbes\": true,\n  \"adversarial\": { \"enabled\": false, \"probes\": [] },\n  \"capabilityKeys\": [\"tools\"],\n  \"capabilityChecks\": {\n    \"tools\": { \"advertised\": true, \"method\": \"tools\u002Flist\", \"responded\": true, \"itemCount\": 2, \"error\": null },\n    \"resources\": { \"advertised\": false, \"method\": \"resources\u002Flist\", \"responded\": false, \"itemCount\": null, \"error\": null },\n    \"prompts\": { \"advertised\": false, \"method\": \"prompts\u002Flist\", \"responded\": false, \"itemCount\": null, \"error\": null }\n  },\n  \"issueClasses\": {\n    \"installRuntime\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"stdioTransport\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"mcpProtocol\": { \"status\": \"pass\", \"issueCodes\": [] }\n  },\n  \"summary\": {\n    \"status\": \"pass\",\n    \"issueClass\": \"\",\n    \"primaryIssueCode\": \"\",\n    \"primaryIssueClass\": \"\",\n    \"issueCounts\": { \"error\": 0, \"warning\": 0, \"total\": 0 },\n    \"badge\": { \"label\": \"mcp stdio\", \"message\": \"pass\", \"color\": \"brightgreen\" },\n    \"display\": \"verified\"\n  },\n  \"toolSchema\": {\n    \"checked\": true,\n    \"toolCount\": 2,\n    \"toolNames\": [\"read_file\", \"search\"],\n    \"validToolCount\": 2,\n    \"warningCount\": 0,\n    \"errorCount\": 0,\n    \"duplicateNames\": []\n  },\n  \"checks\": {\n    \"initialize\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"stdout\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"jsonRpc\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"operation\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"capabilities\": {\n      \"status\": \"pass\",\n      \"issueCodes\": [],\n      \"tools\": { \"status\": \"pass\", \"issueCodes\": [], \"advertised\": true, \"method\": \"tools\u002Flist\", \"responded\": true, \"itemCount\": 2 },\n      \"resources\": { \"status\": \"skipped\", \"issueCodes\": [], \"advertised\": false, \"method\": \"resources\u002Flist\", \"responded\": false, \"itemCount\": null },\n      \"prompts\": { \"status\": \"skipped\", \"issueCodes\": [], \"advertised\": false, \"method\": \"prompts\u002Flist\", \"responded\": false, \"itemCount\": null }\n    },\n    \"toolSchema\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"adversarial\": { \"status\": \"skipped\", \"issueCodes\": [] },\n    \"process\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"pythonBuffering\": { \"status\": \"pass\", \"issueCodes\": [] },\n    \"staticScan\": { \"status\": \"skipped\", \"issueCodes\": [] },\n    \"repeat\": { \"status\": \"skipped\", \"issueCodes\": [] }\n  }\n}\n```\n\nThe guard is registry-agnostic. It does not care whether an install command came from Smithery, Glama, GitHub, or a private catalog; it validates the command, working directory, optional source path, and observed stdio behavior.\n\n## CI\n\n```yaml\n- run: npm ci\n- run: npx mcp-stdio-guard --profile ci --scan src --request tools\u002Flist -- node .\u002Fserver.js\n```\n\n## Output\n\nPassing server:\n\n```text\nPASS MCP stdio guard\ninitialize: ok\nframes: 2 stdout \u002F 0 invalid\nstderr: 0 lines\nprotocol: 2025-11-25\nrequest: tools\u002Flist responded\ntool schemas: 2\u002F2 valid\n```\n\nPolluted stdout:\n\n```text\nFAIL MCP stdio guard\ninitialize: ok\nframes: 2 stdout \u002F 1 invalid\nstderr: 0 lines\nprotocol: 2025-11-25\nrequest: tools\u002Flist responded\n[error] stdout-non-json: stdout line 1 is not JSON-RPC: \"server starting...\"\n```\n\n## Design\n\n- Runtime dependencies: zero.\n- Default behavior: validate the real process boundary.\n- Optional static scan: intentionally simple and conservative; catches common JavaScript and Python stdout writes, stdout logging handlers, and visible startup-output risks.\n- CI posture: fail on protocol corruption, crashes, and missing responses.\n- Promotion promise: no fake stars, no spam, just a tool that catches a real MCP failure mode.\n\n## License\n\nMIT\n","mcp-stdio-guard 是一个用于检测MCP stdio服务器中stdout污染和握手失败问题的工具。它通过执行真实的MCP初始化握手、检查工具、资源和提示列表功能，以及发送并验证每个stdout帧来确保协议通道的纯净性。该工具采用Node.js编写，支持JSON-RPC消息格式，并遵循Model Context Protocol (MCP)规范。适用于开发和测试阶段，特别是在CI\u002FCD流水线中作为质量控制的一部分，帮助开发者提前发现可能导致客户端连接失败的问题，从而提高系统的稳定性和可靠性。",2,"2026-06-11 04:03:21","CREATED_QUERY"]