@yawlabs/mcp-compliance 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,627 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk2 from "chalk";
6
+
7
+ // src/runner.ts
8
+ import { request } from "undici";
9
+
10
+ // src/grader.ts
11
+ function computeGrade(score) {
12
+ if (score >= 90) return "A";
13
+ if (score >= 75) return "B";
14
+ if (score >= 60) return "C";
15
+ if (score >= 40) return "D";
16
+ return "F";
17
+ }
18
+ function computeScore(tests) {
19
+ const total = tests.length;
20
+ const passed = tests.filter((t) => t.passed).length;
21
+ const failed = total - passed;
22
+ const requiredTests = tests.filter((t) => t.required);
23
+ const requiredPassed = requiredTests.filter((t) => t.passed).length;
24
+ const requiredScore = requiredTests.length > 0 ? requiredPassed / requiredTests.length * 70 : 70;
25
+ const optionalTests = tests.filter((t) => !t.required);
26
+ const optionalPassed = optionalTests.filter((t) => t.passed).length;
27
+ const optionalScore = optionalTests.length > 0 ? optionalPassed / optionalTests.length * 30 : 30;
28
+ const score = Math.round(requiredScore + optionalScore);
29
+ const overall = requiredPassed === requiredTests.length ? passed === total ? "pass" : "partial" : "fail";
30
+ const categories = {};
31
+ for (const t of tests) {
32
+ if (!categories[t.category]) categories[t.category] = { passed: 0, total: 0 };
33
+ categories[t.category].total++;
34
+ if (t.passed) categories[t.category].passed++;
35
+ }
36
+ return {
37
+ score,
38
+ grade: computeGrade(score),
39
+ overall,
40
+ summary: { total, passed, failed, required: requiredTests.length, requiredPassed },
41
+ categories
42
+ };
43
+ }
44
+
45
+ // src/badge.ts
46
+ function generateBadge(url) {
47
+ let parsed;
48
+ try {
49
+ parsed = new URL(url);
50
+ } catch {
51
+ parsed = new URL("https://unknown");
52
+ }
53
+ const hostname = parsed.hostname;
54
+ const encoded = encodeURIComponent(hostname);
55
+ const imageUrl = `https://mcp.hosting/api/compliance/${encoded}/badge`;
56
+ const reportUrl = `https://mcp.hosting/compliance/${encoded}`;
57
+ return {
58
+ imageUrl,
59
+ reportUrl,
60
+ markdown: `[![MCP Compliant](${imageUrl})](${reportUrl})`,
61
+ html: `<a href="${reportUrl}"><img src="${imageUrl}" alt="MCP Compliant"></a>`
62
+ };
63
+ }
64
+
65
+ // src/runner.ts
66
+ var SPEC_VERSION = "2025-11-25";
67
+ var SPEC_BASE = `https://modelcontextprotocol.io/specification/${SPEC_VERSION}`;
68
+ function createIdCounter() {
69
+ let id = 0;
70
+ return () => ++id;
71
+ }
72
+ var _defaultNextId = createIdCounter();
73
+ async function mcpRequest(backendUrl, method, params, nextId = _defaultNextId) {
74
+ const id = nextId();
75
+ const body = JSON.stringify({
76
+ jsonrpc: "2.0",
77
+ id,
78
+ method,
79
+ params: params || {}
80
+ });
81
+ const res = await request(backendUrl, {
82
+ method: "POST",
83
+ headers: {
84
+ "Content-Type": "application/json",
85
+ "Accept": "application/json, text/event-stream"
86
+ },
87
+ body,
88
+ signal: AbortSignal.timeout(15e3)
89
+ });
90
+ const text = await res.body.text();
91
+ const responseHeaders = {};
92
+ for (const [k, v] of Object.entries(res.headers)) {
93
+ if (typeof v === "string") responseHeaders[k] = v;
94
+ }
95
+ try {
96
+ return { statusCode: res.statusCode, body: JSON.parse(text), headers: responseHeaders };
97
+ } catch {
98
+ return { statusCode: res.statusCode, body: { _raw: text }, headers: responseHeaders };
99
+ }
100
+ }
101
+ async function mcpNotification(backendUrl, method, params) {
102
+ await request(backendUrl, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} }),
106
+ signal: AbortSignal.timeout(5e3)
107
+ }).then((r) => r.body.text()).catch(() => {
108
+ });
109
+ }
110
+ async function runComplianceSuite(url, options = {}) {
111
+ let parsed;
112
+ try {
113
+ parsed = new URL(url);
114
+ if (!["http:", "https:"].includes(parsed.protocol)) {
115
+ throw new Error("Only HTTP and HTTPS URLs are supported");
116
+ }
117
+ } catch (e) {
118
+ if (e.message.includes("Only HTTP")) throw e;
119
+ throw new Error(`Invalid URL: ${url}`);
120
+ }
121
+ const backendUrl = url;
122
+ const tests = [];
123
+ const nextId = createIdCounter();
124
+ const rpc = (method, params) => mcpRequest(backendUrl, method, params, nextId);
125
+ let serverInfo = {
126
+ protocolVersion: null,
127
+ name: null,
128
+ version: null,
129
+ capabilities: {}
130
+ };
131
+ let toolCount = 0;
132
+ let toolNames = [];
133
+ let resourceCount = 0;
134
+ let promptCount = 0;
135
+ async function test(id, name, category, required, specRef, fn) {
136
+ const start = Date.now();
137
+ try {
138
+ const result = await fn();
139
+ tests.push({
140
+ id,
141
+ name,
142
+ category,
143
+ required,
144
+ passed: result.passed,
145
+ details: result.details,
146
+ durationMs: Date.now() - start,
147
+ specRef: `${SPEC_BASE}/${specRef}`
148
+ });
149
+ options.onProgress?.(id, result.passed, result.details);
150
+ } catch (err) {
151
+ tests.push({
152
+ id,
153
+ name,
154
+ category,
155
+ required,
156
+ passed: false,
157
+ details: `Error: ${err.message}`,
158
+ durationMs: Date.now() - start,
159
+ specRef: `${SPEC_BASE}/${specRef}`
160
+ });
161
+ options.onProgress?.(id, false, `Error: ${err.message}`);
162
+ }
163
+ }
164
+ await test("transport-post", "HTTP POST accepted", "transport", true, "basic/transports#streamable-http", async () => {
165
+ const res = await request(backendUrl, {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
168
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
169
+ signal: AbortSignal.timeout(1e4)
170
+ });
171
+ await res.body.text();
172
+ const passed = res.statusCode >= 200 && res.statusCode < 300;
173
+ const note = res.statusCode === 401 || res.statusCode === 403 ? " (auth required)" : "";
174
+ return { passed, details: `HTTP ${res.statusCode}${note}` };
175
+ });
176
+ await test("transport-content-type", "Responds with JSON or SSE", "transport", true, "basic/transports#streamable-http", async () => {
177
+ const res = await request(backendUrl, {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
180
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
181
+ signal: AbortSignal.timeout(1e4)
182
+ });
183
+ await res.body.text();
184
+ const rawCt = res.headers["content-type"];
185
+ const ct = (Array.isArray(rawCt) ? rawCt[0] : rawCt || "").toLowerCase();
186
+ const valid = ct.includes("application/json") || ct.includes("text/event-stream");
187
+ return { passed: valid, details: `Content-Type: ${ct}` };
188
+ });
189
+ let initRes = null;
190
+ await test("lifecycle-init", "Initialize handshake", "lifecycle", true, "basic/lifecycle#initialization", async () => {
191
+ initRes = await rpc("initialize", {
192
+ protocolVersion: SPEC_VERSION,
193
+ capabilities: { roots: { listChanged: true }, sampling: {} },
194
+ clientInfo: { name: "mcp-compliance", version: "1.0.0" }
195
+ });
196
+ const result = initRes.body?.result;
197
+ if (!result) return { passed: false, details: "No result in response" };
198
+ serverInfo.protocolVersion = result.protocolVersion || null;
199
+ serverInfo.name = result.serverInfo?.name || null;
200
+ serverInfo.version = result.serverInfo?.version || null;
201
+ serverInfo.capabilities = result.capabilities || {};
202
+ return { passed: !!result.protocolVersion, details: `Protocol: ${result.protocolVersion || "missing"}` };
203
+ });
204
+ 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}` };
209
+ });
210
+ await test("lifecycle-server-info", "Includes serverInfo", "lifecycle", false, "basic/lifecycle#initialization", async () => {
211
+ const info = initRes?.body?.result?.serverInfo;
212
+ return { passed: !!info?.name, details: info ? `${info.name} v${info.version || "?"}` : "Missing serverInfo" };
213
+ });
214
+ await test("lifecycle-capabilities", "Returns capabilities object", "lifecycle", true, "basic/lifecycle#capability-negotiation", async () => {
215
+ const caps = initRes?.body?.result?.capabilities;
216
+ if (!caps || typeof caps !== "object") return { passed: false, details: "No capabilities object in response" };
217
+ const declared = Object.keys(caps).filter((k) => caps[k] !== void 0);
218
+ return { passed: true, details: declared.length > 0 ? `Capabilities: ${declared.join(", ")}` : "Empty capabilities (valid)" };
219
+ });
220
+ await test("lifecycle-jsonrpc", "Response is valid JSON-RPC 2.0", "lifecycle", true, "basic", async () => {
221
+ const body = initRes?.body;
222
+ const valid = body?.jsonrpc === "2.0" && body?.id !== void 0 && (body?.result !== void 0 || body?.error !== void 0);
223
+ return { passed: valid, details: valid ? "Valid JSON-RPC 2.0 response" : `Missing fields: jsonrpc=${body?.jsonrpc}, id=${body?.id}` };
224
+ });
225
+ await mcpNotification(backendUrl, "notifications/initialized");
226
+ await test("lifecycle-ping", "Responds to ping", "lifecycle", true, "basic/utilities#ping", async () => {
227
+ const res = await rpc("ping");
228
+ const body = res.body;
229
+ if (body?.error) return { passed: false, details: `Error: ${body.error.message}` };
230
+ if (body?.result !== void 0) return { passed: true, details: "Ping responded successfully" };
231
+ return { passed: false, details: "No result in ping response" };
232
+ });
233
+ const hasTools = !!serverInfo.capabilities.tools;
234
+ await test("tools-list", "tools/list returns valid response", "tools", hasTools, "server/tools#listing-tools", async () => {
235
+ const res = await rpc("tools/list");
236
+ const tools = res.body?.result?.tools;
237
+ if (!Array.isArray(tools)) return { passed: false, details: "No tools array in result" };
238
+ toolCount = tools.length;
239
+ toolNames = tools.map((t) => t.name).filter(Boolean);
240
+ return { passed: true, details: `${toolCount} tool(s): ${toolNames.slice(0, 5).join(", ")}${toolCount > 5 ? "..." : ""}` };
241
+ });
242
+ 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 || [];
245
+ const issues = [];
246
+ const warnings = [];
247
+ for (const tool of tools) {
248
+ if (!tool.name) {
249
+ issues.push("Tool missing name");
250
+ continue;
251
+ }
252
+ if (tool.name.length > 128 || !/^[A-Za-z0-9_.\-]+$/.test(tool.name)) {
253
+ issues.push(`${tool.name}: name format invalid`);
254
+ }
255
+ if (!tool.description) warnings.push(`${tool.name}: missing description`);
256
+ if (!tool.inputSchema) {
257
+ issues.push(`${tool.name}: missing inputSchema (required)`);
258
+ } else if (typeof tool.inputSchema !== "object" || tool.inputSchema === null) {
259
+ issues.push(`${tool.name}: inputSchema must be a valid JSON Schema object`);
260
+ } else if (tool.inputSchema.type !== "object") {
261
+ issues.push(`${tool.name}: inputSchema.type must be "object" (got "${tool.inputSchema.type || "undefined"}")`);
262
+ }
263
+ }
264
+ const detail = issues.length === 0 ? warnings.length > 0 ? `Schemas valid. Warnings: ${warnings.join("; ")}` : "All tools have valid schemas" : issues.join("; ");
265
+ return { passed: issues.length === 0, details: detail };
266
+ });
267
+ if (toolNames.length > 0) {
268
+ await test("tools-call", "tools/call responds correctly", "tools", false, "server/tools#calling-tools", async () => {
269
+ const res = await rpc("tools/call", { name: toolNames[0], arguments: {} });
270
+ const result = res.body?.result;
271
+ const error = res.body?.error;
272
+ if (error) {
273
+ const code = error.code;
274
+ if (code === -32602 || code === -32600) {
275
+ return { passed: true, details: `Invalid params error (acceptable): code ${code}` };
276
+ }
277
+ return { passed: true, details: `Protocol error: code ${code} \u2014 ${error.message}` };
278
+ }
279
+ if (result?.content && Array.isArray(result.content)) {
280
+ const badItems = result.content.filter((c) => !c.type);
281
+ if (badItems.length > 0) return { passed: false, details: `${badItems.length} content item(s) missing 'type' field` };
282
+ return { passed: true, details: `Returned ${result.content.length} content item(s)` };
283
+ }
284
+ if (result?.isError && result?.content && Array.isArray(result.content)) {
285
+ return { passed: true, details: "Tool returned execution error with content (valid)" };
286
+ }
287
+ return { passed: false, details: "Response missing content array" };
288
+ });
289
+ await test("tools-call-unknown", "Returns error for unknown tool name", "errors", false, "server/tools#error-handling", async () => {
290
+ const res = await rpc("tools/call", { name: "__nonexistent_tool_compliance_test__", arguments: {} });
291
+ const error = res.body?.error;
292
+ const isError = res.body?.result?.isError;
293
+ if (error) return { passed: true, details: `Error code: ${error.code} \u2014 ${error.message}` };
294
+ if (isError) return { passed: true, details: "Tool execution error with isError=true (valid)" };
295
+ return { passed: false, details: "No error returned for nonexistent tool" };
296
+ });
297
+ }
298
+ const hasResources = !!serverInfo.capabilities.resources;
299
+ if (hasResources) {
300
+ await test("resources-list", "resources/list returns valid response", "resources", true, "server/resources#listing-resources", async () => {
301
+ const res = await rpc("resources/list");
302
+ const resources = res.body?.result?.resources;
303
+ if (!Array.isArray(resources)) return { passed: false, details: "No resources array" };
304
+ resourceCount = resources.length;
305
+ return { passed: true, details: `${resourceCount} resource(s)` };
306
+ });
307
+ 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 || [];
310
+ const issues = [];
311
+ for (const r of resources) {
312
+ if (!r.uri) issues.push("Resource missing uri");
313
+ else {
314
+ try {
315
+ new URL(r.uri);
316
+ } catch {
317
+ issues.push(`${r.uri}: invalid URI format`);
318
+ }
319
+ }
320
+ if (!r.name) issues.push(`${r.uri || "?"}: missing name`);
321
+ }
322
+ return { passed: issues.length === 0, details: issues.length === 0 ? "All resources valid" : issues.join("; ") };
323
+ });
324
+ if (resourceCount > 0) {
325
+ await test("resources-read", "resources/read returns content", "resources", false, "server/resources#reading-resources", async () => {
326
+ const listRes = await rpc("resources/list");
327
+ const firstUri = listRes.body?.result?.resources?.[0]?.uri;
328
+ if (!firstUri) return { passed: false, details: "No resource URI to test" };
329
+ const readRes = await rpc("resources/read", { uri: firstUri });
330
+ const contents = readRes.body?.result?.contents;
331
+ if (!Array.isArray(contents)) return { passed: false, details: "No contents array" };
332
+ const issues = [];
333
+ for (const c of contents) {
334
+ if (!c.uri) issues.push("Content item missing uri");
335
+ if (!c.text && !c.blob) issues.push(`Content item for ${c.uri || "?"} missing both text and blob`);
336
+ }
337
+ if (issues.length > 0) return { passed: false, details: issues.join("; ") };
338
+ return { passed: true, details: `Read ${contents.length} content item(s) from ${firstUri}` };
339
+ });
340
+ }
341
+ await test("resources-templates", "resources/templates/list returns valid response", "resources", false, "server/resources#resource-templates", async () => {
342
+ const res = await rpc("resources/templates/list");
343
+ const error = res.body?.error;
344
+ if (error) {
345
+ if (error.code === -32601) return { passed: true, details: "Method not supported (acceptable)" };
346
+ return { passed: false, details: `Error: ${error.message}` };
347
+ }
348
+ const templates = res.body?.result?.resourceTemplates;
349
+ if (!Array.isArray(templates)) return { passed: false, details: "No resourceTemplates array" };
350
+ const issues = [];
351
+ for (const t of templates) {
352
+ if (!t.uriTemplate) issues.push("Template missing uriTemplate");
353
+ if (!t.name) issues.push(`${t.uriTemplate || "?"}: missing name`);
354
+ }
355
+ if (issues.length > 0) return { passed: false, details: issues.join("; ") };
356
+ return { passed: true, details: `${templates.length} resource template(s)` };
357
+ });
358
+ }
359
+ const hasPrompts = !!serverInfo.capabilities.prompts;
360
+ if (hasPrompts) {
361
+ let promptNames = [];
362
+ await test("prompts-list", "prompts/list returns valid response", "prompts", true, "server/prompts#listing-prompts", async () => {
363
+ const res = await rpc("prompts/list");
364
+ const prompts = res.body?.result?.prompts;
365
+ if (!Array.isArray(prompts)) return { passed: false, details: "No prompts array" };
366
+ promptCount = prompts.length;
367
+ promptNames = prompts.map((p) => p.name).filter(Boolean);
368
+ return { passed: true, details: `${promptCount} prompt(s): ${promptNames.slice(0, 5).join(", ")}${promptCount > 5 ? "..." : ""}` };
369
+ });
370
+ 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 || [];
373
+ const issues = [];
374
+ for (const p of prompts) {
375
+ if (!p.name) issues.push("Prompt missing name");
376
+ if (p.arguments && !Array.isArray(p.arguments)) issues.push(`${p.name || "?"}: arguments must be an array`);
377
+ if (Array.isArray(p.arguments)) {
378
+ for (const arg of p.arguments) {
379
+ if (!arg.name) issues.push(`${p.name}: argument missing name`);
380
+ }
381
+ }
382
+ }
383
+ return { passed: issues.length === 0, details: issues.length === 0 ? "All prompts valid" : issues.join("; ") };
384
+ });
385
+ if (promptNames.length > 0) {
386
+ await test("prompts-get", "prompts/get returns valid messages", "prompts", false, "server/prompts#getting-a-prompt", async () => {
387
+ const res = await rpc("prompts/get", { name: promptNames[0] });
388
+ const error = res.body?.error;
389
+ if (error) return { passed: true, details: `Error (may need arguments): code ${error.code}` };
390
+ const messages = res.body?.result?.messages;
391
+ if (!Array.isArray(messages)) return { passed: false, details: "No messages array in result" };
392
+ const issues = [];
393
+ for (const msg of messages) {
394
+ if (!msg.role || !["user", "assistant"].includes(msg.role)) issues.push(`Invalid role: ${msg.role}`);
395
+ if (!msg.content) issues.push("Message missing content");
396
+ }
397
+ if (issues.length > 0) return { passed: false, details: issues.join("; ") };
398
+ return { passed: true, details: `${messages.length} message(s) from ${promptNames[0]}` };
399
+ });
400
+ }
401
+ }
402
+ await test("error-unknown-method", "Returns JSON-RPC error for unknown method", "errors", true, "basic", async () => {
403
+ const res = await rpc("nonexistent/method");
404
+ const error = res.body?.error;
405
+ if (!error) return { passed: false, details: "No JSON-RPC error returned for unknown method" };
406
+ const correctCode = error.code === -32601;
407
+ return {
408
+ passed: true,
409
+ details: `Error code: ${error.code}${correctCode ? " (correct: Method not found)" : " (expected -32601)"} \u2014 ${error.message}`
410
+ };
411
+ });
412
+ await test("error-method-code", "Uses correct JSON-RPC error code for unknown method", "errors", false, "basic", async () => {
413
+ const res = await rpc("nonexistent/method");
414
+ const error = res.body?.error;
415
+ if (!error) return { passed: false, details: "No error returned" };
416
+ return { passed: error.code === -32601, details: `Expected -32601, got ${error.code}` };
417
+ });
418
+ await test("error-invalid-jsonrpc", "Handles malformed JSON-RPC", "errors", true, "basic", async () => {
419
+ const res = await request(backendUrl, {
420
+ method: "POST",
421
+ headers: { "Content-Type": "application/json" },
422
+ body: JSON.stringify({ not: "a valid jsonrpc message" }),
423
+ signal: AbortSignal.timeout(1e4)
424
+ });
425
+ const text = await res.body.text();
426
+ try {
427
+ const body = JSON.parse(text);
428
+ if (body?.error) {
429
+ const correctCode = body.error.code === -32600;
430
+ return { passed: true, details: `Error code: ${body.error.code}${correctCode ? " (correct: Invalid Request)" : ""} \u2014 ${body.error.message}` };
431
+ }
432
+ } catch {
433
+ }
434
+ if (res.statusCode >= 400 && res.statusCode < 500) return { passed: true, details: `HTTP ${res.statusCode} (acceptable)` };
435
+ return { passed: false, details: `HTTP ${res.statusCode} \u2014 expected JSON-RPC error or 4xx status` };
436
+ });
437
+ await test("error-invalid-json", "Handles invalid JSON body", "errors", false, "basic", async () => {
438
+ const res = await request(backendUrl, {
439
+ method: "POST",
440
+ headers: { "Content-Type": "application/json" },
441
+ body: "{this is not valid json!!!",
442
+ signal: AbortSignal.timeout(1e4)
443
+ });
444
+ const text = await res.body.text();
445
+ try {
446
+ const body = JSON.parse(text);
447
+ if (body?.error) return { passed: true, details: `Error code: ${body.error.code} \u2014 ${body.error.message}` };
448
+ } catch {
449
+ }
450
+ if (res.statusCode >= 400 && res.statusCode < 500) return { passed: true, details: `HTTP ${res.statusCode} (acceptable)` };
451
+ return { passed: false, details: `HTTP ${res.statusCode} \u2014 expected parse error or 4xx status` };
452
+ });
453
+ await test("error-missing-params", "Returns error for tools/call without name", "errors", false, "server/tools#error-handling", async () => {
454
+ const res = await rpc("tools/call", {});
455
+ const error = res.body?.error;
456
+ const isError = res.body?.result?.isError;
457
+ if (error) {
458
+ const correctCode = error.code === -32602;
459
+ return { passed: true, details: `Error code: ${error.code}${correctCode ? " (correct: Invalid params)" : ""} \u2014 ${error.message}` };
460
+ }
461
+ if (isError) return { passed: true, details: "Tool execution error (valid)" };
462
+ return { passed: false, details: "No error for tools/call without name" };
463
+ });
464
+ const { score, grade, overall, summary, categories } = computeScore(tests);
465
+ const badge = generateBadge(url);
466
+ return {
467
+ specVersion: SPEC_VERSION,
468
+ url,
469
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
470
+ score,
471
+ grade,
472
+ overall,
473
+ summary,
474
+ categories,
475
+ tests,
476
+ serverInfo,
477
+ toolCount,
478
+ toolNames,
479
+ resourceCount,
480
+ promptCount,
481
+ badge
482
+ };
483
+ }
484
+
485
+ // src/reporter.ts
486
+ import chalk from "chalk";
487
+ var CATEGORY_LABELS = {
488
+ transport: "Transport",
489
+ lifecycle: "Lifecycle",
490
+ tools: "Tools",
491
+ resources: "Resources",
492
+ prompts: "Prompts",
493
+ errors: "Error Handling",
494
+ schema: "Schema Validation"
495
+ };
496
+ var CATEGORY_ORDER = ["transport", "lifecycle", "tools", "resources", "prompts", "errors", "schema"];
497
+ function gradeColor(grade) {
498
+ switch (grade) {
499
+ case "A":
500
+ return chalk.green.bold(grade);
501
+ case "B":
502
+ return chalk.greenBright.bold(grade);
503
+ case "C":
504
+ return chalk.yellow.bold(grade);
505
+ case "D":
506
+ return chalk.rgb(255, 165, 0).bold(grade);
507
+ case "F":
508
+ return chalk.red.bold(grade);
509
+ }
510
+ }
511
+ function overallColor(overall) {
512
+ switch (overall) {
513
+ case "pass":
514
+ return chalk.green.bold("PASS");
515
+ case "partial":
516
+ return chalk.yellow.bold("PARTIAL");
517
+ case "fail":
518
+ return chalk.red.bold("FAIL");
519
+ default:
520
+ return overall;
521
+ }
522
+ }
523
+ function testLine(t) {
524
+ const icon = t.passed ? chalk.green(" PASS") : chalk.red(" FAIL");
525
+ const req = t.required ? chalk.dim(" (required)") : "";
526
+ const dur = chalk.dim(` ${t.durationMs}ms`);
527
+ return `${icon} ${t.name}${req}${dur}
528
+ ${chalk.dim(` ${t.details}`)}`;
529
+ }
530
+ function formatTerminal(report) {
531
+ const lines = [];
532
+ lines.push("");
533
+ lines.push(chalk.bold("MCP Compliance Report"));
534
+ lines.push(chalk.dim(`Spec: ${report.specVersion} | ${report.timestamp}`));
535
+ lines.push(chalk.dim(`URL: ${report.url}`));
536
+ if (report.serverInfo.name) {
537
+ lines.push(chalk.dim(`Server: ${report.serverInfo.name} v${report.serverInfo.version || "?"} (protocol ${report.serverInfo.protocolVersion || "?"})`));
538
+ }
539
+ lines.push("");
540
+ lines.push(` Grade: ${gradeColor(report.grade)} Score: ${chalk.bold(String(report.score))}% Overall: ${overallColor(report.overall)}`);
541
+ lines.push(` Tests: ${chalk.green(String(report.summary.passed))} passed / ${chalk.red(String(report.summary.failed))} failed / ${report.summary.total} total`);
542
+ lines.push(` Required: ${report.summary.requiredPassed}/${report.summary.required} passed`);
543
+ const grouped = {};
544
+ for (const t of report.tests) {
545
+ if (!grouped[t.category]) grouped[t.category] = [];
546
+ grouped[t.category].push(t);
547
+ }
548
+ for (const cat of CATEGORY_ORDER) {
549
+ const catTests = grouped[cat];
550
+ if (!catTests || catTests.length === 0) continue;
551
+ const catStats = report.categories[cat];
552
+ const label = CATEGORY_LABELS[cat] || cat;
553
+ const catColor = catStats && catStats.passed === catStats.total ? chalk.green : chalk.yellow;
554
+ lines.push("");
555
+ lines.push(catColor(` ${label} (${catStats?.passed || 0}/${catStats?.total || 0})`));
556
+ for (const t of catTests) {
557
+ lines.push(testLine(t));
558
+ }
559
+ }
560
+ if (report.toolCount > 0) {
561
+ lines.push("");
562
+ lines.push(chalk.dim(` Tools (${report.toolCount}): ${report.toolNames.slice(0, 10).join(", ")}${report.toolCount > 10 ? "..." : ""}`));
563
+ }
564
+ if (report.resourceCount > 0) {
565
+ lines.push(chalk.dim(` Resources: ${report.resourceCount}`));
566
+ }
567
+ if (report.promptCount > 0) {
568
+ lines.push(chalk.dim(` Prompts: ${report.promptCount}`));
569
+ }
570
+ lines.push("");
571
+ lines.push(chalk.dim(" Badge markdown:"));
572
+ lines.push(` ${report.badge.markdown}`);
573
+ lines.push("");
574
+ return lines.join("\n");
575
+ }
576
+ function formatJson(report) {
577
+ return JSON.stringify(report, null, 2);
578
+ }
579
+
580
+ // src/index.ts
581
+ 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) => {
584
+ try {
585
+ if (opts.format === "terminal") {
586
+ console.log(chalk2.dim(`
587
+ Testing ${url}...
588
+ `));
589
+ }
590
+ const report = await runComplianceSuite(url);
591
+ if (opts.format === "json") {
592
+ console.log(formatJson(report));
593
+ } else {
594
+ console.log(formatTerminal(report));
595
+ }
596
+ if (opts.strict && report.overall === "fail") {
597
+ process.exit(1);
598
+ }
599
+ } catch (err) {
600
+ if (opts.format === "json") {
601
+ console.error(JSON.stringify({ error: err.message }));
602
+ } else {
603
+ console.error(chalk2.red(`
604
+ Error: ${err.message}
605
+ `));
606
+ }
607
+ process.exit(1);
608
+ }
609
+ });
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) => {
611
+ try {
612
+ console.log(chalk2.dim(`
613
+ Testing ${url}...
614
+ `));
615
+ const report = await runComplianceSuite(url);
616
+ console.log(`Grade: ${report.grade} (${report.score}%)
617
+ `);
618
+ console.log(report.badge.markdown);
619
+ console.log("");
620
+ } catch (err) {
621
+ console.error(chalk2.red(`
622
+ Error: ${err.message}
623
+ `));
624
+ process.exit(1);
625
+ }
626
+ });
627
+ program.parse();
@@ -0,0 +1,2 @@
1
+
2
+ export { }