@yawlabs/mcp-compliance 0.7.0 → 0.8.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![GitHub stars](https://img.shields.io/github/stars/YawLabs/mcp-compliance)](https://github.com/YawLabs/mcp-compliance/stargazers)
6
6
  [![CI](https://github.com/YawLabs/mcp-compliance/actions/workflows/ci.yml/badge.svg)](https://github.com/YawLabs/mcp-compliance/actions/workflows/ci.yml)
7
7
 
8
- **Test any MCP server for spec compliance.** 78-test suite covering transport, lifecycle, tools, resources, prompts, error handling, schema validation, and security against the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25). CLI, MCP server, and programmatic API.
8
+ **Test any MCP server for spec compliance.** 81-test suite covering transport, lifecycle, tools, resources, prompts, error handling, schema validation, and security against the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25). CLI, MCP server, and programmatic API.
9
9
 
10
10
  Built and maintained by [Yaw Labs](https://yaw.sh).
11
11
 
@@ -15,7 +15,7 @@ MCP servers are multiplying fast — but most ship without compliance testing. B
15
15
 
16
16
  This tool solves that:
17
17
 
18
- - **78 tests across 8 categories** — transport, lifecycle, tools, resources, prompts, error handling, schema validation, and security. No gaps.
18
+ - **81 tests across 8 categories** — transport, lifecycle, tools, resources, prompts, error handling, schema validation, and security. No gaps.
19
19
  - **Capability-driven** — tests adapt to what the server declares. If it says it supports tools, tool tests become required. No false failures for features the server doesn't claim.
20
20
  - **Graded scoring** — A-F letter grade with a weighted score (required tests 70%, optional 30%). One number to communicate compliance.
21
21
  - **CI-ready** — `--strict` mode exits with code 1 on required test failures. Drop it into any pipeline.
@@ -100,7 +100,7 @@ mcp-compliance badge https://my-server.com/mcp
100
100
 
101
101
  Outputs the markdown embed for a compliance badge hosted at [mcp.hosting](https://mcp.hosting).
102
102
 
103
- ## What the 78 tests check
103
+ ## What the 81 tests check
104
104
 
105
105
  <details>
106
106
  <summary><strong>Transport (13 tests)</strong></summary>
@@ -122,7 +122,7 @@ Outputs the markdown embed for a compliance badge hosted at [mcp.hosting](https:
122
122
  </details>
123
123
 
124
124
  <details>
125
- <summary><strong>Lifecycle (15 tests)</strong></summary>
125
+ <summary><strong>Lifecycle (17 tests)</strong></summary>
126
126
 
127
127
  - **lifecycle-init** — Initialize handshake succeeds (required)
128
128
  - **lifecycle-proto-version** — Returns valid YYYY-MM-DD protocol version (required)
@@ -139,6 +139,8 @@ Outputs the markdown embed for a compliance badge hosted at [mcp.hosting](https:
139
139
  - **lifecycle-completions** — completion/complete accepted (required if completions capability declared)
140
140
  - **lifecycle-cancellation** — Handles cancellation notifications
141
141
  - **lifecycle-progress** — Handles progress notifications gracefully
142
+ - **lifecycle-list-changed** — Accepts listChanged notifications for declared capabilities
143
+ - **lifecycle-progress-token** — Supports progress tokens in requests via SSE
142
144
 
143
145
  </details>
144
146
 
@@ -201,9 +203,10 @@ Outputs the markdown embed for a compliance badge hosted at [mcp.hosting](https:
201
203
  </details>
202
204
 
203
205
  <details>
204
- <summary><strong>Security (22 tests)</strong></summary>
206
+ <summary><strong>Security (23 tests)</strong></summary>
205
207
 
206
208
  - **security-auth-required** — Rejects unauthenticated requests
209
+ - **security-www-authenticate** — 401 responses include WWW-Authenticate header
207
210
  - **security-auth-malformed** — Rejects malformed auth credentials
208
211
  - **security-tls-required** — Enforces HTTPS/TLS
209
212
  - **security-session-entropy** — Session IDs are high-entropy
@@ -318,7 +321,7 @@ Restart your MCP client and approve the server when prompted.
318
321
 
319
322
  ### Tools
320
323
 
321
- - **mcp_compliance_test** — Run the full 78-test suite against a URL. Supports auth, custom headers, timeout, retries, and category/test filtering. Returns grade, score, and detailed results.
324
+ - **mcp_compliance_test** — Run the full 81-test suite against a URL. Supports auth, custom headers, timeout, retries, and category/test filtering. Returns grade, score, and detailed results.
322
325
  - **mcp_compliance_badge** — Get the badge markdown/HTML for a server. Supports auth and custom headers.
323
326
  - **mcp_compliance_explain** — Explain what a specific test ID checks and why it matters.
324
327
 
@@ -345,7 +348,7 @@ const report2 = await runComplianceSuite('https://my-server.com/mcp', {
345
348
 
346
349
  The compliance testing methodology is published as an open specification:
347
350
 
348
- - **[MCP Compliance Testing Specification](./MCP_COMPLIANCE_SPEC.md)** — test execution model, scoring algorithm, all 78 test rules with pass/fail criteria (CC BY 4.0)
351
+ - **[MCP Compliance Testing Specification](./MCP_COMPLIANCE_SPEC.md)** — test execution model, scoring algorithm, all 81 test rules with pass/fail criteria (CC BY 4.0)
349
352
  - **[Machine-readable rule catalog](./mcp-compliance-rules.json)** — JSON Schema-compliant catalog for programmatic consumption
350
353
 
351
354
  These are complementary to (not competing with) the [official MCP specification](https://modelcontextprotocol.io/specification/2025-11-25). The MCP spec defines what servers must do; this spec defines how to verify compliance.
@@ -174,7 +174,7 @@ var TEST_DEFINITIONS = [
174
174
  description: "Sends a request with Accept: text/event-stream and checks that SSE responses include the event: message field. Per spec, servers MUST set event: message for JSON-RPC messages in SSE streams.",
175
175
  recommendation: 'Include "event: message" before each "data:" line in your SSE responses. This is required by the MCP spec for JSON-RPC messages sent over SSE.'
176
176
  },
177
- // ── Lifecycle (15 tests) ─────────────────────────────────────────
177
+ // ── Lifecycle (17 tests) ─────────────────────────────────────────
178
178
  {
179
179
  id: "lifecycle-init",
180
180
  name: "Initialize handshake",
@@ -310,6 +310,24 @@ var TEST_DEFINITIONS = [
310
310
  description: "Sends a notifications/progress to the server and verifies it does not error. Note: per spec, progress flows from server to client during long-running requests. This test validates the server handles unexpected notifications gracefully.",
311
311
  recommendation: "Accept unknown notifications without returning an error. The server should not crash or return a non-2xx status for notifications it does not recognize."
312
312
  },
313
+ {
314
+ id: "lifecycle-list-changed",
315
+ name: "Accepts listChanged notifications",
316
+ category: "lifecycle",
317
+ required: false,
318
+ specRef: "basic/lifecycle#capability-negotiation",
319
+ description: "Sends notifications/tools/list_changed, notifications/resources/list_changed, and notifications/prompts/list_changed for declared capabilities and verifies the server accepts them.",
320
+ recommendation: "Accept listChanged notifications gracefully. When received, re-fetch the relevant list to detect changes. These notifications signal that the client's cached list may be stale."
321
+ },
322
+ {
323
+ id: "lifecycle-progress-token",
324
+ name: "Supports progress tokens in requests",
325
+ category: "lifecycle",
326
+ required: false,
327
+ specRef: "basic/utilities#progress",
328
+ description: "Sends a tools/call request with _meta.progressToken and checks if the server sends progress notifications via SSE. Progress support is optional but recommended for long-running operations.",
329
+ recommendation: "When a request includes _meta.progressToken, send notifications/progress events via SSE to report progress. Include progressToken, progress (current), and optionally total fields."
330
+ },
313
331
  // ── Tools (4 tests) ──────────────────────────────────────────────
314
332
  {
315
333
  id: "tools-list",
@@ -567,7 +585,7 @@ var TEST_DEFINITIONS = [
567
585
  description: "Validates every resource has a valid URI (parseable as a URL) and a name field.",
568
586
  recommendation: "Ensure every resource has a valid, parseable URI and a name field. Add description and mimeType for better client integration."
569
587
  },
570
- // ── Security: Auth & Transport (9 tests) ─────────────────────────
588
+ // ── Security: Auth & Transport (10 tests) ────────────────────────
571
589
  {
572
590
  id: "security-auth-required",
573
591
  name: "Rejects unauthenticated requests",
@@ -577,6 +595,15 @@ var TEST_DEFINITIONS = [
577
595
  description: "Sends a request without an Authorization header and verifies the server returns HTTP 401. Servers exposed over the network should require authentication.",
578
596
  recommendation: "Implement authentication on your MCP endpoint. Return HTTP 401 Unauthorized for requests without valid credentials. Use OAuth 2.1 or Bearer tokens as recommended by the MCP spec."
579
597
  },
598
+ {
599
+ id: "security-www-authenticate",
600
+ name: "401 responses include WWW-Authenticate header",
601
+ category: "security",
602
+ required: false,
603
+ specRef: "basic/authorization",
604
+ description: "When the server returns HTTP 401, checks for a WWW-Authenticate header indicating the required authentication scheme. Per HTTP spec (RFC 9110), servers SHOULD include this header.",
605
+ recommendation: `Include a WWW-Authenticate header in 401 responses to indicate the required auth scheme (e.g., 'WWW-Authenticate: Bearer realm="mcp"').`
606
+ },
580
607
  {
581
608
  id: "security-auth-malformed",
582
609
  name: "Rejects malformed auth credentials",
@@ -1494,6 +1521,90 @@ async function runComplianceSuite(url, options = {}) {
1494
1521
  };
1495
1522
  }
1496
1523
  );
1524
+ await test(
1525
+ "lifecycle-list-changed",
1526
+ "Accepts listChanged notifications",
1527
+ "lifecycle",
1528
+ false,
1529
+ "basic/lifecycle#capability-negotiation",
1530
+ async () => {
1531
+ const notifications = [
1532
+ { method: "notifications/tools/list_changed", gate: hasTools },
1533
+ { method: "notifications/resources/list_changed", gate: hasResources },
1534
+ { method: "notifications/prompts/list_changed", gate: hasPrompts }
1535
+ ];
1536
+ const applicable = notifications.filter((n) => n.gate);
1537
+ if (applicable.length === 0) {
1538
+ return { passed: true, details: "No capabilities declared \u2014 listChanged notifications not applicable" };
1539
+ }
1540
+ const issues = [];
1541
+ for (const { method } of applicable) {
1542
+ try {
1543
+ const res = await mcpNotification(backendUrl, method, void 0, buildHeaders(), timeout);
1544
+ if (res.statusCode < 200 || res.statusCode >= 300) {
1545
+ issues.push(`${method}: HTTP ${res.statusCode}`);
1546
+ }
1547
+ } catch (err) {
1548
+ issues.push(`${method}: ${err instanceof Error ? err.message : "error"}`);
1549
+ }
1550
+ }
1551
+ if (issues.length > 0) return { passed: false, details: issues.join("; ") };
1552
+ return {
1553
+ passed: true,
1554
+ details: `${applicable.length} listChanged notification(s) accepted: ${applicable.map((n) => n.method).join(", ")}`
1555
+ };
1556
+ }
1557
+ );
1558
+ await test(
1559
+ "lifecycle-progress-token",
1560
+ "Supports progress tokens in requests",
1561
+ "lifecycle",
1562
+ false,
1563
+ "basic/utilities#progress",
1564
+ async () => {
1565
+ if (!hasTools || toolNames.length === 0) {
1566
+ return { passed: true, details: "No tools available for progress token test (skipped)" };
1567
+ }
1568
+ const progressToken = "compliance-progress-test";
1569
+ const reqBody = JSON.stringify({
1570
+ jsonrpc: "2.0",
1571
+ id: nextId(),
1572
+ method: "tools/call",
1573
+ params: {
1574
+ name: toolNames[0],
1575
+ arguments: {},
1576
+ _meta: { progressToken }
1577
+ }
1578
+ });
1579
+ try {
1580
+ const res = await request(backendUrl, {
1581
+ method: "POST",
1582
+ headers: {
1583
+ "Content-Type": "application/json",
1584
+ Accept: "text/event-stream",
1585
+ ...buildHeaders()
1586
+ },
1587
+ body: reqBody,
1588
+ signal: AbortSignal.timeout(timeout)
1589
+ });
1590
+ const text = await res.body.text();
1591
+ const rawCtProgress = res.headers["content-type"];
1592
+ const ct = (Array.isArray(rawCtProgress) ? rawCtProgress[0] : rawCtProgress || "").toLowerCase();
1593
+ if (ct.includes("text/event-stream") && text.includes("notifications/progress")) {
1594
+ return { passed: true, details: "Server sent progress notifications via SSE with progressToken" };
1595
+ }
1596
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1597
+ return {
1598
+ passed: true,
1599
+ details: "Server accepted request with progressToken (no progress events observed \u2014 optional)"
1600
+ };
1601
+ }
1602
+ return { passed: true, details: `HTTP ${res.statusCode} \u2014 request with progressToken accepted` };
1603
+ } catch {
1604
+ return { passed: true, details: "Request with progressToken handled (no progress events observed \u2014 optional)" };
1605
+ }
1606
+ }
1607
+ );
1497
1608
  await test(
1498
1609
  "transport-content-type-init",
1499
1610
  "Initialize response has valid content type",
@@ -2428,6 +2539,39 @@ async function runComplianceSuite(url, options = {}) {
2428
2539
  }
2429
2540
  }
2430
2541
  );
2542
+ await test(
2543
+ "security-www-authenticate",
2544
+ "401 responses include WWW-Authenticate header",
2545
+ "security",
2546
+ false,
2547
+ "basic/authorization",
2548
+ async () => {
2549
+ if (!hasAuth) {
2550
+ return { passed: true, details: "Skipped: server does not require auth" };
2551
+ }
2552
+ const noAuthHeaders = {};
2553
+ if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
2554
+ try {
2555
+ const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout);
2556
+ if (res.statusCode === 401) {
2557
+ const wwwAuth = res.headers["www-authenticate"];
2558
+ if (wwwAuth) {
2559
+ return { passed: true, details: `WWW-Authenticate: ${wwwAuth}` };
2560
+ }
2561
+ return {
2562
+ passed: false,
2563
+ details: "HTTP 401 but missing WWW-Authenticate header (spec: SHOULD include to indicate required auth scheme)"
2564
+ };
2565
+ }
2566
+ if (res.statusCode === 403) {
2567
+ return { passed: true, details: "HTTP 403 (WWW-Authenticate not applicable for 403)" };
2568
+ }
2569
+ return { passed: true, details: `HTTP ${res.statusCode} \u2014 not a 401 response` };
2570
+ } catch {
2571
+ return { passed: true, details: "Connection rejected (acceptable)" };
2572
+ }
2573
+ }
2574
+ );
2431
2575
  await test(
2432
2576
  "security-auth-malformed",
2433
2577
  "Rejects malformed auth credentials",
package/dist/index.js CHANGED
@@ -189,7 +189,7 @@ var TEST_DEFINITIONS = [
189
189
  description: "Sends a request with Accept: text/event-stream and checks that SSE responses include the event: message field. Per spec, servers MUST set event: message for JSON-RPC messages in SSE streams.",
190
190
  recommendation: 'Include "event: message" before each "data:" line in your SSE responses. This is required by the MCP spec for JSON-RPC messages sent over SSE.'
191
191
  },
192
- // ── Lifecycle (15 tests) ─────────────────────────────────────────
192
+ // ── Lifecycle (17 tests) ─────────────────────────────────────────
193
193
  {
194
194
  id: "lifecycle-init",
195
195
  name: "Initialize handshake",
@@ -325,6 +325,24 @@ var TEST_DEFINITIONS = [
325
325
  description: "Sends a notifications/progress to the server and verifies it does not error. Note: per spec, progress flows from server to client during long-running requests. This test validates the server handles unexpected notifications gracefully.",
326
326
  recommendation: "Accept unknown notifications without returning an error. The server should not crash or return a non-2xx status for notifications it does not recognize."
327
327
  },
328
+ {
329
+ id: "lifecycle-list-changed",
330
+ name: "Accepts listChanged notifications",
331
+ category: "lifecycle",
332
+ required: false,
333
+ specRef: "basic/lifecycle#capability-negotiation",
334
+ description: "Sends notifications/tools/list_changed, notifications/resources/list_changed, and notifications/prompts/list_changed for declared capabilities and verifies the server accepts them.",
335
+ recommendation: "Accept listChanged notifications gracefully. When received, re-fetch the relevant list to detect changes. These notifications signal that the client's cached list may be stale."
336
+ },
337
+ {
338
+ id: "lifecycle-progress-token",
339
+ name: "Supports progress tokens in requests",
340
+ category: "lifecycle",
341
+ required: false,
342
+ specRef: "basic/utilities#progress",
343
+ description: "Sends a tools/call request with _meta.progressToken and checks if the server sends progress notifications via SSE. Progress support is optional but recommended for long-running operations.",
344
+ recommendation: "When a request includes _meta.progressToken, send notifications/progress events via SSE to report progress. Include progressToken, progress (current), and optionally total fields."
345
+ },
328
346
  // ── Tools (4 tests) ──────────────────────────────────────────────
329
347
  {
330
348
  id: "tools-list",
@@ -582,7 +600,7 @@ var TEST_DEFINITIONS = [
582
600
  description: "Validates every resource has a valid URI (parseable as a URL) and a name field.",
583
601
  recommendation: "Ensure every resource has a valid, parseable URI and a name field. Add description and mimeType for better client integration."
584
602
  },
585
- // ── Security: Auth & Transport (9 tests) ─────────────────────────
603
+ // ── Security: Auth & Transport (10 tests) ────────────────────────
586
604
  {
587
605
  id: "security-auth-required",
588
606
  name: "Rejects unauthenticated requests",
@@ -592,6 +610,15 @@ var TEST_DEFINITIONS = [
592
610
  description: "Sends a request without an Authorization header and verifies the server returns HTTP 401. Servers exposed over the network should require authentication.",
593
611
  recommendation: "Implement authentication on your MCP endpoint. Return HTTP 401 Unauthorized for requests without valid credentials. Use OAuth 2.1 or Bearer tokens as recommended by the MCP spec."
594
612
  },
613
+ {
614
+ id: "security-www-authenticate",
615
+ name: "401 responses include WWW-Authenticate header",
616
+ category: "security",
617
+ required: false,
618
+ specRef: "basic/authorization",
619
+ description: "When the server returns HTTP 401, checks for a WWW-Authenticate header indicating the required authentication scheme. Per HTTP spec (RFC 9110), servers SHOULD include this header.",
620
+ recommendation: `Include a WWW-Authenticate header in 401 responses to indicate the required auth scheme (e.g., 'WWW-Authenticate: Bearer realm="mcp"').`
621
+ },
595
622
  {
596
623
  id: "security-auth-malformed",
597
624
  name: "Rejects malformed auth credentials",
@@ -1509,6 +1536,90 @@ async function runComplianceSuite(url, options = {}) {
1509
1536
  };
1510
1537
  }
1511
1538
  );
1539
+ await test(
1540
+ "lifecycle-list-changed",
1541
+ "Accepts listChanged notifications",
1542
+ "lifecycle",
1543
+ false,
1544
+ "basic/lifecycle#capability-negotiation",
1545
+ async () => {
1546
+ const notifications = [
1547
+ { method: "notifications/tools/list_changed", gate: hasTools },
1548
+ { method: "notifications/resources/list_changed", gate: hasResources },
1549
+ { method: "notifications/prompts/list_changed", gate: hasPrompts }
1550
+ ];
1551
+ const applicable = notifications.filter((n) => n.gate);
1552
+ if (applicable.length === 0) {
1553
+ return { passed: true, details: "No capabilities declared \u2014 listChanged notifications not applicable" };
1554
+ }
1555
+ const issues = [];
1556
+ for (const { method } of applicable) {
1557
+ try {
1558
+ const res = await mcpNotification(backendUrl, method, void 0, buildHeaders(), timeout);
1559
+ if (res.statusCode < 200 || res.statusCode >= 300) {
1560
+ issues.push(`${method}: HTTP ${res.statusCode}`);
1561
+ }
1562
+ } catch (err) {
1563
+ issues.push(`${method}: ${err instanceof Error ? err.message : "error"}`);
1564
+ }
1565
+ }
1566
+ if (issues.length > 0) return { passed: false, details: issues.join("; ") };
1567
+ return {
1568
+ passed: true,
1569
+ details: `${applicable.length} listChanged notification(s) accepted: ${applicable.map((n) => n.method).join(", ")}`
1570
+ };
1571
+ }
1572
+ );
1573
+ await test(
1574
+ "lifecycle-progress-token",
1575
+ "Supports progress tokens in requests",
1576
+ "lifecycle",
1577
+ false,
1578
+ "basic/utilities#progress",
1579
+ async () => {
1580
+ if (!hasTools || toolNames.length === 0) {
1581
+ return { passed: true, details: "No tools available for progress token test (skipped)" };
1582
+ }
1583
+ const progressToken = "compliance-progress-test";
1584
+ const reqBody = JSON.stringify({
1585
+ jsonrpc: "2.0",
1586
+ id: nextId(),
1587
+ method: "tools/call",
1588
+ params: {
1589
+ name: toolNames[0],
1590
+ arguments: {},
1591
+ _meta: { progressToken }
1592
+ }
1593
+ });
1594
+ try {
1595
+ const res = await request(backendUrl, {
1596
+ method: "POST",
1597
+ headers: {
1598
+ "Content-Type": "application/json",
1599
+ Accept: "text/event-stream",
1600
+ ...buildHeaders()
1601
+ },
1602
+ body: reqBody,
1603
+ signal: AbortSignal.timeout(timeout)
1604
+ });
1605
+ const text = await res.body.text();
1606
+ const rawCtProgress = res.headers["content-type"];
1607
+ const ct = (Array.isArray(rawCtProgress) ? rawCtProgress[0] : rawCtProgress || "").toLowerCase();
1608
+ if (ct.includes("text/event-stream") && text.includes("notifications/progress")) {
1609
+ return { passed: true, details: "Server sent progress notifications via SSE with progressToken" };
1610
+ }
1611
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1612
+ return {
1613
+ passed: true,
1614
+ details: "Server accepted request with progressToken (no progress events observed \u2014 optional)"
1615
+ };
1616
+ }
1617
+ return { passed: true, details: `HTTP ${res.statusCode} \u2014 request with progressToken accepted` };
1618
+ } catch {
1619
+ return { passed: true, details: "Request with progressToken handled (no progress events observed \u2014 optional)" };
1620
+ }
1621
+ }
1622
+ );
1512
1623
  await test(
1513
1624
  "transport-content-type-init",
1514
1625
  "Initialize response has valid content type",
@@ -2443,6 +2554,39 @@ async function runComplianceSuite(url, options = {}) {
2443
2554
  }
2444
2555
  }
2445
2556
  );
2557
+ await test(
2558
+ "security-www-authenticate",
2559
+ "401 responses include WWW-Authenticate header",
2560
+ "security",
2561
+ false,
2562
+ "basic/authorization",
2563
+ async () => {
2564
+ if (!hasAuth) {
2565
+ return { passed: true, details: "Skipped: server does not require auth" };
2566
+ }
2567
+ const noAuthHeaders = {};
2568
+ if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
2569
+ try {
2570
+ const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout);
2571
+ if (res.statusCode === 401) {
2572
+ const wwwAuth = res.headers["www-authenticate"];
2573
+ if (wwwAuth) {
2574
+ return { passed: true, details: `WWW-Authenticate: ${wwwAuth}` };
2575
+ }
2576
+ return {
2577
+ passed: false,
2578
+ details: "HTTP 401 but missing WWW-Authenticate header (spec: SHOULD include to indicate required auth scheme)"
2579
+ };
2580
+ }
2581
+ if (res.statusCode === 403) {
2582
+ return { passed: true, details: "HTTP 403 (WWW-Authenticate not applicable for 403)" };
2583
+ }
2584
+ return { passed: true, details: `HTTP ${res.statusCode} \u2014 not a 401 response` };
2585
+ } catch {
2586
+ return { passed: true, details: "Connection rejected (acceptable)" };
2587
+ }
2588
+ }
2589
+ );
2446
2590
  await test(
2447
2591
  "security-auth-malformed",
2448
2592
  "Rejects malformed auth credentials",
@@ -3258,7 +3402,7 @@ async function runComplianceSuite(url, options = {}) {
3258
3402
  function registerTools(server) {
3259
3403
  server.tool(
3260
3404
  "mcp_compliance_test",
3261
- "Run the full MCP compliance test suite against a server URL. Returns grade (A-F), score, and detailed results for all 78 tests covering transport, lifecycle, tools, resources, prompts, errors, schema validation, and security.",
3405
+ "Run the full MCP compliance test suite against a server URL. Returns grade (A-F), score, and detailed results for all 81 tests covering transport, lifecycle, tools, resources, prompts, errors, schema validation, and security.",
3262
3406
  {
3263
3407
  url: z.string().url().describe("The MCP server URL to test (must be HTTP or HTTPS)"),
3264
3408
  auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
@@ -2,7 +2,7 @@ import {
2
2
  SPEC_BASE,
3
3
  TEST_DEFINITIONS,
4
4
  runComplianceSuite
5
- } from "../chunk-SELO4TOW.js";
5
+ } from "../chunk-DOIOJVEE.js";
6
6
 
7
7
  // src/mcp/server.ts
8
8
  import { createRequire } from "module";
@@ -14,7 +14,7 @@ import { z } from "zod";
14
14
  function registerTools(server) {
15
15
  server.tool(
16
16
  "mcp_compliance_test",
17
- "Run the full MCP compliance test suite against a server URL. Returns grade (A-F), score, and detailed results for all 78 tests covering transport, lifecycle, tools, resources, prompts, errors, schema validation, and security.",
17
+ "Run the full MCP compliance test suite against a server URL. Returns grade (A-F), score, and detailed results for all 81 tests covering transport, lifecycle, tools, resources, prompts, errors, schema validation, and security.",
18
18
  {
19
19
  url: z.string().url().describe("The MCP server URL to test (must be HTTP or HTTPS)"),
20
20
  auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
package/dist/runner.d.ts CHANGED
@@ -60,7 +60,7 @@ interface TestDefinition {
60
60
  description: string;
61
61
  recommendation: string;
62
62
  }
63
- /** All 78 test IDs with descriptions for the explain command */
63
+ /** All 81 test IDs with descriptions for the explain command */
64
64
  declare const TEST_DEFINITIONS: TestDefinition[];
65
65
 
66
66
  declare function computeGrade(score: number): Grade;
package/dist/runner.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  generateBadge,
8
8
  parseSSEResponse,
9
9
  runComplianceSuite
10
- } from "./chunk-SELO4TOW.js";
10
+ } from "./chunk-DOIOJVEE.js";
11
11
  export {
12
12
  SPEC_BASE,
13
13
  SPEC_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp-compliance",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI tool and MCP server that tests MCP servers for spec compliance",
5
5
  "license": "MIT",
6
6
  "author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",