@eidonze/mcpdoctor 0.1.4 → 0.1.6
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 +69 -113
- package/bin/mcpdoctor.mjs +13 -3
- package/package.json +41 -8
- package/test/smoke.mjs +17 -0
package/README.md
CHANGED
|
@@ -1,36 +1,65 @@
|
|
|
1
1
|
# mcpdoctor
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@eidonze/mcpdoctor)
|
|
6
|
+
[](https://github.com/xka0085-byte/mcp-doctor/actions/workflows/ci.yml)
|
|
7
|
+
[](LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Find the right guide
|
|
8
10
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
- a **SHA-256 digest** of the response body, plus latency and size.
|
|
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)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
It is the endpoint inspector you run *before* pointing an MCP client or an
|
|
19
|
-
x402 buyer at an endpoint — a preflight/debugging tool for AI-agent payment
|
|
20
|
-
infrastructure. Zero dependencies. Node ≥ 18. Never touches keys, never pays.
|
|
18
|
+
## Run it now
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
Check an MCP server without calling any tools:
|
|
23
21
|
|
|
24
22
|
```bash
|
|
25
|
-
|
|
26
|
-
npx @eidonze/mcpdoctor inspect <url>
|
|
23
|
+
npx @eidonze/mcpdoctor@0.1.5 schema https://your-mcp-server.example/mcp --json
|
|
27
24
|
```
|
|
28
25
|
|
|
29
|
-
|
|
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
|
+
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.
|
|
30
59
|
|
|
31
60
|
## GitHub Actions
|
|
32
61
|
|
|
33
|
-
|
|
62
|
+
Add a read-only check to pull requests:
|
|
34
63
|
|
|
35
64
|
```yaml
|
|
36
65
|
name: MCP Trust Check
|
|
@@ -40,122 +69,49 @@ jobs:
|
|
|
40
69
|
mcp-trust:
|
|
41
70
|
runs-on: ubuntu-latest
|
|
42
71
|
steps:
|
|
43
|
-
- uses: xka0085-byte/
|
|
72
|
+
- uses: xka0085-byte/mcp-doctor@v1
|
|
44
73
|
with:
|
|
45
74
|
endpoint: https://your-mcp-server.example/mcp
|
|
46
75
|
mode: schema
|
|
47
76
|
format: markdown
|
|
48
77
|
```
|
|
49
78
|
|
|
50
|
-
`
|
|
51
|
-
for a read-only x402 HTTP 402 preflight. A non-zero exit means the observed result
|
|
52
|
-
was `FAIL` or `UNKNOWN`; this is a CI gate, not a security certification.
|
|
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.
|
|
53
80
|
|
|
54
|
-
##
|
|
81
|
+
## Starter template
|
|
55
82
|
|
|
56
|
-
|
|
57
|
-
we hit every way a paid endpoint can silently break:
|
|
83
|
+
Start a new MCP project with this check already wired in:
|
|
58
84
|
|
|
59
|
-
-
|
|
60
|
-
challenge is invisible to every x402 client — they only react to `402`;
|
|
61
|
-
- payment documents arrive in **four different places** (`Payment-Required` /
|
|
62
|
-
`X-Payment-Required` / `WWW-Authenticate` headers, or the response body) and
|
|
63
|
-
in **two different shapes** (v1 top-level `accepts[]`, v2 nested `x402.accepts`);
|
|
64
|
-
parsers that only look at one place report false failures;
|
|
65
|
-
- vendor hint headers without `accepts[]` can shadow the real payment document.
|
|
85
|
+
- [Secure MCP Server Starter](examples/secure-mcp-server-starter/)
|
|
66
86
|
|
|
67
|
-
|
|
87
|
+
The check is read-only and should target a public test endpoint. Do not place private keys or credentials in workflow inputs.
|
|
68
88
|
|
|
69
|
-
##
|
|
89
|
+
## What the report means
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
mcpdoctor inspect <url> [--method=GET|POST] [--format=json|markdown]
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
| Option | Default | Description |
|
|
76
|
-
|---|---|---|
|
|
77
|
-
| `--method` | `GET` | HTTP verb for the probe (`POST` sends `{}`) |
|
|
78
|
-
| `--format` | `markdown` | `json` for CI / machine-readable reports |
|
|
79
|
-
| `--json` | — | shorthand for `--format=json` |
|
|
91
|
+
`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.
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
Example schema failure:
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Inspect MCP initialization and `tools/list` without calling tools:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
node mcpdoctor/bin/mcpdoctor.mjs schema https://your-mcp-server.example/mcp --json
|
|
95
|
+
```text
|
|
96
|
+
FAIL REQUIRED_PROPERTY_UNDEFINED tools[0] requires undeclared property query
|
|
89
97
|
```
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
Example x402 failure:
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Inspecting an MCP endpoint that is free (no payment gate) — the tool reports
|
|
97
|
-
the 200 instead of a 402, and still verifies the discovery manifests:
|
|
98
|
-
|
|
99
|
-
```json
|
|
100
|
-
{
|
|
101
|
-
"endpoint": "https://agenttoll-receipts.app.workbuddy.host/mcp",
|
|
102
|
-
"method": "POST",
|
|
103
|
-
"finalStatus": "FAIL",
|
|
104
|
-
"http": { "status": 200, "elapsedMs": 2830, "bodyBytes": 82,
|
|
105
|
-
"bodySha256": "598dc097dcca1e573c742100863899c00724a021b421a5bb49952d49fec6b4f4" },
|
|
106
|
-
"findings": [
|
|
107
|
-
{ "status": "FAIL", "code": "NO_402", "message": "Expected HTTP 402, received 200" }
|
|
108
|
-
],
|
|
109
|
-
"metadata": [
|
|
110
|
-
{ "path": "/.well-known/mcp/server.json", "status": 200, "reachable": true }
|
|
111
|
-
]
|
|
112
|
-
}
|
|
101
|
+
```text
|
|
102
|
+
FAIL NO_402 Expected HTTP 402, received 400
|
|
113
103
|
```
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
payment document mcpdoctor cannot parse, which is exactly the class of silent
|
|
117
|
-
breakage this CLI exists to catch:
|
|
118
|
-
|
|
119
|
-
```json
|
|
120
|
-
{
|
|
121
|
-
"endpoint": "https://x402.org/protected",
|
|
122
|
-
"finalStatus": "FAIL",
|
|
123
|
-
"http": { "status": 402 },
|
|
124
|
-
"headers": { "paymentRequired": true },
|
|
125
|
-
"findings": [
|
|
126
|
-
{ "status": "FAIL", "code": "NO_ACCEPTS",
|
|
127
|
-
"message": "No recognizable payment requirement found" }
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## What it does NOT do
|
|
133
|
-
|
|
134
|
-
- No MCP protocol session: no `initialize`, no `tools/list` — mcpdoctor
|
|
135
|
-
checks what an endpoint *advertises* (402 documents, discovery manifests),
|
|
136
|
-
not a full MCP handshake.
|
|
137
|
-
- No wallet, signing, settlement, or retries. Read-only preflight only.
|
|
138
|
-
- `PASS` means the observed 402 document had recognizable, complete fields —
|
|
139
|
-
it is not a security or payment-success certification.
|
|
140
|
-
|
|
141
|
-
## Why
|
|
105
|
+
## Why it exists
|
|
142
106
|
|
|
143
|
-
|
|
144
|
-
and hit every way a paid endpoint can silently break:
|
|
107
|
+
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.
|
|
145
108
|
|
|
146
|
-
|
|
147
|
-
challenge is invisible to every x402 client — they only react to `402`;
|
|
148
|
-
- payment documents arrive in **four different places** and **two different
|
|
149
|
-
shapes**; parsers that only look at one place report false failures;
|
|
150
|
-
- vendor hint headers without `accepts[]` can shadow the real payment document.
|
|
109
|
+
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`.
|
|
151
110
|
|
|
152
|
-
|
|
111
|
+
## Feedback
|
|
153
112
|
|
|
154
|
-
|
|
113
|
+
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.
|
|
155
114
|
|
|
156
|
-
|
|
157
|
-
[hub & docs](https://xka0085-byte.github.io/evidence-tools/) ·
|
|
158
|
-
[ReceiptRail (live MCP service)](https://github.com/xka0085-byte/agenttoll) ·
|
|
159
|
-
[npm: @eidonze/mcpdoctor](https://www.npmjs.com/package/@eidonze/mcpdoctor)
|
|
115
|
+
## License
|
|
160
116
|
|
|
161
|
-
MIT
|
|
117
|
+
MIT
|
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.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 =
|
|
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 =
|
|
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,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eidonze/mcpdoctor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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": {
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcpdoctor": "bin/mcpdoctor.mjs"
|
|
10
|
+
},
|
|
9
11
|
"main": "bin/mcpdoctor.mjs",
|
|
10
|
-
"files": [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"test",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node test/smoke.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"mcp-security",
|
|
28
|
+
"mcp-scanner",
|
|
29
|
+
"mcp-schema",
|
|
30
|
+
"mcp-oauth",
|
|
31
|
+
"agent-security",
|
|
32
|
+
"tool-schema",
|
|
33
|
+
"x402-inspector",
|
|
34
|
+
"http-402",
|
|
35
|
+
"mcp-cli",
|
|
36
|
+
"mcp-debugging",
|
|
37
|
+
"developer-tools",
|
|
38
|
+
"diagnostics",
|
|
39
|
+
"x402",
|
|
40
|
+
"cli"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/xka0085-byte/mcp-doctor.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/xka0085-byte/mcp-doctor/issues"
|
|
48
|
+
},
|
|
16
49
|
"homepage": "https://xka0085-byte.github.io/evidence-tools/"
|
|
17
50
|
}
|
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 }));
|