@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 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-server"]
137
+ "args": ["-y", "@yawlabs/mcp-compliance"],
138
+ "env": {},
139
+ "args_extra": ["mcp"]
123
140
  }
124
141
  }
125
142
  }
126
143
  ```
127
144
 
128
- Or if installed globally:
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 hostname = parsed.hostname;
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: { "Content-Type": "application/json" },
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
- const rpc = (method, params) => mcpRequest(backendUrl, method, params, nextId);
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 res = await rpc("tools/list");
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 res = await rpc("resources/list");
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 res = await rpc("prompts/list");
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 hostname = parsed.hostname;
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: { "Content-Type": "application/json" },
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
- const rpc = (method, params) => mcpRequest(backendUrl, method, params, nextId);
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 version = initRes?.body?.result?.protocolVersion;
206
- if (!version) return { passed: false, details: "No protocolVersion" };
207
- const valid = /^\d{4}-\d{2}-\d{2}$/.test(version);
208
- return { passed: valid, details: `Version: ${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 res = await rpc("tools/list");
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 res = await rpc("resources/list");
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 res = await rpc("prompts/list");
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("0.1.0");
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);
@@ -1,15 +1,18 @@
1
1
  import {
2
2
  TEST_DEFINITIONS,
3
3
  runComplianceSuite
4
- } from "../chunk-4AQGMM2X.js";
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: "0.1.0"
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
@@ -4,7 +4,7 @@ import {
4
4
  computeScore,
5
5
  generateBadge,
6
6
  runComplianceSuite
7
- } from "./chunk-4AQGMM2X.js";
7
+ } from "./chunk-OOJ4PMF7.js";
8
8
  export {
9
9
  TEST_DEFINITIONS,
10
10
  computeGrade,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp-compliance",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool and MCP server that tests MCP servers for spec compliance",
5
5
  "license": "MIT",
6
6
  "author": "Yaw Labs (https://yaw.sh)",