@yawlabs/mcp-compliance 0.1.1 → 0.1.2
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 +19 -2
- package/dist/{chunk-4AQGMM2X.js → chunk-OOJ4PMF7.js} +71 -21
- package/dist/index.js +97 -30
- package/dist/mcp/server.js +5 -2
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,23 @@ mcp-compliance test https://my-server.com/mcp --format json
|
|
|
27
27
|
|
|
28
28
|
# Strict mode — exits with code 1 on required test failure (for CI)
|
|
29
29
|
mcp-compliance test https://my-server.com/mcp --strict
|
|
30
|
+
|
|
31
|
+
# With authentication
|
|
32
|
+
mcp-compliance test https://my-server.com/mcp --auth "Bearer tok123"
|
|
33
|
+
|
|
34
|
+
# Custom headers (repeatable)
|
|
35
|
+
mcp-compliance test https://my-server.com/mcp -H "Authorization: Bearer tok123" -H "X-Api-Key: abc"
|
|
30
36
|
```
|
|
31
37
|
|
|
38
|
+
### Options
|
|
39
|
+
|
|
40
|
+
| Option | Description |
|
|
41
|
+
|--------|-------------|
|
|
42
|
+
| `--format <format>` | Output format: `terminal` or `json` (default: `terminal`) |
|
|
43
|
+
| `--strict` | Exit with code 1 on any required test failure (for CI) |
|
|
44
|
+
| `-H, --header <header>` | Add header to all requests, format `"Key: Value"` (repeatable) |
|
|
45
|
+
| `--auth <token>` | Shorthand for `-H "Authorization: <token>"` |
|
|
46
|
+
|
|
32
47
|
### Get badge markdown
|
|
33
48
|
|
|
34
49
|
```bash
|
|
@@ -119,13 +134,15 @@ Add to your Claude Code MCP config:
|
|
|
119
134
|
"mcpServers": {
|
|
120
135
|
"mcp-compliance": {
|
|
121
136
|
"command": "npx",
|
|
122
|
-
"args": ["@yawlabs/mcp-compliance
|
|
137
|
+
"args": ["-y", "@yawlabs/mcp-compliance"],
|
|
138
|
+
"env": {},
|
|
139
|
+
"args_extra": ["mcp"]
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
}
|
|
126
143
|
```
|
|
127
144
|
|
|
128
|
-
Or
|
|
145
|
+
Or run the MCP server directly:
|
|
129
146
|
|
|
130
147
|
```json
|
|
131
148
|
{
|
|
@@ -44,8 +44,7 @@ function generateBadge(url) {
|
|
|
44
44
|
} catch {
|
|
45
45
|
parsed = new URL("https://unknown");
|
|
46
46
|
}
|
|
47
|
-
const
|
|
48
|
-
const encoded = encodeURIComponent(hostname);
|
|
47
|
+
const encoded = encodeURIComponent(parsed.href);
|
|
49
48
|
const imageUrl = `https://mcp.hosting/api/compliance/${encoded}/badge`;
|
|
50
49
|
const reportUrl = `https://mcp.hosting/compliance/${encoded}`;
|
|
51
50
|
return {
|
|
@@ -91,8 +90,26 @@ function createIdCounter() {
|
|
|
91
90
|
let id = 0;
|
|
92
91
|
return () => ++id;
|
|
93
92
|
}
|
|
93
|
+
function parseSSEResponse(text) {
|
|
94
|
+
const lines = text.split("\n");
|
|
95
|
+
let lastJsonRpcResponse = null;
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
if (line.startsWith("data: ")) {
|
|
98
|
+
const data = line.slice(6).trim();
|
|
99
|
+
if (!data) continue;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(data);
|
|
102
|
+
if (parsed.jsonrpc === "2.0" && parsed.id !== void 0) {
|
|
103
|
+
lastJsonRpcResponse = parsed;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lastJsonRpcResponse;
|
|
110
|
+
}
|
|
94
111
|
var _defaultNextId = createIdCounter();
|
|
95
|
-
async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId) {
|
|
112
|
+
async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId, extraHeaders) {
|
|
96
113
|
const id = nextId();
|
|
97
114
|
const body = JSON.stringify({
|
|
98
115
|
jsonrpc: "2.0",
|
|
@@ -100,12 +117,14 @@ async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId) {
|
|
|
100
117
|
method,
|
|
101
118
|
params: params || {}
|
|
102
119
|
});
|
|
120
|
+
const headers = {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
"Accept": "application/json, text/event-stream",
|
|
123
|
+
...extraHeaders
|
|
124
|
+
};
|
|
103
125
|
const res = await request(backendUrl, {
|
|
104
126
|
method: "POST",
|
|
105
|
-
headers
|
|
106
|
-
"Content-Type": "application/json",
|
|
107
|
-
"Accept": "application/json, text/event-stream"
|
|
108
|
-
},
|
|
127
|
+
headers,
|
|
109
128
|
body,
|
|
110
129
|
signal: AbortSignal.timeout(15e3)
|
|
111
130
|
});
|
|
@@ -114,16 +133,32 @@ async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId) {
|
|
|
114
133
|
for (const [k, v] of Object.entries(res.headers)) {
|
|
115
134
|
if (typeof v === "string") responseHeaders[k] = v;
|
|
116
135
|
}
|
|
136
|
+
const contentType = (responseHeaders["content-type"] || "").toLowerCase();
|
|
137
|
+
if (contentType.includes("text/event-stream")) {
|
|
138
|
+
const parsed = parseSSEResponse(text);
|
|
139
|
+
if (parsed) {
|
|
140
|
+
return { statusCode: res.statusCode, body: parsed, headers: responseHeaders };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return { statusCode: res.statusCode, body: JSON.parse(text), headers: responseHeaders };
|
|
144
|
+
} catch {
|
|
145
|
+
return { statusCode: res.statusCode, body: { _raw: text }, headers: responseHeaders };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
117
148
|
try {
|
|
118
149
|
return { statusCode: res.statusCode, body: JSON.parse(text), headers: responseHeaders };
|
|
119
150
|
} catch {
|
|
120
151
|
return { statusCode: res.statusCode, body: { _raw: text }, headers: responseHeaders };
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
|
-
async function mcpNotification(backendUrl, method, params) {
|
|
154
|
+
async function mcpNotification(backendUrl, method, params, extraHeaders) {
|
|
155
|
+
const headers = {
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
...extraHeaders
|
|
158
|
+
};
|
|
124
159
|
await request(backendUrl, {
|
|
125
160
|
method: "POST",
|
|
126
|
-
headers
|
|
161
|
+
headers,
|
|
127
162
|
body: JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} }),
|
|
128
163
|
signal: AbortSignal.timeout(5e3)
|
|
129
164
|
}).then((r) => r.body.text()).catch(() => {
|
|
@@ -143,7 +178,16 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
143
178
|
const backendUrl = url;
|
|
144
179
|
const tests = [];
|
|
145
180
|
const nextId = createIdCounter();
|
|
146
|
-
|
|
181
|
+
let sessionId = null;
|
|
182
|
+
let negotiatedProtocolVersion = null;
|
|
183
|
+
const userHeaders = options.headers || {};
|
|
184
|
+
function buildHeaders() {
|
|
185
|
+
const h = { ...userHeaders };
|
|
186
|
+
if (sessionId) h["mcp-session-id"] = sessionId;
|
|
187
|
+
if (negotiatedProtocolVersion) h["mcp-protocol-version"] = negotiatedProtocolVersion;
|
|
188
|
+
return h;
|
|
189
|
+
}
|
|
190
|
+
const rpc = (method, params) => mcpRequest(backendUrl, method, params, nextId, buildHeaders());
|
|
147
191
|
let serverInfo = {
|
|
148
192
|
protocolVersion: null,
|
|
149
193
|
name: null,
|
|
@@ -186,7 +230,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
186
230
|
await test("transport-post", "HTTP POST accepted", "transport", true, "basic/transports#streamable-http", async () => {
|
|
187
231
|
const res = await request(backendUrl, {
|
|
188
232
|
method: "POST",
|
|
189
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
|
|
233
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", ...userHeaders },
|
|
190
234
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
191
235
|
signal: AbortSignal.timeout(1e4)
|
|
192
236
|
});
|
|
@@ -198,7 +242,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
198
242
|
await test("transport-content-type", "Responds with JSON or SSE", "transport", true, "basic/transports#streamable-http", async () => {
|
|
199
243
|
const res = await request(backendUrl, {
|
|
200
244
|
method: "POST",
|
|
201
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
|
|
245
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", ...userHeaders },
|
|
202
246
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
203
247
|
signal: AbortSignal.timeout(1e4)
|
|
204
248
|
});
|
|
@@ -221,6 +265,9 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
221
265
|
serverInfo.name = result.serverInfo?.name || null;
|
|
222
266
|
serverInfo.version = result.serverInfo?.version || null;
|
|
223
267
|
serverInfo.capabilities = result.capabilities || {};
|
|
268
|
+
const sid = initRes.headers["mcp-session-id"];
|
|
269
|
+
if (sid) sessionId = sid;
|
|
270
|
+
if (result.protocolVersion) negotiatedProtocolVersion = result.protocolVersion;
|
|
224
271
|
return { passed: !!result.protocolVersion, details: `Protocol: ${result.protocolVersion || "missing"}` };
|
|
225
272
|
});
|
|
226
273
|
await test("lifecycle-proto-version", "Returns valid protocol version", "lifecycle", true, "basic/lifecycle#version-negotiation", async () => {
|
|
@@ -244,7 +291,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
244
291
|
const valid = body?.jsonrpc === "2.0" && body?.id !== void 0 && (body?.result !== void 0 || body?.error !== void 0);
|
|
245
292
|
return { passed: valid, details: valid ? "Valid JSON-RPC 2.0 response" : `Missing fields: jsonrpc=${body?.jsonrpc}, id=${body?.id}` };
|
|
246
293
|
});
|
|
247
|
-
await mcpNotification(backendUrl, "notifications/initialized");
|
|
294
|
+
await mcpNotification(backendUrl, "notifications/initialized", void 0, buildHeaders());
|
|
248
295
|
await test("lifecycle-ping", "Responds to ping", "lifecycle", true, "basic/utilities#ping", async () => {
|
|
249
296
|
const res = await rpc("ping");
|
|
250
297
|
const body = res.body;
|
|
@@ -253,17 +300,18 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
253
300
|
return { passed: false, details: "No result in ping response" };
|
|
254
301
|
});
|
|
255
302
|
const hasTools = !!serverInfo.capabilities.tools;
|
|
303
|
+
let cachedToolsList = null;
|
|
256
304
|
await test("tools-list", "tools/list returns valid response", "tools", hasTools, "server/tools#listing-tools", async () => {
|
|
257
305
|
const res = await rpc("tools/list");
|
|
258
306
|
const tools = res.body?.result?.tools;
|
|
259
307
|
if (!Array.isArray(tools)) return { passed: false, details: "No tools array in result" };
|
|
308
|
+
cachedToolsList = tools;
|
|
260
309
|
toolCount = tools.length;
|
|
261
310
|
toolNames = tools.map((t) => t.name).filter(Boolean);
|
|
262
311
|
return { passed: true, details: `${toolCount} tool(s): ${toolNames.slice(0, 5).join(", ")}${toolCount > 5 ? "..." : ""}` };
|
|
263
312
|
});
|
|
264
313
|
await test("tools-schema", "All tools have name and inputSchema", "schema", hasTools, "server/tools#data-types", async () => {
|
|
265
|
-
const
|
|
266
|
-
const tools = res.body?.result?.tools || [];
|
|
314
|
+
const tools = cachedToolsList ?? (await rpc("tools/list")).body?.result?.tools ?? [];
|
|
267
315
|
const issues = [];
|
|
268
316
|
const warnings = [];
|
|
269
317
|
for (const tool of tools) {
|
|
@@ -319,16 +367,17 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
319
367
|
}
|
|
320
368
|
const hasResources = !!serverInfo.capabilities.resources;
|
|
321
369
|
if (hasResources) {
|
|
370
|
+
let cachedResourcesList = null;
|
|
322
371
|
await test("resources-list", "resources/list returns valid response", "resources", true, "server/resources#listing-resources", async () => {
|
|
323
372
|
const res = await rpc("resources/list");
|
|
324
373
|
const resources = res.body?.result?.resources;
|
|
325
374
|
if (!Array.isArray(resources)) return { passed: false, details: "No resources array" };
|
|
375
|
+
cachedResourcesList = resources;
|
|
326
376
|
resourceCount = resources.length;
|
|
327
377
|
return { passed: true, details: `${resourceCount} resource(s)` };
|
|
328
378
|
});
|
|
329
379
|
await test("resources-schema", "Resources have uri and name", "schema", true, "server/resources#data-types", async () => {
|
|
330
|
-
const
|
|
331
|
-
const resources = res.body?.result?.resources || [];
|
|
380
|
+
const resources = cachedResourcesList ?? (await rpc("resources/list")).body?.result?.resources ?? [];
|
|
332
381
|
const issues = [];
|
|
333
382
|
for (const r of resources) {
|
|
334
383
|
if (!r.uri) issues.push("Resource missing uri");
|
|
@@ -381,17 +430,18 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
381
430
|
const hasPrompts = !!serverInfo.capabilities.prompts;
|
|
382
431
|
if (hasPrompts) {
|
|
383
432
|
let promptNames = [];
|
|
433
|
+
let cachedPromptsList = null;
|
|
384
434
|
await test("prompts-list", "prompts/list returns valid response", "prompts", true, "server/prompts#listing-prompts", async () => {
|
|
385
435
|
const res = await rpc("prompts/list");
|
|
386
436
|
const prompts = res.body?.result?.prompts;
|
|
387
437
|
if (!Array.isArray(prompts)) return { passed: false, details: "No prompts array" };
|
|
438
|
+
cachedPromptsList = prompts;
|
|
388
439
|
promptCount = prompts.length;
|
|
389
440
|
promptNames = prompts.map((p) => p.name).filter(Boolean);
|
|
390
441
|
return { passed: true, details: `${promptCount} prompt(s): ${promptNames.slice(0, 5).join(", ")}${promptCount > 5 ? "..." : ""}` };
|
|
391
442
|
});
|
|
392
443
|
await test("prompts-schema", "Prompts have name field", "schema", true, "server/prompts#data-types", async () => {
|
|
393
|
-
const
|
|
394
|
-
const prompts = res.body?.result?.prompts || [];
|
|
444
|
+
const prompts = cachedPromptsList ?? (await rpc("prompts/list")).body?.result?.prompts ?? [];
|
|
395
445
|
const issues = [];
|
|
396
446
|
for (const p of prompts) {
|
|
397
447
|
if (!p.name) issues.push("Prompt missing name");
|
|
@@ -440,7 +490,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
440
490
|
await test("error-invalid-jsonrpc", "Handles malformed JSON-RPC", "errors", true, "basic", async () => {
|
|
441
491
|
const res = await request(backendUrl, {
|
|
442
492
|
method: "POST",
|
|
443
|
-
headers: { "Content-Type": "application/json" },
|
|
493
|
+
headers: { "Content-Type": "application/json", ...buildHeaders() },
|
|
444
494
|
body: JSON.stringify({ not: "a valid jsonrpc message" }),
|
|
445
495
|
signal: AbortSignal.timeout(1e4)
|
|
446
496
|
});
|
|
@@ -459,7 +509,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
459
509
|
await test("error-invalid-json", "Handles invalid JSON body", "errors", false, "basic", async () => {
|
|
460
510
|
const res = await request(backendUrl, {
|
|
461
511
|
method: "POST",
|
|
462
|
-
headers: { "Content-Type": "application/json" },
|
|
512
|
+
headers: { "Content-Type": "application/json", ...buildHeaders() },
|
|
463
513
|
body: "{this is not valid json!!!",
|
|
464
514
|
signal: AbortSignal.timeout(1e4)
|
|
465
515
|
});
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import chalk2 from "chalk";
|
|
6
|
+
import { createRequire } from "module";
|
|
6
7
|
|
|
7
8
|
// src/runner.ts
|
|
8
9
|
import { request } from "undici";
|
|
@@ -50,8 +51,7 @@ function generateBadge(url) {
|
|
|
50
51
|
} catch {
|
|
51
52
|
parsed = new URL("https://unknown");
|
|
52
53
|
}
|
|
53
|
-
const
|
|
54
|
-
const encoded = encodeURIComponent(hostname);
|
|
54
|
+
const encoded = encodeURIComponent(parsed.href);
|
|
55
55
|
const imageUrl = `https://mcp.hosting/api/compliance/${encoded}/badge`;
|
|
56
56
|
const reportUrl = `https://mcp.hosting/compliance/${encoded}`;
|
|
57
57
|
return {
|
|
@@ -69,8 +69,26 @@ function createIdCounter() {
|
|
|
69
69
|
let id = 0;
|
|
70
70
|
return () => ++id;
|
|
71
71
|
}
|
|
72
|
+
function parseSSEResponse(text) {
|
|
73
|
+
const lines = text.split("\n");
|
|
74
|
+
let lastJsonRpcResponse = null;
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (line.startsWith("data: ")) {
|
|
77
|
+
const data = line.slice(6).trim();
|
|
78
|
+
if (!data) continue;
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(data);
|
|
81
|
+
if (parsed.jsonrpc === "2.0" && parsed.id !== void 0) {
|
|
82
|
+
lastJsonRpcResponse = parsed;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lastJsonRpcResponse;
|
|
89
|
+
}
|
|
72
90
|
var _defaultNextId = createIdCounter();
|
|
73
|
-
async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId) {
|
|
91
|
+
async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId, extraHeaders) {
|
|
74
92
|
const id = nextId();
|
|
75
93
|
const body = JSON.stringify({
|
|
76
94
|
jsonrpc: "2.0",
|
|
@@ -78,12 +96,14 @@ async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId) {
|
|
|
78
96
|
method,
|
|
79
97
|
params: params || {}
|
|
80
98
|
});
|
|
99
|
+
const headers = {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"Accept": "application/json, text/event-stream",
|
|
102
|
+
...extraHeaders
|
|
103
|
+
};
|
|
81
104
|
const res = await request(backendUrl, {
|
|
82
105
|
method: "POST",
|
|
83
|
-
headers
|
|
84
|
-
"Content-Type": "application/json",
|
|
85
|
-
"Accept": "application/json, text/event-stream"
|
|
86
|
-
},
|
|
106
|
+
headers,
|
|
87
107
|
body,
|
|
88
108
|
signal: AbortSignal.timeout(15e3)
|
|
89
109
|
});
|
|
@@ -92,16 +112,32 @@ async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId) {
|
|
|
92
112
|
for (const [k, v] of Object.entries(res.headers)) {
|
|
93
113
|
if (typeof v === "string") responseHeaders[k] = v;
|
|
94
114
|
}
|
|
115
|
+
const contentType = (responseHeaders["content-type"] || "").toLowerCase();
|
|
116
|
+
if (contentType.includes("text/event-stream")) {
|
|
117
|
+
const parsed = parseSSEResponse(text);
|
|
118
|
+
if (parsed) {
|
|
119
|
+
return { statusCode: res.statusCode, body: parsed, headers: responseHeaders };
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return { statusCode: res.statusCode, body: JSON.parse(text), headers: responseHeaders };
|
|
123
|
+
} catch {
|
|
124
|
+
return { statusCode: res.statusCode, body: { _raw: text }, headers: responseHeaders };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
95
127
|
try {
|
|
96
128
|
return { statusCode: res.statusCode, body: JSON.parse(text), headers: responseHeaders };
|
|
97
129
|
} catch {
|
|
98
130
|
return { statusCode: res.statusCode, body: { _raw: text }, headers: responseHeaders };
|
|
99
131
|
}
|
|
100
132
|
}
|
|
101
|
-
async function mcpNotification(backendUrl, method, params) {
|
|
133
|
+
async function mcpNotification(backendUrl, method, params, extraHeaders) {
|
|
134
|
+
const headers = {
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
...extraHeaders
|
|
137
|
+
};
|
|
102
138
|
await request(backendUrl, {
|
|
103
139
|
method: "POST",
|
|
104
|
-
headers
|
|
140
|
+
headers,
|
|
105
141
|
body: JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} }),
|
|
106
142
|
signal: AbortSignal.timeout(5e3)
|
|
107
143
|
}).then((r) => r.body.text()).catch(() => {
|
|
@@ -121,7 +157,16 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
121
157
|
const backendUrl = url;
|
|
122
158
|
const tests = [];
|
|
123
159
|
const nextId = createIdCounter();
|
|
124
|
-
|
|
160
|
+
let sessionId = null;
|
|
161
|
+
let negotiatedProtocolVersion = null;
|
|
162
|
+
const userHeaders = options.headers || {};
|
|
163
|
+
function buildHeaders() {
|
|
164
|
+
const h = { ...userHeaders };
|
|
165
|
+
if (sessionId) h["mcp-session-id"] = sessionId;
|
|
166
|
+
if (negotiatedProtocolVersion) h["mcp-protocol-version"] = negotiatedProtocolVersion;
|
|
167
|
+
return h;
|
|
168
|
+
}
|
|
169
|
+
const rpc = (method, params) => mcpRequest(backendUrl, method, params, nextId, buildHeaders());
|
|
125
170
|
let serverInfo = {
|
|
126
171
|
protocolVersion: null,
|
|
127
172
|
name: null,
|
|
@@ -164,7 +209,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
164
209
|
await test("transport-post", "HTTP POST accepted", "transport", true, "basic/transports#streamable-http", async () => {
|
|
165
210
|
const res = await request(backendUrl, {
|
|
166
211
|
method: "POST",
|
|
167
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
|
|
212
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", ...userHeaders },
|
|
168
213
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
169
214
|
signal: AbortSignal.timeout(1e4)
|
|
170
215
|
});
|
|
@@ -176,7 +221,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
176
221
|
await test("transport-content-type", "Responds with JSON or SSE", "transport", true, "basic/transports#streamable-http", async () => {
|
|
177
222
|
const res = await request(backendUrl, {
|
|
178
223
|
method: "POST",
|
|
179
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
|
|
224
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", ...userHeaders },
|
|
180
225
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
181
226
|
signal: AbortSignal.timeout(1e4)
|
|
182
227
|
});
|
|
@@ -199,13 +244,16 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
199
244
|
serverInfo.name = result.serverInfo?.name || null;
|
|
200
245
|
serverInfo.version = result.serverInfo?.version || null;
|
|
201
246
|
serverInfo.capabilities = result.capabilities || {};
|
|
247
|
+
const sid = initRes.headers["mcp-session-id"];
|
|
248
|
+
if (sid) sessionId = sid;
|
|
249
|
+
if (result.protocolVersion) negotiatedProtocolVersion = result.protocolVersion;
|
|
202
250
|
return { passed: !!result.protocolVersion, details: `Protocol: ${result.protocolVersion || "missing"}` };
|
|
203
251
|
});
|
|
204
252
|
await test("lifecycle-proto-version", "Returns valid protocol version", "lifecycle", true, "basic/lifecycle#version-negotiation", async () => {
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
207
|
-
const valid = /^\d{4}-\d{2}-\d{2}$/.test(
|
|
208
|
-
return { passed: valid, details: `Version: ${
|
|
253
|
+
const version2 = initRes?.body?.result?.protocolVersion;
|
|
254
|
+
if (!version2) return { passed: false, details: "No protocolVersion" };
|
|
255
|
+
const valid = /^\d{4}-\d{2}-\d{2}$/.test(version2);
|
|
256
|
+
return { passed: valid, details: `Version: ${version2}` };
|
|
209
257
|
});
|
|
210
258
|
await test("lifecycle-server-info", "Includes serverInfo", "lifecycle", false, "basic/lifecycle#initialization", async () => {
|
|
211
259
|
const info = initRes?.body?.result?.serverInfo;
|
|
@@ -222,7 +270,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
222
270
|
const valid = body?.jsonrpc === "2.0" && body?.id !== void 0 && (body?.result !== void 0 || body?.error !== void 0);
|
|
223
271
|
return { passed: valid, details: valid ? "Valid JSON-RPC 2.0 response" : `Missing fields: jsonrpc=${body?.jsonrpc}, id=${body?.id}` };
|
|
224
272
|
});
|
|
225
|
-
await mcpNotification(backendUrl, "notifications/initialized");
|
|
273
|
+
await mcpNotification(backendUrl, "notifications/initialized", void 0, buildHeaders());
|
|
226
274
|
await test("lifecycle-ping", "Responds to ping", "lifecycle", true, "basic/utilities#ping", async () => {
|
|
227
275
|
const res = await rpc("ping");
|
|
228
276
|
const body = res.body;
|
|
@@ -231,17 +279,18 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
231
279
|
return { passed: false, details: "No result in ping response" };
|
|
232
280
|
});
|
|
233
281
|
const hasTools = !!serverInfo.capabilities.tools;
|
|
282
|
+
let cachedToolsList = null;
|
|
234
283
|
await test("tools-list", "tools/list returns valid response", "tools", hasTools, "server/tools#listing-tools", async () => {
|
|
235
284
|
const res = await rpc("tools/list");
|
|
236
285
|
const tools = res.body?.result?.tools;
|
|
237
286
|
if (!Array.isArray(tools)) return { passed: false, details: "No tools array in result" };
|
|
287
|
+
cachedToolsList = tools;
|
|
238
288
|
toolCount = tools.length;
|
|
239
289
|
toolNames = tools.map((t) => t.name).filter(Boolean);
|
|
240
290
|
return { passed: true, details: `${toolCount} tool(s): ${toolNames.slice(0, 5).join(", ")}${toolCount > 5 ? "..." : ""}` };
|
|
241
291
|
});
|
|
242
292
|
await test("tools-schema", "All tools have name and inputSchema", "schema", hasTools, "server/tools#data-types", async () => {
|
|
243
|
-
const
|
|
244
|
-
const tools = res.body?.result?.tools || [];
|
|
293
|
+
const tools = cachedToolsList ?? (await rpc("tools/list")).body?.result?.tools ?? [];
|
|
245
294
|
const issues = [];
|
|
246
295
|
const warnings = [];
|
|
247
296
|
for (const tool of tools) {
|
|
@@ -297,16 +346,17 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
297
346
|
}
|
|
298
347
|
const hasResources = !!serverInfo.capabilities.resources;
|
|
299
348
|
if (hasResources) {
|
|
349
|
+
let cachedResourcesList = null;
|
|
300
350
|
await test("resources-list", "resources/list returns valid response", "resources", true, "server/resources#listing-resources", async () => {
|
|
301
351
|
const res = await rpc("resources/list");
|
|
302
352
|
const resources = res.body?.result?.resources;
|
|
303
353
|
if (!Array.isArray(resources)) return { passed: false, details: "No resources array" };
|
|
354
|
+
cachedResourcesList = resources;
|
|
304
355
|
resourceCount = resources.length;
|
|
305
356
|
return { passed: true, details: `${resourceCount} resource(s)` };
|
|
306
357
|
});
|
|
307
358
|
await test("resources-schema", "Resources have uri and name", "schema", true, "server/resources#data-types", async () => {
|
|
308
|
-
const
|
|
309
|
-
const resources = res.body?.result?.resources || [];
|
|
359
|
+
const resources = cachedResourcesList ?? (await rpc("resources/list")).body?.result?.resources ?? [];
|
|
310
360
|
const issues = [];
|
|
311
361
|
for (const r of resources) {
|
|
312
362
|
if (!r.uri) issues.push("Resource missing uri");
|
|
@@ -359,17 +409,18 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
359
409
|
const hasPrompts = !!serverInfo.capabilities.prompts;
|
|
360
410
|
if (hasPrompts) {
|
|
361
411
|
let promptNames = [];
|
|
412
|
+
let cachedPromptsList = null;
|
|
362
413
|
await test("prompts-list", "prompts/list returns valid response", "prompts", true, "server/prompts#listing-prompts", async () => {
|
|
363
414
|
const res = await rpc("prompts/list");
|
|
364
415
|
const prompts = res.body?.result?.prompts;
|
|
365
416
|
if (!Array.isArray(prompts)) return { passed: false, details: "No prompts array" };
|
|
417
|
+
cachedPromptsList = prompts;
|
|
366
418
|
promptCount = prompts.length;
|
|
367
419
|
promptNames = prompts.map((p) => p.name).filter(Boolean);
|
|
368
420
|
return { passed: true, details: `${promptCount} prompt(s): ${promptNames.slice(0, 5).join(", ")}${promptCount > 5 ? "..." : ""}` };
|
|
369
421
|
});
|
|
370
422
|
await test("prompts-schema", "Prompts have name field", "schema", true, "server/prompts#data-types", async () => {
|
|
371
|
-
const
|
|
372
|
-
const prompts = res.body?.result?.prompts || [];
|
|
423
|
+
const prompts = cachedPromptsList ?? (await rpc("prompts/list")).body?.result?.prompts ?? [];
|
|
373
424
|
const issues = [];
|
|
374
425
|
for (const p of prompts) {
|
|
375
426
|
if (!p.name) issues.push("Prompt missing name");
|
|
@@ -418,7 +469,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
418
469
|
await test("error-invalid-jsonrpc", "Handles malformed JSON-RPC", "errors", true, "basic", async () => {
|
|
419
470
|
const res = await request(backendUrl, {
|
|
420
471
|
method: "POST",
|
|
421
|
-
headers: { "Content-Type": "application/json" },
|
|
472
|
+
headers: { "Content-Type": "application/json", ...buildHeaders() },
|
|
422
473
|
body: JSON.stringify({ not: "a valid jsonrpc message" }),
|
|
423
474
|
signal: AbortSignal.timeout(1e4)
|
|
424
475
|
});
|
|
@@ -437,7 +488,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
437
488
|
await test("error-invalid-json", "Handles invalid JSON body", "errors", false, "basic", async () => {
|
|
438
489
|
const res = await request(backendUrl, {
|
|
439
490
|
method: "POST",
|
|
440
|
-
headers: { "Content-Type": "application/json" },
|
|
491
|
+
headers: { "Content-Type": "application/json", ...buildHeaders() },
|
|
441
492
|
body: "{this is not valid json!!!",
|
|
442
493
|
signal: AbortSignal.timeout(1e4)
|
|
443
494
|
});
|
|
@@ -578,16 +629,30 @@ function formatJson(report) {
|
|
|
578
629
|
}
|
|
579
630
|
|
|
580
631
|
// src/index.ts
|
|
632
|
+
var require2 = createRequire(import.meta.url);
|
|
633
|
+
var { version } = require2("../package.json");
|
|
634
|
+
function parseHeaderArg(value, prev) {
|
|
635
|
+
const idx = value.indexOf(":");
|
|
636
|
+
if (idx === -1) {
|
|
637
|
+
throw new Error(`Invalid header format: "${value}" (expected "Key: Value")`);
|
|
638
|
+
}
|
|
639
|
+
const key = value.slice(0, idx).trim();
|
|
640
|
+
const val = value.slice(idx + 1).trim();
|
|
641
|
+
prev[key] = val;
|
|
642
|
+
return prev;
|
|
643
|
+
}
|
|
581
644
|
var program = new Command();
|
|
582
|
-
program.name("mcp-compliance").description("Test MCP servers for spec compliance").version(
|
|
583
|
-
program.command("test").description("Run the full compliance test suite against an MCP server").argument("<url>", "MCP server URL to test").option("--format <format>", "Output format: terminal or json", "terminal").option("--strict", "Exit with code 1 on any required test failure (for CI)").action(async (url, opts) => {
|
|
645
|
+
program.name("mcp-compliance").description("Test MCP servers for spec compliance").version(version);
|
|
646
|
+
program.command("test").description("Run the full compliance test suite against an MCP server").argument("<url>", "MCP server URL to test").option("--format <format>", "Output format: terminal or json", "terminal").option("--strict", "Exit with code 1 on any required test failure (for CI)").option("-H, --header <header>", 'Add header to all requests (format: "Key: Value", repeatable)', parseHeaderArg, {}).option("--auth <token>", 'Shorthand for -H "Authorization: <token>"').action(async (url, opts) => {
|
|
584
647
|
try {
|
|
648
|
+
const headers = { ...opts.header };
|
|
649
|
+
if (opts.auth) headers["Authorization"] = opts.auth;
|
|
585
650
|
if (opts.format === "terminal") {
|
|
586
651
|
console.log(chalk2.dim(`
|
|
587
652
|
Testing ${url}...
|
|
588
653
|
`));
|
|
589
654
|
}
|
|
590
|
-
const report = await runComplianceSuite(url);
|
|
655
|
+
const report = await runComplianceSuite(url, { headers });
|
|
591
656
|
if (opts.format === "json") {
|
|
592
657
|
console.log(formatJson(report));
|
|
593
658
|
} else {
|
|
@@ -607,12 +672,14 @@ Error: ${err.message}
|
|
|
607
672
|
process.exit(1);
|
|
608
673
|
}
|
|
609
674
|
});
|
|
610
|
-
program.command("badge").description("Run tests and output just the badge markdown embed code").argument("<url>", "MCP server URL to test").action(async (url) => {
|
|
675
|
+
program.command("badge").description("Run tests and output just the badge markdown embed code").argument("<url>", "MCP server URL to test").option("-H, --header <header>", 'Add header to all requests (format: "Key: Value", repeatable)', parseHeaderArg, {}).option("--auth <token>", 'Shorthand for -H "Authorization: <token>"').action(async (url, opts) => {
|
|
611
676
|
try {
|
|
677
|
+
const headers = { ...opts.header };
|
|
678
|
+
if (opts.auth) headers["Authorization"] = opts.auth;
|
|
612
679
|
console.log(chalk2.dim(`
|
|
613
680
|
Testing ${url}...
|
|
614
681
|
`));
|
|
615
|
-
const report = await runComplianceSuite(url);
|
|
682
|
+
const report = await runComplianceSuite(url, { headers });
|
|
616
683
|
console.log(`Grade: ${report.grade} (${report.score}%)
|
|
617
684
|
`);
|
|
618
685
|
console.log(report.badge.markdown);
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TEST_DEFINITIONS,
|
|
3
3
|
runComplianceSuite
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-OOJ4PMF7.js";
|
|
5
5
|
|
|
6
6
|
// src/mcp/server.ts
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
var require2 = createRequire(import.meta.url);
|
|
12
|
+
var { version } = require2("../../package.json");
|
|
10
13
|
var server = new McpServer({
|
|
11
14
|
name: "mcp-compliance",
|
|
12
|
-
version
|
|
15
|
+
version
|
|
13
16
|
});
|
|
14
17
|
server.tool(
|
|
15
18
|
"mcp_compliance_test",
|
package/dist/runner.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ declare function generateBadge(url: string): {
|
|
|
90
90
|
interface RunOptions {
|
|
91
91
|
/** Optional callback for progress updates */
|
|
92
92
|
onProgress?: (testId: string, passed: boolean, details: string) => void;
|
|
93
|
+
/** Extra headers to include on all requests */
|
|
94
|
+
headers?: Record<string, string>;
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
95
97
|
* Run the full MCP compliance test suite against a URL.
|
package/dist/runner.js
CHANGED