mcp-stdio-guard 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +154 -0
  2. package/package.json +1 -1
  3. package/src/index.js +1053 -22
package/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  <p align="center">
12
12
  <a href="https://github.com/1Utkarsh1/mcp-stdio-guard/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/1Utkarsh1/mcp-stdio-guard/actions/workflows/ci.yml/badge.svg" /></a>
13
13
  <a href="https://www.npmjs.com/package/mcp-stdio-guard"><img alt="npm" src="https://img.shields.io/npm/v/mcp-stdio-guard?color=0b6bcb" /></a>
14
+ <a href="https://badge.socket.dev/npm/package/mcp-stdio-guard/0.3.0"><img alt="Socket" src="https://badge.socket.dev/npm/package/mcp-stdio-guard/0.3.0" /></a>
14
15
  <img alt="runtime dependencies" src="https://img.shields.io/badge/runtime%20deps-0-1f8f4c" />
15
16
  <img alt="node" src="https://img.shields.io/badge/node-%3E%3D18-2f855a" />
16
17
  <a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-111827" /></a>
@@ -77,6 +78,12 @@ JSON output for CI:
77
78
  mcp-stdio-guard --json --request tools/list -- node ./server.js
78
79
  ```
79
80
 
81
+ Repeat the same guard to catch cold/warm startup behavior:
82
+
83
+ ```bash
84
+ mcp-stdio-guard --repeat 2 --request tools/list -- node ./server.js
85
+ ```
86
+
80
87
  ## What It Catches
81
88
 
82
89
  <p align="center">
@@ -86,6 +93,7 @@ mcp-stdio-guard --json --request tools/list -- node ./server.js
86
93
  | Problem | Runtime check | Static scan |
87
94
  | --- | --- | --- |
88
95
  | `console.log("starting")` before server startup | Yes | Yes |
96
+ | Dependency/import-time stdout pollution | Yes with `--repeat` | No |
89
97
  | Python `print("debug")` in a stdio server | Yes | Yes |
90
98
  | Late stdout logs after `initialize` | Yes | Partial |
91
99
  | Invalid JSON-RPC frames | Yes | No |
@@ -116,6 +124,7 @@ mcp-stdio-guard [options] -- <command> [args...]
116
124
  | --- | --- |
117
125
  | `--protocol <version>` | MCP protocol version to send, default `2025-11-25` |
118
126
  | `--timeout <ms>` | initialize and request timeout, default `5000` |
127
+ | `--repeat <count>` | run the same guard multiple times to catch cold/warm startup behavior |
119
128
  | `--request <method>` | send one MCP request after initialization, for example `tools/list` |
120
129
  | `--params <json>` | JSON params for `--request` |
121
130
  | `--scan <path>` | scan source for risky stdout writes |
@@ -124,6 +133,151 @@ mcp-stdio-guard [options] -- <command> [args...]
124
133
  | `--cwd <path>` | run the server command from a specific directory |
125
134
  | `--help` | show help |
126
135
 
136
+ ## JSON Contract
137
+
138
+ `--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:
139
+
140
+ | Field | Meaning |
141
+ | --- | --- |
142
+ | `schemaVersion` | JSON contract version, currently `1` |
143
+ | `ok` | `true` when no error-severity issue was found |
144
+ | `command` | command and arguments that were validated |
145
+ | `protocol` | MCP protocol version sent by the guard |
146
+ | `negotiatedProtocol` | protocol version returned by the server, when available |
147
+ | `initialized` | whether the server completed the initialize handshake |
148
+ | `operation` | post-initialize request result, or `null` when `--request` was not used |
149
+ | `process` | startup, timeout, exit code, signal, and guard-termination metadata for a single run; repeat mode exposes this inside each `runs` entry |
150
+ | `checks` | badge-friendly per-class statuses |
151
+ | `issueClasses` | registry-friendly summary grouped by `installRuntime`, `stdioTransport`, and `mcpProtocol` |
152
+ | `fingerprint` | redacted reproducibility metadata for debugging registry and CI runs |
153
+ | `issues` | machine-readable diagnostics with `class`, `severity`, `code`, and `message`; repeat mode also adds `run` |
154
+ | `staticScan` | whether source scanning was enabled and whether findings fail the command |
155
+ | `staticFindings` | source scan findings with file, line, and message |
156
+ | `runs` | per-run results when `--repeat` is used |
157
+
158
+ Check statuses are `pass`, `fail`, `warning`, or `skipped`. The `checks` object separates the signal into `initialize`, `stdout`, `jsonRpc`, `operation`, `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.
159
+
160
+ `issueClasses` is additive to `checks`. It groups issue codes by the kind of problem a registry or client should display:
161
+
162
+ | Issue class | Meaning | Display guidance |
163
+ | --- | --- | --- |
164
+ | `installRuntime` | the command could not start, timed out, exited, crashed, or hit a runtime advisory | show as "needs inspection" or "runtime/install issue"; do not present it as an MCP protocol violation |
165
+ | `stdioTransport` | stdout was not a clean newline-delimited JSON-RPC channel, or source scan found risky stdout writes | show as stdio hygiene failure; ask maintainers to keep diagnostics on stderr |
166
+ | `mcpProtocol` | the server emitted invalid JSON-RPC/MCP responses, mismatched request ids, or returned initialize/operation errors | show as MCP/JSON-RPC conformance issue |
167
+
168
+ Current issue-code mapping:
169
+
170
+ | Issue class | Issue codes |
171
+ | --- | --- |
172
+ | `installRuntime` | `initialize-timeout`, `operation-missing-response`, `operation-timeout`, `python-buffered-stdio`, `server-crashed`, `server-exited`, `spawn-failed` |
173
+ | `stdioTransport` | `static-stdout-write`, `stdout-content-length-framing`, `stdout-empty-line`, `stdout-non-json`, `stdout-without-newline` |
174
+ | `mcpProtocol` | `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`, `response-id-mismatch`, `response-id-type-mismatch`, `stdout-invalid-json-rpc`, `stdout-unexpected-request-id` |
175
+
176
+ Initialize lifecycle checks are part of the MCP protocol class. Missing or invalid `protocolVersion` and `capabilities` fail the run before the guard sends `notifications/initialized` or any normal request. Missing or invalid `serverInfo` is warning-level so registries can surface incomplete metadata without confusing it with a broken transport.
177
+
178
+ JSON-RPC invariant checks distinguish wrong response ids from id type round-trip problems and fail servers that respond to `notifications/initialized`. JSON-RPC error frames must be structured with numeric `code` and string `message` fields.
179
+
180
+ Runtime issue codes remain backward-compatible. For finer registry display, runtime issues may also include a stable `detailCode`:
181
+
182
+ | Existing issue code | Detail codes |
183
+ | --- | --- |
184
+ | `spawn-failed` | `spawn-failed-before-startup` |
185
+ | `server-exited` | `clean-exit-before-initialize`, `nonzero-exit-before-initialize`, `signal-exit-before-initialize` |
186
+ | `initialize-timeout` | `startup-timeout` |
187
+ | `operation-timeout` | `request-timeout` |
188
+ | `operation-missing-response` | `clean-exit-during-operation`, `nonzero-exit-during-operation`, `signal-exit-during-operation` |
189
+ | `server-crashed` | `nonzero-exit-after-initialize`, `signal-exit-after-initialize` |
190
+
191
+ `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`, 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`.
192
+
193
+ Spawn failure shape:
194
+
195
+ | Field | Shape |
196
+ | --- | --- |
197
+ | `process.spawnError` | `null` or `{ "code": "ENOENT", "message": "spawn missing-command ENOENT" }` |
198
+ | `issues[].spawnErrorCode` | short platform error code such as `ENOENT`, or `""` when unavailable |
199
+
200
+ `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/arch, relevant runtime versions, package metadata when detectable, static-scan context, and startup/total duration. Environment variable values are always emitted as `<redacted>` and only explicitly provided env names are listed.
201
+
202
+ Registry display flow:
203
+
204
+ | Step | Use |
205
+ | --- | --- |
206
+ | 1 | Show `issueClasses` first so install/runtime, stdio transport, and MCP protocol failures stay distinct |
207
+ | 2 | Use `fingerprint.command`, `fingerprint.cwd`, and `fingerprint.package` to show what was actually run |
208
+ | 3 | Compare `fingerprint.system`, `fingerprint.runtimes`, and `fingerprint.timings` before marking a package broken |
209
+ | 4 | Show `fingerprint.env.names` only when debugging; never ask users to paste secret values |
210
+
211
+ Example:
212
+
213
+ ```json
214
+ {
215
+ "schemaVersion": 1,
216
+ "ok": true,
217
+ "fingerprint": {
218
+ "guard": { "name": "mcp-stdio-guard", "version": "0.3.0" },
219
+ "command": {
220
+ "executable": "node",
221
+ "args": ["./server.js"],
222
+ "argv": ["node", "./server.js"]
223
+ },
224
+ "cwd": {
225
+ "requested": "/repo/server",
226
+ "resolved": "/repo/server",
227
+ "exists": true
228
+ },
229
+ "protocol": "2025-11-25",
230
+ "timeoutMs": 5000,
231
+ "repeat": 1,
232
+ "operation": { "method": "tools/list", "hasParams": false },
233
+ "system": { "platform": "darwin", "arch": "arm64", "osRelease": "25.0.0" },
234
+ "runtimes": {
235
+ "node": { "version": "v24.0.0", "role": "guard-and-target" }
236
+ },
237
+ "package": null,
238
+ "env": {
239
+ "inherited": true,
240
+ "names": ["API_TOKEN"],
241
+ "values": { "API_TOKEN": "<redacted>" }
242
+ },
243
+ "staticScan": { "enabled": false, "path": "", "failOnFindings": false },
244
+ "timings": { "startupMs": 42, "totalMs": 96 }
245
+ },
246
+ "process": {
247
+ "started": true,
248
+ "pid": 12345,
249
+ "outcome": "guard-terminated",
250
+ "phase": "post-initialize",
251
+ "exitCode": null,
252
+ "signal": null,
253
+ "timedOut": false,
254
+ "timeoutCode": "",
255
+ "timeoutMs": 5000,
256
+ "killedByGuard": true,
257
+ "killSignal": "SIGTERM",
258
+ "killReason": "guard-finished",
259
+ "spawnError": null
260
+ },
261
+ "issueClasses": {
262
+ "installRuntime": { "status": "pass", "issueCodes": [] },
263
+ "stdioTransport": { "status": "pass", "issueCodes": [] },
264
+ "mcpProtocol": { "status": "pass", "issueCodes": [] }
265
+ },
266
+ "checks": {
267
+ "initialize": { "status": "pass", "issueCodes": [] },
268
+ "stdout": { "status": "pass", "issueCodes": [] },
269
+ "jsonRpc": { "status": "pass", "issueCodes": [] },
270
+ "operation": { "status": "pass", "issueCodes": [] },
271
+ "process": { "status": "pass", "issueCodes": [] },
272
+ "pythonBuffering": { "status": "pass", "issueCodes": [] },
273
+ "staticScan": { "status": "skipped", "issueCodes": [] },
274
+ "repeat": { "status": "skipped", "issueCodes": [] }
275
+ }
276
+ }
277
+ ```
278
+
279
+ The 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.
280
+
127
281
  ## CI
128
282
 
129
283
  ```yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-stdio-guard",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "A runtime zero-dependency CLI that catches stdout pollution and handshake failures in MCP stdio servers.",
5
5
  "type": "module",
6
6
  "bin": {