@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 CHANGED
@@ -1,36 +1,65 @@
1
1
  # mcpdoctor
2
2
 
3
- > ⚠️ **Official package notice**: this is the official `mcpdoctor`, published by npm user [`eidonze`](https://www.npmjs.com/~eidonze), repo [`xka0085-byte/mcp-doctor`](https://github.com/xka0085-byte/mcp-doctor). We are **NOT affiliated** with **mcpdoctor.dev** — that is a *different* product (an MCP **server-auditing** tool). This `mcpdoctor` is an independent, read-only **402/x402 payment-endpoint inspector CLI**. Same name, different tool. Verify the package by its scope: `@eidonze/mcpdoctor`.
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
- **mcpdoctor is a read-only CLI for inspecting MCP and x402 endpoints.**
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)
6
8
 
7
- Point it at a URL, get one `PASS / FAIL / UNKNOWN` report:
9
+ ## Find the right guide
8
10
 
9
- - the **HTTP 402 payment document** — parsed from the known locations
10
- (`Payment-Required` / `X-Payment-Required` / `WWW-Authenticate` headers or
11
- the response body), in both x402 v1 (`accepts[]`) and v2 (`x402.accepts`)
12
- shapes, with every `scheme / network / asset / amount / payTo` field checked;
13
- - the **discovery manifests** — whether `/.well-known/mcp/server.json` and
14
- `/.well-known/x402` are reachable, with a SHA-256 of each;
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
- mcpdoctor is **not an MCP server** and does not implement the MCP protocol.
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
- ## Install
20
+ Check an MCP server without calling any tools:
23
21
 
24
22
  ```bash
25
- npm install -g @eidonze/mcpdoctor # or run without installing:
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
- Current release: `0.1.4`. The CLI is read-only: it does not sign, pay, retry payment, invoke MCP tools, or accept private keys.
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
- Run the check in pull requests without installing a package or storing a key:
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/mcpdoctor@v1
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
- `schema` performs read-only `initialize` and `tools/list` checks. Use `mode: inspect`
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
- ## Why
81
+ ## Starter template
55
82
 
56
- Shipping an x402 service ourselves ([ReceiptRail](https://github.com/xka0085-byte/agenttoll)),
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
- - an endpoint that validates the request body **before** returning the 402
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
- Each check in mcpdoctor maps to a failure mode we actually hit in production.
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
- ## Usage
89
+ ## What the report means
70
90
 
71
- ```bash
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
- Exit codes: `0` PASS · `1` FAIL · `2` UNKNOWN (network/timeout) · `3` usage error.
93
+ Example schema failure:
82
94
 
83
- ## Tool schema checks
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
- This is deterministic static lint, not a prediction of model tool selection or a cross-client certification. See [`SCHEMA-MVP.md`](./SCHEMA-MVP.md).
99
+ Example x402 failure:
92
100
 
93
- ## Real examples (captured 2026-09-29, against live endpoints)
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
- Inspecting the official x402 demo endpoint — it does return 402, but ships a
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
- We ship an x402 service ourselves ([ReceiptRail](https://github.com/xka0085-byte/agenttoll))
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
- - an endpoint that validates the request body **before** returning the 402
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
- Each check in mcpdoctor maps to a failure mode we actually hit in production.
111
+ ## Feedback
153
112
 
154
- ## Part of the Agent / Chain Evidence Tools suite
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
- Read-only diagnostics for AI agents on Web3 —
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 licensed. Known limitations are documented, not hidden.
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.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,50 @@
1
1
  {
2
2
  "name": "@eidonze/mcpdoctor",
3
- "version": "0.1.4",
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": { "mcpdoctor": "bin/mcpdoctor.mjs" },
8
+ "bin": {
9
+ "mcpdoctor": "bin/mcpdoctor.mjs"
10
+ },
9
11
  "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-cli", "mcp-inspector", "mcp-endpoint-inspector", "mcp-debugging", "x402", "cli", "developer-tools", "diagnostics"],
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" },
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 }));