@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 +10 -7
- package/dist/{chunk-SELO4TOW.js → chunk-DOIOJVEE.js} +146 -2
- package/dist/index.js +147 -3
- package/dist/mcp/server.js +2 -2
- package/dist/runner.d.ts +1 -1
- package/dist/runner.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/YawLabs/mcp-compliance/stargazers)
|
|
6
6
|
[](https://github.com/YawLabs/mcp-compliance/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
|
-
**Test any MCP server for spec compliance.**
|
|
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
|
-
- **
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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")'),
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
SPEC_BASE,
|
|
3
3
|
TEST_DEFINITIONS,
|
|
4
4
|
runComplianceSuite
|
|
5
|
-
} from "../chunk-
|
|
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
|
|
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
|
|
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
package/package.json
CHANGED