@eidonze/mcpdoctor 0.1.5 → 0.1.7

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.
package/README.md CHANGED
@@ -1,100 +1,129 @@
1
- # mcpdoctor
2
-
3
- Read-only checks for MCP tool schemas and x402 payment endpoints. Use it before an agent client or buyer depends on an endpoint.
4
-
5
- [![npm](https://img.shields.io/npm/v/@eidonze/mcpdoctor?logo=npm)](https://www.npmjs.com/package/@eidonze/mcpdoctor)
6
- [![CI](https://github.com/xka0085-byte/mcp-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/xka0085-byte/mcp-doctor/actions/workflows/ci.yml)
7
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
-
9
- ## Find the right guide
10
-
11
- - [Public documentation site](https://xka0085-byte.github.io/mcp-doctor/)
12
- - [MCP schema checker](docs/mcp-schema-checker.md)
13
- - [x402 endpoint inspector](docs/x402-endpoint-inspector.md)
14
- - [MCP security CI](docs/mcp-security-ci.md)
15
- - [Troubleshooting](docs/troubleshooting.md)
16
- - [OAuth scope and current limits](docs/mcp-oauth-diagnostics.md)
17
-
18
- ## Run it now
19
-
20
- Check an MCP server without calling any tools:
21
-
22
- ```bash
23
- npx @eidonze/mcpdoctor@0.1.5 schema https://your-mcp-server.example/mcp --json
24
- ```
25
-
26
- Check an x402 endpoint without paying:
27
-
28
- ```bash
29
- npx @eidonze/mcpdoctor@0.1.5 inspect https://your-api.example/paid --method=POST --json
30
- ```
31
-
32
- Requires Node.js 18+. No API key or wallet is needed.
33
-
34
- ## Which command?
35
-
36
- | Command | Checks | Does not do |
37
- |---|---|---|
38
- | `schema` | MCP `initialize`, `tools/list`, tool names, descriptions, and JSON schemas | Does not call tools or prove runtime behavior |
39
- | `inspect` | HTTP 402, x402 v1/v2 payment document, `accepts[]`, discovery manifests, latency and response digest | Does not sign, pay, settle, retry, or verify delivery |
40
-
41
- Exit codes are stable for automation: `0` PASS, `1` FAIL, `2` UNKNOWN/network error, `3` usage error.
42
-
43
- ## GitHub Actions
44
-
45
- Add a read-only check to pull requests:
46
-
47
- ```yaml
48
- name: MCP Trust Check
49
- on: [pull_request]
50
-
51
- jobs:
52
- mcp-trust:
53
- runs-on: ubuntu-latest
54
- steps:
55
- - uses: xka0085-byte/mcp-doctor@v1
56
- with:
57
- endpoint: https://your-mcp-server.example/mcp
58
- mode: schema
59
- format: markdown
60
- ```
61
-
62
- Use `mode: inspect` for an x402 preflight. The action fails on `FAIL` or `UNKNOWN` by default; set `fail-on: false` for an informational check.
63
-
64
- ## Starter template
65
-
66
- Start a new MCP project with this check already wired in:
67
-
68
- - [Secure MCP Server Starter](examples/secure-mcp-server-starter/)
69
-
70
- The check is read-only and should target a public test endpoint. Do not place private keys or credentials in workflow inputs.
71
-
72
- ## What the report means
73
-
74
- `PASS` means the observed response matched the checks in this version. It is not a security audit, protocol certification, payment-success guarantee, or proof that an endpoint is safe. `UNKNOWN` means the endpoint could not be observed within the timeout or response-size limits.
75
-
76
- Example schema failure:
77
-
78
- ```text
79
- FAIL REQUIRED_PROPERTY_UNDEFINED tools[0] requires undeclared property query
80
- ```
81
-
82
- Example x402 failure:
83
-
84
- ```text
85
- FAIL NO_402 Expected HTTP 402, received 400
86
- ```
87
-
88
- ## Why it exists
89
-
90
- The checks come from failure modes observed while building ReceiptRail: body validation that prevents an x402 challenge, payment documents in multiple headers/body shapes, and vendor hint headers that can hide the real `accepts[]` document.
91
-
92
- mcpdoctor is not an MCP server. It is an independent, read-only endpoint inspector. It is not affiliated with mcpdoctor.dev; verify the npm scope is `@eidonze/mcpdoctor`.
93
-
94
- ## Feedback
95
-
96
- Found a false positive or a protocol shape we should support? [Open an issue](https://github.com/xka0085-byte/mcp-doctor/issues). Include the command, redacted output, Node version, and whether the endpoint is MCP or x402. Never include tokens, payment signatures, or private URLs.
97
-
98
- ## License
99
-
100
- MIT
1
+ # mcpdoctor
2
+
3
+ Read-only checks for MCP tool schemas and x402 payment endpoints. Use it before an agent client or buyer depends on an endpoint.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@eidonze/mcpdoctor?logo=npm)](https://www.npmjs.com/package/@eidonze/mcpdoctor)
6
+ [![CI](https://github.com/xka0085-byte/mcp-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/xka0085-byte/mcp-doctor/actions/workflows/ci.yml)
7
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ ## Find the right guide
10
+
11
+ - [Public documentation site](https://xka0085-byte.github.io/mcp-doctor/)
12
+ - [MCP schema checker](docs/mcp-schema-checker.md)
13
+ - [x402 endpoint inspector](docs/x402-endpoint-inspector.md)
14
+ - [MCP security CI](docs/mcp-security-ci.md)
15
+ - [Troubleshooting](docs/troubleshooting.md)
16
+ - [OAuth scope and current limits](docs/mcp-oauth-diagnostics.md)
17
+
18
+ ## Run it now
19
+
20
+ Check an MCP server without calling any tools:
21
+
22
+ ```bash
23
+ npx @eidonze/mcpdoctor@0.1.6 schema https://your-mcp-server.example/mcp --json
24
+ ```
25
+
26
+ Check an x402 endpoint without paying:
27
+
28
+ ```bash
29
+ npx @eidonze/mcpdoctor@0.1.6 inspect https://your-api.example/paid --method=POST --json
30
+ ```
31
+
32
+ Requires Node.js 18+. No API key or wallet is needed.
33
+
34
+ Example schema run against a live endpoint:
35
+
36
+ ```text
37
+ $ npx @eidonze/mcpdoctor schema https://your-mcp-server.example/mcp
38
+
39
+ MCP Tool Schema Report
40
+ - Status: FAIL
41
+ - Endpoint: https://your-mcp-server.example/mcp
42
+ - Protocol: 2025-06-18
43
+ - Tools: 12
44
+
45
+ - FAIL REQUIRED_PROPERTY_UNDEFINED: tools[3] requires undeclared property query
46
+ - WARN PROPERTY_DESCRIPTION_MISSING: tools[7].limit has no description
47
+ ```
48
+
49
+ The exact same run against a healthy server prints `Status: PASS` and exits 0, so CI can gate on it.
50
+
51
+ ## Which command?
52
+
53
+ | Command | Checks | Does not do |
54
+ |---|---|---|
55
+ | `schema` | MCP `initialize`, `tools/list`, tool names, descriptions, and JSON schemas | Does not call tools or prove runtime behavior |
56
+ | `inspect` | HTTP 402, x402 v1/v2 payment document, `accepts[]`, discovery manifests, latency and response digest | Does not sign, pay, settle, retry, or verify delivery |
57
+
58
+ Exit codes are stable for automation: `0` PASS, `1` FAIL, `2` UNKNOWN/network error, `3` usage error.
59
+
60
+ ## What it does NOT check
61
+
62
+ mcpdoctor is a protocol- and schema-level preflight, not a security audit. It does not:
63
+
64
+ - Detect malicious code, prompt injection, or supply-chain risk in the server's implementation
65
+ - Call tools, so it cannot prove runtime behavior, data handling, or permission scope
66
+ - Bypass, test, or evaluate authentication and authorization
67
+ - Verify payment settlement or delivery for x402 endpoints
68
+ - Certify compliance with any spec beyond the response shapes it inspects
69
+
70
+ A `PASS` means the observed handshake and schemas were well-formed at scan time. Nothing more.
71
+
72
+ ## GitHub Actions
73
+
74
+ Add a read-only check to pull requests:
75
+
76
+ ```yaml
77
+ name: MCP Trust Check
78
+ on: [pull_request]
79
+
80
+ jobs:
81
+ mcp-trust:
82
+ runs-on: ubuntu-latest
83
+ steps:
84
+ - uses: xka0085-byte/mcp-doctor@v1
85
+ with:
86
+ endpoint: https://your-mcp-server.example/mcp
87
+ mode: schema
88
+ format: markdown
89
+ ```
90
+
91
+ Use `mode: inspect` for an x402 preflight. The action fails on `FAIL` or `UNKNOWN` by default; set `fail-on: false` for an informational check.
92
+
93
+ ## Starter template
94
+
95
+ Start a new MCP project with this check already wired in:
96
+
97
+ - [Secure MCP Server Starter](examples/secure-mcp-server-starter/)
98
+
99
+ The check is read-only and should target a public test endpoint. Do not place private keys or credentials in workflow inputs.
100
+
101
+ ## What the report means
102
+
103
+ `PASS` means the observed response matched the checks in this version. It is not a security audit, protocol certification, payment-success guarantee, or proof that an endpoint is safe. `UNKNOWN` means the endpoint could not be observed within the timeout or response-size limits.
104
+
105
+ Example schema failure:
106
+
107
+ ```text
108
+ FAIL REQUIRED_PROPERTY_UNDEFINED tools[0] requires undeclared property query
109
+ ```
110
+
111
+ Example x402 failure:
112
+
113
+ ```text
114
+ FAIL NO_402 Expected HTTP 402, received 400
115
+ ```
116
+
117
+ ## Why it exists
118
+
119
+ The checks come from failure modes observed while building ReceiptRail: body validation that prevents an x402 challenge, payment documents in multiple headers/body shapes, and vendor hint headers that can hide the real `accepts[]` document.
120
+
121
+ mcpdoctor is not an MCP server. It is an independent, read-only endpoint inspector. It is not affiliated with mcpdoctor.dev; verify the npm scope is `@eidonze/mcpdoctor`.
122
+
123
+ ## Feedback
124
+
125
+ Found a false positive or a protocol shape we should support? [Open an issue](https://github.com/xka0085-byte/mcp-doctor/issues). Include the command, redacted output, Node version, and whether the endpoint is MCP or x402. Never include tokens, payment signatures, or private URLs.
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,105 @@
1
+ // mcpdoctor-mcp: run mcpdoctor as an MCP server (stdio, zero dependencies).
2
+ // Exposes two tools so AI clients can check endpoints without leaving chat:
3
+ // schema_check(url) - read-only MCP handshake + tools/list validation
4
+ // x402_inspect(url) - read-only 402 challenge validation, never pays
5
+ // Protocol: newline-delimited JSON-RPC 2.0 per the MCP stdio transport spec.
6
+ import { spawn } from 'node:child_process';
7
+ import path from 'node:path';
8
+ import url from 'node:url';
9
+ import readline from 'node:readline';
10
+
11
+ const cliPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), 'mcpdoctor.mjs');
12
+
13
+ const TOOLS = [
14
+ {
15
+ name: 'schema_check',
16
+ description: 'Run a read-only MCP preflight check against a server URL: initialize handshake, tools/list, and tool JSON schema validation. Never calls tools or reads data. Returns PASS/FAIL/UNKNOWN with detailed findings.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ url: { type: 'string', description: 'MCP server endpoint URL, e.g. https://example.com/mcp' },
21
+ },
22
+ required: ['url'],
23
+ },
24
+ },
25
+ {
26
+ name: 'x402_inspect',
27
+ description: 'Inspect an x402 payment endpoint without paying: reads the HTTP 402 challenge, validates the payment document shape, accepts[] entries and discovery manifests. Never signs or settles.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ url: { type: 'string', description: 'x402 resource URL, e.g. https://api.example/paid' },
32
+ method: { type: 'string', enum: ['GET', 'POST'], description: 'HTTP method to probe with, default GET' },
33
+ },
34
+ required: ['url'],
35
+ },
36
+ },
37
+ ];
38
+
39
+ function runCli(args) {
40
+ return new Promise((resolve) => {
41
+ const child = spawn(process.execPath, [cliPath, ...args, '--json'], { stdio: ['ignore', 'pipe', 'pipe'] });
42
+ let out = '', err = '';
43
+ child.stdout.on('data', (d) => (out += d));
44
+ child.stderr.on('data', (d) => (err += d));
45
+ const t = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* gone */ } }, 45000);
46
+ child.on('close', (code) => {
47
+ clearTimeout(t);
48
+ resolve({ code, out: out.trim(), err: err.trim() });
49
+ });
50
+ });
51
+ }
52
+
53
+ async function callTool(name, args) {
54
+ if (name === 'schema_check') {
55
+ const r = await runCli(['schema', String(args.url)]);
56
+ const text = r.out || r.err || `exit ${r.code}`;
57
+ return { content: [{ type: 'text', text }], isError: r.code === 1 ? false : r.code >= 2 };
58
+ }
59
+ if (name === 'x402_inspect') {
60
+ const m = ['GET', 'POST'].includes(String(args.method ?? '').toUpperCase()) ? `--method=${String(args.method).toUpperCase()}` : '';
61
+ const r = await runCli(['inspect', String(args.url), m].filter(Boolean));
62
+ const text = r.out || r.err || `exit ${r.code}`;
63
+ return { content: [{ type: 'text', text }], isError: r.code === 1 ? false : r.code >= 2 };
64
+ }
65
+ throw new Error(`unknown tool: ${name}`);
66
+ }
67
+
68
+ const serverInfo = { name: 'mcpdoctor', version: '0.1.7' };
69
+ const write = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
70
+
71
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
72
+ rl.on('line', async (line) => {
73
+ if (!line.trim()) return;
74
+ let msg;
75
+ try { msg = JSON.parse(line); } catch { return; }
76
+ const { id, method, params } = msg;
77
+ const respond = (result) => id !== undefined && write({ jsonrpc: '2.0', id, result });
78
+ const fail = (code, message) => id !== undefined && write({ jsonrpc: '2.0', id, error: { code, message } });
79
+
80
+ switch (method) {
81
+ case 'initialize':
82
+ respond({ protocolVersion: params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo });
83
+ break;
84
+ case 'notifications/initialized':
85
+ case 'notifications/cancelled':
86
+ break;
87
+ case 'ping':
88
+ respond({});
89
+ break;
90
+ case 'tools/list':
91
+ respond({ tools: TOOLS });
92
+ break;
93
+ case 'tools/call': {
94
+ try {
95
+ const result = await callTool(String(params?.name), params?.arguments ?? {});
96
+ respond(result);
97
+ } catch (e) {
98
+ fail(-32000, String(e.message ?? e));
99
+ }
100
+ break;
101
+ }
102
+ default:
103
+ fail(-32601, `method not found: ${method}`);
104
+ }
105
+ });
package/bin/mcpdoctor.mjs CHANGED
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
3
3
 
4
4
  const MAX_BODY = 1024 * 1024;
5
5
  const TIMEOUT = 10_000;
6
- const VERSION = '0.1.4';
6
+ const VERSION = '0.1.5';
7
7
 
8
8
  const help = `mcpdoctor ${VERSION}
9
9
 
@@ -47,6 +47,16 @@ function headersOf(headers) {
47
47
  }
48
48
 
49
49
  function json(text) { try { return JSON.parse(text); } catch { return null; } }
50
+ function jsonrpc(text) {
51
+ const direct = json(text);
52
+ if (direct && (direct.result !== undefined || direct.error !== undefined)) return direct;
53
+ for (const line of String(text).split(/\r?\n/)) {
54
+ if (!line.startsWith('data:')) continue;
55
+ const obj = json(line.slice(5).trim());
56
+ if (obj && (obj.result !== undefined || obj.error !== undefined)) return obj;
57
+ }
58
+ return direct;
59
+ }
50
60
 
51
61
  function findPaymentDocument(headers, body) {
52
62
  const candidates = [headers['payment-required'], headers['x-payment-required'], headers['www-authenticate'], body].filter(Boolean);
@@ -133,13 +143,13 @@ async function schemaCommand(url, format) {
133
143
  const response = await fetch(url, { method: 'POST', redirect: 'manual', signal: AbortSignal.timeout(TIMEOUT), headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', 'MCP-Protocol-Version': '2025-03-26' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'mcpdoctor', version: VERSION } } }) });
134
144
  const initText = await readBody(response);
135
145
  if (!response.ok) throw new Error(`MCP initialize returned HTTP ${response.status}`);
136
- const init = json(initText);
146
+ const init = jsonrpc(initText);
137
147
  if (!init?.result?.protocolVersion) add('INITIALIZE_RESPONSE_INVALID', 'FAIL', 'Initialize response did not include result.protocolVersion');
138
148
  const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream', 'MCP-Protocol-Version': init?.result?.protocolVersion ?? '2025-03-26' };
139
149
  const list = await fetch(url, { method: 'POST', redirect: 'manual', signal: AbortSignal.timeout(TIMEOUT), headers, body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }) });
140
150
  const listText = await readBody(list);
141
151
  if (!list.ok) throw new Error(`tools/list returned HTTP ${list.status}`);
142
- const parsed = json(listText);
152
+ const parsed = jsonrpc(listText);
143
153
  const tools = parsed?.result?.tools;
144
154
  if (!Array.isArray(tools)) add('TOOLS_LIST_INVALID', 'FAIL', 'tools/list response did not contain result.tools[]');
145
155
  else if (!tools.length) add('NO_TOOLS', 'WARN', 'Server exposes an empty tool list');
package/package.json CHANGED
@@ -1,17 +1,51 @@
1
1
  {
2
2
  "name": "@eidonze/mcpdoctor",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "mcpName": "io.github.xka0085-byte/mcpdoctor",
5
5
  "description": "Read-only MCP and x402 endpoint preflight checks: 402 challenge shape, payment document parsing, accepts[] conformance, discovery manifests, response digest. Zero dependencies.",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
- "bin": { "mcpdoctor": "bin/mcpdoctor.mjs" },
8
+ "bin": {
9
+ "mcpdoctor": "bin/mcpdoctor.mjs",
10
+ "mcpdoctor-mcp": "bin/mcpdoctor-mcp.mjs"
11
+ },
9
12
  "main": "bin/mcpdoctor.mjs",
10
- "files": ["bin", "test", "README.md", "LICENSE"],
11
- "engines": { "node": ">=18" },
12
- "scripts": { "test": "node test/smoke.mjs" },
13
- "keywords": ["mcp", "model-context-protocol", "mcp-security", "mcp-scanner", "mcp-schema", "mcp-oauth", "agent-security", "tool-schema", "x402-inspector", "http-402", "mcp-cli", "mcp-debugging", "developer-tools", "diagnostics", "x402", "cli"],
14
- "repository": { "type": "git", "url": "git+https://github.com/xka0085-byte/mcp-doctor.git" },
15
- "bugs": { "url": "https://github.com/xka0085-byte/mcp-doctor/issues" },
13
+ "files": [
14
+ "bin",
15
+ "test",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "test": "node test/smoke.mjs"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "mcp-security",
29
+ "mcp-scanner",
30
+ "mcp-schema",
31
+ "mcp-oauth",
32
+ "agent-security",
33
+ "tool-schema",
34
+ "x402-inspector",
35
+ "http-402",
36
+ "mcp-cli",
37
+ "mcp-debugging",
38
+ "developer-tools",
39
+ "diagnostics",
40
+ "x402",
41
+ "cli"
42
+ ],
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/xka0085-byte/mcp-doctor.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/xka0085-byte/mcp-doctor/issues"
49
+ },
16
50
  "homepage": "https://xka0085-byte.github.io/evidence-tools/"
17
51
  }
package/test/smoke.mjs CHANGED
@@ -29,6 +29,18 @@ const server = createServer(async (request, response) => {
29
29
  }
30
30
  response.writeHead(200, { 'content-type': 'application/json' }); response.end(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, result: { tools: [{ name: 'hello', description: 'Returns a greeting.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name.' } }, required: ['name'] } }] } })); return;
31
31
  }
32
+ // regression (found against production DeepWiki): servers may answer POST with
33
+ // an SSE stream (text/event-stream) even for plain JSON-RPC calls
34
+ if (request.url === '/mcp-sse') {
35
+ let incoming = ''; for await (const chunk of request) incoming += chunk;
36
+ const rpc = JSON.parse(incoming || '{}');
37
+ const payload = rpc.method === 'initialize'
38
+ ? { jsonrpc: '2.0', id: rpc.id, result: { protocolVersion: '2025-06-18', serverInfo: { name: 'sse-fixture', version: '1' }, capabilities: {} } }
39
+ : { jsonrpc: '2.0', id: rpc.id, result: { tools: [{ name: 'hello', description: 'Returns a greeting.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name.' } }, required: ['name'] } }] } };
40
+ response.writeHead(200, { 'content-type': 'text/event-stream' });
41
+ response.end(`event: message\ndata: ${JSON.stringify(payload)}\n\n`);
42
+ return;
43
+ }
32
44
  // regression (our original production bug): body validation must not shadow the 402
33
45
  if (request.url === '/bad-400') {
34
46
  response.writeHead(400, { 'content-type': 'application/json' });
@@ -83,6 +95,11 @@ try {
83
95
  const schemaReport = JSON.parse(schema.stdout);
84
96
  assert.equal(schemaReport.finalStatus, 'PASS', JSON.stringify(schemaReport.findings));
85
97
  assert.equal(schemaReport.mcp.toolCount, 1);
98
+ const sse = await runSchema(`http://127.0.0.1:${port}/mcp-sse`);
99
+ assert.equal(sse.code, 0, sse.stderr);
100
+ const sseReport = JSON.parse(sse.stdout);
101
+ assert.equal(sseReport.finalStatus, 'PASS', JSON.stringify(sseReport.findings));
102
+ assert.equal(sseReport.mcp.toolCount, 1, 'SSE-wrapped JSON-RPC must be parsed');
86
103
  const help = await new Promise((resolve) => {
87
104
  const child = spawn(process.execPath, [cli, '--help'], { stdio: ['ignore', 'pipe', 'pipe'] });
88
105
  let stdout = ''; child.stdout.on('data', (chunk) => { stdout += chunk; }); child.on('close', (code) => resolve({ code, stdout }));