@eidonze/mcpdoctor 0.1.6 → 0.1.8
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 +15 -3
- package/bin/mcpdoctor-mcp.mjs +105 -0
- package/bin/mcpdoctor.mjs +9 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -20,13 +20,13 @@ Read-only checks for MCP tool schemas and x402 payment endpoints. Use it before
|
|
|
20
20
|
Check an MCP server without calling any tools:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npx @eidonze/mcpdoctor
|
|
23
|
+
npx @eidonze/mcpdoctor schema https://your-mcp-server.example/mcp --json
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Check an x402 endpoint without paying:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npx @eidonze/mcpdoctor
|
|
29
|
+
npx @eidonze/mcpdoctor inspect https://your-api.example/paid --method=POST --json
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Requires Node.js 18+. No API key or wallet is needed.
|
|
@@ -57,6 +57,18 @@ The exact same run against a healthy server prints `Status: PASS` and exits 0, s
|
|
|
57
57
|
|
|
58
58
|
Exit codes are stable for automation: `0` PASS, `1` FAIL, `2` UNKNOWN/network error, `3` usage error.
|
|
59
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
|
+
|
|
60
72
|
## GitHub Actions
|
|
61
73
|
|
|
62
74
|
Add a read-only check to pull requests:
|
|
@@ -76,7 +88,7 @@ jobs:
|
|
|
76
88
|
format: markdown
|
|
77
89
|
```
|
|
78
90
|
|
|
79
|
-
Use `mode: inspect` for an x402 preflight. The action fails on `FAIL` or `UNKNOWN` by default; set `fail-on: false` for an informational check.
|
|
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. Looking for it in the Marketplace? Search "MCP Trust Check" or "mcp server test", or browse the [mcp-trust-check listing](https://github.com/marketplace/actions/mcp-trust-check).
|
|
80
92
|
|
|
81
93
|
## Starter template
|
|
82
94
|
|
|
@@ -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.8' };
|
|
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.
|
|
6
|
+
const VERSION = '0.1.8';
|
|
7
7
|
|
|
8
8
|
const help = `mcpdoctor ${VERSION}
|
|
9
9
|
|
|
@@ -133,7 +133,8 @@ function markdown(report) {
|
|
|
133
133
|
const lines = [`# mcpdoctor report`, '', `- Status: **${report.finalStatus}**`, `- Endpoint: ${report.endpoint}`, `- HTTP: ${report.http.status}`, `- Response: ${report.http.elapsedMs} ms / ${report.http.bodyBytes} bytes`, `- Body SHA-256: \`${report.http.bodySha256}\``, '', '## Payment requirements', ''];
|
|
134
134
|
if (!report.x402.accepts.length) lines.push('- None parsed', '');
|
|
135
135
|
report.x402.accepts.forEach((item, i) => lines.push(`### accepts[${i}]`, '', ...Object.entries(item).map(([key, value]) => `- ${key}: ${value ?? 'missing'}`), ''));
|
|
136
|
-
lines.push('## Findings', '', ...(report.findings.length ? report.findings.map((item) => `- **${item.status}** \`${item.code}\`: ${item.message}`) : ['- None']), '', '## Limitations', '', ...report.limitations.map((item) => `- ${item}`)
|
|
136
|
+
lines.push('## Findings', '', ...(report.findings.length ? report.findings.map((item) => `- **${item.status}** \`${item.code}\`: ${item.message}`) : ['- None']), '', '## Limitations', '', ...report.limitations.map((item) => `- ${item}`));
|
|
137
|
+
if (report.finalStatus === 'PASS') lines.push('', '## Next steps', '', 'Gate CI on this exact check (paste into .github/workflows/x402.yml):', '', '```yaml', '- uses: xka0085-byte/mcp-doctor@v1', ' with:', ` endpoint: ${report.endpoint}`, ' mode: inspect', '```', '', 'Weekly re-scan and docs: https://xka0085-byte.github.io/mcp-doctor/x402-endpoint-inspector.html');
|
|
137
138
|
return lines.join('\n');
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -166,7 +167,12 @@ async function schemaCommand(url, format) {
|
|
|
166
167
|
}
|
|
167
168
|
if (!findings.length) add('STATIC_SCHEMA_CHECKS_PASSED', 'INFO', `${tools.length} tools passed deterministic schema checks`);
|
|
168
169
|
const report = { reportVersion: 1, inspectorVersion: VERSION, mode: 'read-only MCP initialize + tools/list', checkedAt: new Date().toISOString(), endpoint: url.toString(), finalStatus: findings.some(x => x.severity === 'FAIL') ? 'FAIL' : 'PASS', mcp: { protocolVersion: init?.result?.protocolVersion ?? null, serverInfo: init?.result?.serverInfo ?? null, toolCount: Array.isArray(tools) ? tools.length : null }, findings, limitations: ['Static schema checks do not predict model tool selection or prove behavior across all clients.', 'No tool was called; side effects, authorization, data quality and security were not tested.'] };
|
|
169
|
-
|
|
170
|
+
if (format === 'json') { console.log(JSON.stringify(report, null, 2)); }
|
|
171
|
+
else {
|
|
172
|
+
let text = `# MCP Tool Schema Report\n\n- Status: **${report.finalStatus}**\n- Endpoint: ${report.endpoint}\n- Protocol: ${report.mcp.protocolVersion ?? 'unknown'}\n- Tools: ${report.mcp.toolCount ?? 'unknown'}\n\n${findings.map(x => `- **${x.severity}** \`${x.code}\`: ${x.message}`).join('\n')}\n\n${report.limitations.map(x => `- ${x}`).join('\n')}`;
|
|
173
|
+
if (report.finalStatus === 'PASS') text += `\n\nNext steps\n- Gate CI on this exact check (paste into .github/workflows/mcp.yml):\n\n - uses: xka0085-byte/mcp-doctor@v1\n with:\n endpoint: ${report.endpoint}\n mode: schema\n\n- Weekly re-scan and a README status badge: https://xka0085-byte.github.io/mcp-doctor/trust/\n`;
|
|
174
|
+
console.log(text);
|
|
175
|
+
}
|
|
170
176
|
process.exitCode = report.finalStatus === 'PASS' ? 0 : 1;
|
|
171
177
|
}
|
|
172
178
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eidonze/mcpdoctor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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
8
|
"bin": {
|
|
9
|
-
"mcpdoctor": "bin/mcpdoctor.mjs"
|
|
9
|
+
"mcpdoctor": "bin/mcpdoctor.mjs",
|
|
10
|
+
"mcpdoctor-mcp": "bin/mcpdoctor-mcp.mjs"
|
|
10
11
|
},
|
|
11
12
|
"main": "bin/mcpdoctor.mjs",
|
|
12
13
|
"files": [
|