@yawlabs/mcp-compliance 0.14.3 → 0.15.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 +1 -1
- package/dist/{chunk-6PF56RRO.js → chunk-A3UG3J63.js} +68 -24
- package/dist/index.js +116 -77
- package/dist/mcp/server.js +7 -24
- package/dist/runner.d.ts +18 -18
- package/dist/runner.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
Built and maintained by [Yaw Labs](https://yaw.sh).
|
|
11
11
|
|
|
12
|
-
[](yaw
|
|
12
|
+
[](https://yaw.sh/mcp/install?name=mcp-compliance&command=npx&args=-y%2C%40yawlabs%2Fmcp-compliance&description=Test%20any%20MCP%20server%20against%20the%20spec%20-%2088-test%20suite%20with%20letter-grade%20scoring&source=https%3A%2F%2Fgithub.com%2FYawLabs%2Fmcp-compliance)
|
|
13
13
|
|
|
14
14
|
One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.
|
|
15
15
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/runner.ts
|
|
2
|
-
import { createRequire } from "module";
|
|
3
2
|
import { request as request2 } from "undici";
|
|
4
3
|
|
|
5
4
|
// src/badge.ts
|
|
@@ -74,6 +73,27 @@ function computeScore(tests) {
|
|
|
74
73
|
};
|
|
75
74
|
}
|
|
76
75
|
|
|
76
|
+
// src/pkg-version.ts
|
|
77
|
+
import { existsSync, readFileSync } from "fs";
|
|
78
|
+
import { dirname, join } from "path";
|
|
79
|
+
import { fileURLToPath } from "url";
|
|
80
|
+
function readPackageVersion(metaUrl) {
|
|
81
|
+
let dir = dirname(fileURLToPath(metaUrl));
|
|
82
|
+
for (; ; ) {
|
|
83
|
+
const pkgPath = join(dir, "package.json");
|
|
84
|
+
if (existsSync(pkgPath)) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
|
|
87
|
+
} catch {
|
|
88
|
+
return "0.0.0";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const parent = dirname(dir);
|
|
92
|
+
if (parent === dir) return "0.0.0";
|
|
93
|
+
dir = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
// src/transport/http.ts
|
|
78
98
|
import { request } from "undici";
|
|
79
99
|
|
|
@@ -127,10 +147,17 @@ function createHttpTransport(opts) {
|
|
|
127
147
|
}
|
|
128
148
|
return out;
|
|
129
149
|
}
|
|
130
|
-
async function doRawRequest(method, body, extraHeaders, timeout) {
|
|
150
|
+
async function doRawRequest(method, body, extraHeaders, timeout, omitUserHeaders) {
|
|
151
|
+
const base = sessionHeaders();
|
|
152
|
+
if (omitUserHeaders && omitUserHeaders.length > 0) {
|
|
153
|
+
const drop = new Set(omitUserHeaders.map((h) => h.toLowerCase()));
|
|
154
|
+
for (const key of Object.keys(base)) {
|
|
155
|
+
if (drop.has(key.toLowerCase())) delete base[key];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
131
158
|
const headers = {
|
|
132
159
|
Accept: "application/json, text/event-stream",
|
|
133
|
-
...
|
|
160
|
+
...base,
|
|
134
161
|
...extraHeaders
|
|
135
162
|
};
|
|
136
163
|
if (body !== void 0 && !("Content-Type" in headers) && !("content-type" in headers)) {
|
|
@@ -155,7 +182,7 @@ function createHttpTransport(opts) {
|
|
|
155
182
|
async request(method, params, nextId, init) {
|
|
156
183
|
const id = nextId();
|
|
157
184
|
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
158
|
-
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout);
|
|
185
|
+
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout, init.omitUserHeaders);
|
|
159
186
|
const contentType = (raw.headers["content-type"] || "").toLowerCase();
|
|
160
187
|
let parsed;
|
|
161
188
|
if (contentType.includes("text/event-stream")) {
|
|
@@ -185,7 +212,7 @@ function createHttpTransport(opts) {
|
|
|
185
212
|
},
|
|
186
213
|
async notify(method, params, init) {
|
|
187
214
|
const body = JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} });
|
|
188
|
-
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout);
|
|
215
|
+
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout, init.omitUserHeaders);
|
|
189
216
|
return { statusCode: raw.statusCode, headers: raw.headers };
|
|
190
217
|
},
|
|
191
218
|
async close() {
|
|
@@ -392,23 +419,30 @@ function createStdioTransport(opts) {
|
|
|
392
419
|
child.stdin?.end();
|
|
393
420
|
} catch {
|
|
394
421
|
}
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
422
|
+
const treeKill = (force) => {
|
|
423
|
+
if (isWindows && child.pid !== void 0) {
|
|
424
|
+
try {
|
|
425
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", ...force ? ["/f"] : []], { stdio: "ignore" });
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
398
429
|
try {
|
|
399
|
-
child.kill("SIGKILL");
|
|
430
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
400
431
|
} catch {
|
|
401
432
|
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const gracePeriodMs = 2e3;
|
|
436
|
+
await new Promise((resolve) => {
|
|
437
|
+
const timer = setTimeout(() => {
|
|
438
|
+
treeKill(true);
|
|
402
439
|
resolve();
|
|
403
440
|
}, gracePeriodMs);
|
|
404
441
|
child.once("exit", () => {
|
|
405
442
|
clearTimeout(timer);
|
|
406
443
|
resolve();
|
|
407
444
|
});
|
|
408
|
-
|
|
409
|
-
child.kill(isWindows ? void 0 : "SIGTERM");
|
|
410
|
-
} catch {
|
|
411
|
-
}
|
|
445
|
+
treeKill(false);
|
|
412
446
|
});
|
|
413
447
|
rejectAllPending(new Error("stdio transport: closed"));
|
|
414
448
|
},
|
|
@@ -1247,8 +1281,7 @@ var TEST_DEFINITIONS = [
|
|
|
1247
1281
|
|
|
1248
1282
|
// src/runner.ts
|
|
1249
1283
|
var TEST_DEFINITIONS_MAP = new Map(TEST_DEFINITIONS.map((t) => [t.id, t]));
|
|
1250
|
-
var
|
|
1251
|
-
var { version: TOOL_VERSION } = _require("../package.json");
|
|
1284
|
+
var TOOL_VERSION = readPackageVersion(import.meta.url);
|
|
1252
1285
|
var SPEC_VERSION = "2025-11-25";
|
|
1253
1286
|
var SPEC_BASE = `https://modelcontextprotocol.io/specification/${SPEC_VERSION}`;
|
|
1254
1287
|
var VALID_CONTENT_TYPES = ["text", "image", "audio", "resource", "resource_link"];
|
|
@@ -1451,10 +1484,11 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
1451
1484
|
const retries = options.retries || 0;
|
|
1452
1485
|
let sessionId = null;
|
|
1453
1486
|
let negotiatedProtocolVersion = null;
|
|
1454
|
-
async function mcpRequest(_backendUrl, method, params, idCounter, extraHeaders, timeoutMs) {
|
|
1487
|
+
async function mcpRequest(_backendUrl, method, params, idCounter, extraHeaders, timeoutMs, omitUserHeaders) {
|
|
1455
1488
|
const res = await transport.request(method, params, idCounter, {
|
|
1456
1489
|
timeout: timeoutMs,
|
|
1457
|
-
headers: extraHeaders
|
|
1490
|
+
headers: extraHeaders,
|
|
1491
|
+
omitUserHeaders
|
|
1458
1492
|
});
|
|
1459
1493
|
return {
|
|
1460
1494
|
statusCode: res.statusCode ?? 200,
|
|
@@ -2467,7 +2501,7 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
2467
2501
|
issues.push("Tool missing name");
|
|
2468
2502
|
continue;
|
|
2469
2503
|
}
|
|
2470
|
-
if (tool.name.length > 128 || !/^[A-Za-z0-9_
|
|
2504
|
+
if (tool.name.length > 128 || !/^[A-Za-z0-9_.-]+$/.test(tool.name)) {
|
|
2471
2505
|
issues.push(`${tool.name}: name format invalid`);
|
|
2472
2506
|
}
|
|
2473
2507
|
if (!tool.description) warnings.push(`Tool "${tool.name}" missing description`);
|
|
@@ -3168,7 +3202,9 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3168
3202
|
const noAuthHeaders = {};
|
|
3169
3203
|
if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
|
|
3170
3204
|
try {
|
|
3171
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout
|
|
3205
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout, [
|
|
3206
|
+
"authorization"
|
|
3207
|
+
]);
|
|
3172
3208
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
3173
3209
|
return { passed: true, details: `HTTP ${res.statusCode} (unauthenticated request rejected)` };
|
|
3174
3210
|
}
|
|
@@ -3191,7 +3227,9 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3191
3227
|
const noAuthHeaders = {};
|
|
3192
3228
|
if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
|
|
3193
3229
|
try {
|
|
3194
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout
|
|
3230
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout, [
|
|
3231
|
+
"authorization"
|
|
3232
|
+
]);
|
|
3195
3233
|
if (res.statusCode === 401) {
|
|
3196
3234
|
const wwwAuth = res.headers["www-authenticate"];
|
|
3197
3235
|
if (wwwAuth) {
|
|
@@ -3219,14 +3257,16 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3219
3257
|
"basic/authorization",
|
|
3220
3258
|
async () => {
|
|
3221
3259
|
if (!hasAuth) {
|
|
3222
|
-
return { passed:
|
|
3260
|
+
return { passed: true, details: "Skipped: server does not require auth" };
|
|
3223
3261
|
}
|
|
3224
3262
|
const malformedHeaders = {
|
|
3225
3263
|
Authorization: "Bearer INVALID_GARBAGE_TOKEN_!@#$%^&*()"
|
|
3226
3264
|
};
|
|
3227
3265
|
if (sessionId) malformedHeaders["mcp-session-id"] = sessionId;
|
|
3228
3266
|
try {
|
|
3229
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, malformedHeaders, timeout
|
|
3267
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, malformedHeaders, timeout, [
|
|
3268
|
+
"authorization"
|
|
3269
|
+
]);
|
|
3230
3270
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
3231
3271
|
return { passed: true, details: `HTTP ${res.statusCode} (malformed auth rejected)` };
|
|
3232
3272
|
}
|
|
@@ -3316,7 +3356,9 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3316
3356
|
"mcp-session-id": sessionId
|
|
3317
3357
|
};
|
|
3318
3358
|
try {
|
|
3319
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, sessionOnlyHeaders, timeout
|
|
3359
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, sessionOnlyHeaders, timeout, [
|
|
3360
|
+
"authorization"
|
|
3361
|
+
]);
|
|
3320
3362
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
3321
3363
|
return { passed: true, details: `HTTP ${res.statusCode} (session ID alone not sufficient for auth)` };
|
|
3322
3364
|
}
|
|
@@ -3717,9 +3759,10 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3717
3759
|
return { passed: true, details: "No tools available to test (skipped)" };
|
|
3718
3760
|
}
|
|
3719
3761
|
try {
|
|
3762
|
+
const maliciousArgs = JSON.parse('{"__injected_param__":"malicious_value","__proto__":{"admin":true}}');
|
|
3720
3763
|
const res = await rpc("tools/call", {
|
|
3721
3764
|
name: toolNames[0],
|
|
3722
|
-
arguments:
|
|
3765
|
+
arguments: maliciousArgs
|
|
3723
3766
|
});
|
|
3724
3767
|
const error = res.body?.error;
|
|
3725
3768
|
if (error) {
|
|
@@ -4123,6 +4166,7 @@ export {
|
|
|
4123
4166
|
generateBadge,
|
|
4124
4167
|
computeGrade,
|
|
4125
4168
|
computeScore,
|
|
4169
|
+
readPackageVersion,
|
|
4126
4170
|
parseSSEResponse,
|
|
4127
4171
|
TEST_DEFINITIONS,
|
|
4128
4172
|
SPEC_VERSION,
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { watch as fsWatch, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
5
|
-
import { createRequire
|
|
5
|
+
import { createRequire } from "module";
|
|
6
6
|
import { createInterface } from "readline/promises";
|
|
7
7
|
import chalk2 from "chalk";
|
|
8
8
|
import { Command, Option } from "commander";
|
|
@@ -117,10 +117,17 @@ function createHttpTransport(opts) {
|
|
|
117
117
|
}
|
|
118
118
|
return out;
|
|
119
119
|
}
|
|
120
|
-
async function doRawRequest(method, body, extraHeaders, timeout) {
|
|
120
|
+
async function doRawRequest(method, body, extraHeaders, timeout, omitUserHeaders) {
|
|
121
|
+
const base = sessionHeaders();
|
|
122
|
+
if (omitUserHeaders && omitUserHeaders.length > 0) {
|
|
123
|
+
const drop = new Set(omitUserHeaders.map((h) => h.toLowerCase()));
|
|
124
|
+
for (const key of Object.keys(base)) {
|
|
125
|
+
if (drop.has(key.toLowerCase())) delete base[key];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
121
128
|
const headers = {
|
|
122
129
|
Accept: "application/json, text/event-stream",
|
|
123
|
-
...
|
|
130
|
+
...base,
|
|
124
131
|
...extraHeaders
|
|
125
132
|
};
|
|
126
133
|
if (body !== void 0 && !("Content-Type" in headers) && !("content-type" in headers)) {
|
|
@@ -145,7 +152,7 @@ function createHttpTransport(opts) {
|
|
|
145
152
|
async request(method, params, nextId, init) {
|
|
146
153
|
const id = nextId();
|
|
147
154
|
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
148
|
-
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout);
|
|
155
|
+
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout, init.omitUserHeaders);
|
|
149
156
|
const contentType = (raw.headers["content-type"] || "").toLowerCase();
|
|
150
157
|
let parsed;
|
|
151
158
|
if (contentType.includes("text/event-stream")) {
|
|
@@ -175,7 +182,7 @@ function createHttpTransport(opts) {
|
|
|
175
182
|
},
|
|
176
183
|
async notify(method, params, init) {
|
|
177
184
|
const body = JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} });
|
|
178
|
-
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout);
|
|
185
|
+
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout, init.omitUserHeaders);
|
|
179
186
|
return { statusCode: raw.statusCode, headers: raw.headers };
|
|
180
187
|
},
|
|
181
188
|
async close() {
|
|
@@ -231,10 +238,10 @@ function createStdioTransport(opts) {
|
|
|
231
238
|
const pending = /* @__PURE__ */ new Map();
|
|
232
239
|
let stdoutBuffer = "";
|
|
233
240
|
let stderrBuffer = "";
|
|
234
|
-
const spawnReady = new Promise((
|
|
241
|
+
const spawnReady = new Promise((resolve2, reject) => {
|
|
235
242
|
child.once("spawn", () => {
|
|
236
243
|
spawned = true;
|
|
237
|
-
|
|
244
|
+
resolve2();
|
|
238
245
|
});
|
|
239
246
|
child.once("error", (err) => {
|
|
240
247
|
if (!spawned) reject(err);
|
|
@@ -330,9 +337,9 @@ function createStdioTransport(opts) {
|
|
|
330
337
|
if (spawnError) throw new Error(annotateWithStderr(`stdio transport: spawn failed \u2014 ${spawnError.message}`));
|
|
331
338
|
const stdin = child.stdin;
|
|
332
339
|
if (!stdin || stdin.destroyed) throw new Error(annotateWithStderr("stdio transport: stdin is closed"));
|
|
333
|
-
return new Promise((
|
|
340
|
+
return new Promise((resolve2, reject) => {
|
|
334
341
|
stdin.write(`${line}
|
|
335
|
-
`, "utf8", (err) => err ? reject(err) :
|
|
342
|
+
`, "utf8", (err) => err ? reject(err) : resolve2());
|
|
336
343
|
});
|
|
337
344
|
}
|
|
338
345
|
const transport = {
|
|
@@ -354,7 +361,7 @@ function createStdioTransport(opts) {
|
|
|
354
361
|
async request(method, params, nextId, init) {
|
|
355
362
|
const id = nextId();
|
|
356
363
|
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
357
|
-
return new Promise((
|
|
364
|
+
return new Promise((resolve2, reject) => {
|
|
358
365
|
const timer = setTimeout(() => {
|
|
359
366
|
pending.delete(id);
|
|
360
367
|
reject(
|
|
@@ -363,7 +370,7 @@ function createStdioTransport(opts) {
|
|
|
363
370
|
)
|
|
364
371
|
);
|
|
365
372
|
}, init.timeout);
|
|
366
|
-
pending.set(id, { resolve:
|
|
373
|
+
pending.set(id, { resolve: resolve2, reject, id, timer });
|
|
367
374
|
writeLine(body).catch((err) => {
|
|
368
375
|
clearTimeout(timer);
|
|
369
376
|
pending.delete(id);
|
|
@@ -382,23 +389,30 @@ function createStdioTransport(opts) {
|
|
|
382
389
|
child.stdin?.end();
|
|
383
390
|
} catch {
|
|
384
391
|
}
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
const timer = setTimeout(() => {
|
|
392
|
+
const treeKill = (force) => {
|
|
393
|
+
if (isWindows && child.pid !== void 0) {
|
|
388
394
|
try {
|
|
389
|
-
child.
|
|
395
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", ...force ? ["/f"] : []], { stdio: "ignore" });
|
|
390
396
|
} catch {
|
|
391
397
|
}
|
|
392
|
-
|
|
398
|
+
} else {
|
|
399
|
+
try {
|
|
400
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const gracePeriodMs = 2e3;
|
|
406
|
+
await new Promise((resolve2) => {
|
|
407
|
+
const timer = setTimeout(() => {
|
|
408
|
+
treeKill(true);
|
|
409
|
+
resolve2();
|
|
393
410
|
}, gracePeriodMs);
|
|
394
411
|
child.once("exit", () => {
|
|
395
412
|
clearTimeout(timer);
|
|
396
|
-
|
|
413
|
+
resolve2();
|
|
397
414
|
});
|
|
398
|
-
|
|
399
|
-
child.kill(isWindows ? void 0 : "SIGTERM");
|
|
400
|
-
} catch {
|
|
401
|
-
}
|
|
415
|
+
treeKill(false);
|
|
402
416
|
});
|
|
403
417
|
rejectAllPending(new Error("stdio transport: closed"));
|
|
404
418
|
},
|
|
@@ -662,7 +676,9 @@ function diffReports(baseline, current) {
|
|
|
662
676
|
}
|
|
663
677
|
function formatDiff(summary) {
|
|
664
678
|
const lines = [];
|
|
665
|
-
|
|
679
|
+
let arrow = "\u2192";
|
|
680
|
+
if (summary.currentScore > summary.baselineScore) arrow = "\u2191";
|
|
681
|
+
else if (summary.currentScore < summary.baselineScore) arrow = "\u2193";
|
|
666
682
|
lines.push(
|
|
667
683
|
`Grade ${summary.baselineGrade} (${summary.baselineScore}%) ${arrow} ${summary.currentGrade} (${summary.currentScore}%)`
|
|
668
684
|
);
|
|
@@ -699,17 +715,37 @@ function hasRegressions(summary) {
|
|
|
699
715
|
}
|
|
700
716
|
|
|
701
717
|
// src/mcp/server.ts
|
|
702
|
-
import {
|
|
703
|
-
import { basename, dirname
|
|
704
|
-
import { fileURLToPath } from "url";
|
|
718
|
+
import { realpathSync } from "fs";
|
|
719
|
+
import { basename, dirname as dirname2 } from "path";
|
|
720
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
705
721
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
706
722
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
707
723
|
|
|
724
|
+
// src/pkg-version.ts
|
|
725
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
726
|
+
import { dirname, join as join2 } from "path";
|
|
727
|
+
import { fileURLToPath } from "url";
|
|
728
|
+
function readPackageVersion(metaUrl) {
|
|
729
|
+
let dir = dirname(fileURLToPath(metaUrl));
|
|
730
|
+
for (; ; ) {
|
|
731
|
+
const pkgPath = join2(dir, "package.json");
|
|
732
|
+
if (existsSync2(pkgPath)) {
|
|
733
|
+
try {
|
|
734
|
+
return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "0.0.0";
|
|
735
|
+
} catch {
|
|
736
|
+
return "0.0.0";
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const parent = dirname(dir);
|
|
740
|
+
if (parent === dir) return "0.0.0";
|
|
741
|
+
dir = parent;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
708
745
|
// src/mcp/tools.ts
|
|
709
746
|
import { z } from "zod";
|
|
710
747
|
|
|
711
748
|
// src/runner.ts
|
|
712
|
-
import { createRequire } from "module";
|
|
713
749
|
import { request as request2 } from "undici";
|
|
714
750
|
|
|
715
751
|
// src/badge.ts
|
|
@@ -1604,8 +1640,7 @@ var TEST_DEFINITIONS = [
|
|
|
1604
1640
|
|
|
1605
1641
|
// src/runner.ts
|
|
1606
1642
|
var TEST_DEFINITIONS_MAP = new Map(TEST_DEFINITIONS.map((t) => [t.id, t]));
|
|
1607
|
-
var
|
|
1608
|
-
var { version: TOOL_VERSION } = _require("../package.json");
|
|
1643
|
+
var TOOL_VERSION = readPackageVersion(import.meta.url);
|
|
1609
1644
|
var SPEC_VERSION = "2025-11-25";
|
|
1610
1645
|
var SPEC_BASE = `https://modelcontextprotocol.io/specification/${SPEC_VERSION}`;
|
|
1611
1646
|
var VALID_CONTENT_TYPES = ["text", "image", "audio", "resource", "resource_link"];
|
|
@@ -1808,10 +1843,11 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
1808
1843
|
const retries = options.retries || 0;
|
|
1809
1844
|
let sessionId = null;
|
|
1810
1845
|
let negotiatedProtocolVersion = null;
|
|
1811
|
-
async function mcpRequest(_backendUrl, method, params, idCounter, extraHeaders, timeoutMs) {
|
|
1846
|
+
async function mcpRequest(_backendUrl, method, params, idCounter, extraHeaders, timeoutMs, omitUserHeaders) {
|
|
1812
1847
|
const res = await transport.request(method, params, idCounter, {
|
|
1813
1848
|
timeout: timeoutMs,
|
|
1814
|
-
headers: extraHeaders
|
|
1849
|
+
headers: extraHeaders,
|
|
1850
|
+
omitUserHeaders
|
|
1815
1851
|
});
|
|
1816
1852
|
return {
|
|
1817
1853
|
statusCode: res.statusCode ?? 200,
|
|
@@ -2824,7 +2860,7 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
2824
2860
|
issues.push("Tool missing name");
|
|
2825
2861
|
continue;
|
|
2826
2862
|
}
|
|
2827
|
-
if (tool.name.length > 128 || !/^[A-Za-z0-9_
|
|
2863
|
+
if (tool.name.length > 128 || !/^[A-Za-z0-9_.-]+$/.test(tool.name)) {
|
|
2828
2864
|
issues.push(`${tool.name}: name format invalid`);
|
|
2829
2865
|
}
|
|
2830
2866
|
if (!tool.description) warnings.push(`Tool "${tool.name}" missing description`);
|
|
@@ -3525,7 +3561,9 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3525
3561
|
const noAuthHeaders = {};
|
|
3526
3562
|
if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
|
|
3527
3563
|
try {
|
|
3528
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout
|
|
3564
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout, [
|
|
3565
|
+
"authorization"
|
|
3566
|
+
]);
|
|
3529
3567
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
3530
3568
|
return { passed: true, details: `HTTP ${res.statusCode} (unauthenticated request rejected)` };
|
|
3531
3569
|
}
|
|
@@ -3548,7 +3586,9 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3548
3586
|
const noAuthHeaders = {};
|
|
3549
3587
|
if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
|
|
3550
3588
|
try {
|
|
3551
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout
|
|
3589
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, noAuthHeaders, timeout, [
|
|
3590
|
+
"authorization"
|
|
3591
|
+
]);
|
|
3552
3592
|
if (res.statusCode === 401) {
|
|
3553
3593
|
const wwwAuth = res.headers["www-authenticate"];
|
|
3554
3594
|
if (wwwAuth) {
|
|
@@ -3576,14 +3616,16 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3576
3616
|
"basic/authorization",
|
|
3577
3617
|
async () => {
|
|
3578
3618
|
if (!hasAuth) {
|
|
3579
|
-
return { passed:
|
|
3619
|
+
return { passed: true, details: "Skipped: server does not require auth" };
|
|
3580
3620
|
}
|
|
3581
3621
|
const malformedHeaders = {
|
|
3582
3622
|
Authorization: "Bearer INVALID_GARBAGE_TOKEN_!@#$%^&*()"
|
|
3583
3623
|
};
|
|
3584
3624
|
if (sessionId) malformedHeaders["mcp-session-id"] = sessionId;
|
|
3585
3625
|
try {
|
|
3586
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, malformedHeaders, timeout
|
|
3626
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, malformedHeaders, timeout, [
|
|
3627
|
+
"authorization"
|
|
3628
|
+
]);
|
|
3587
3629
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
3588
3630
|
return { passed: true, details: `HTTP ${res.statusCode} (malformed auth rejected)` };
|
|
3589
3631
|
}
|
|
@@ -3673,7 +3715,9 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3673
3715
|
"mcp-session-id": sessionId
|
|
3674
3716
|
};
|
|
3675
3717
|
try {
|
|
3676
|
-
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, sessionOnlyHeaders, timeout
|
|
3718
|
+
const res = await mcpRequest(backendUrl, "ping", void 0, nextId, sessionOnlyHeaders, timeout, [
|
|
3719
|
+
"authorization"
|
|
3720
|
+
]);
|
|
3677
3721
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
3678
3722
|
return { passed: true, details: `HTTP ${res.statusCode} (session ID alone not sufficient for auth)` };
|
|
3679
3723
|
}
|
|
@@ -4074,9 +4118,10 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
4074
4118
|
return { passed: true, details: "No tools available to test (skipped)" };
|
|
4075
4119
|
}
|
|
4076
4120
|
try {
|
|
4121
|
+
const maliciousArgs = JSON.parse('{"__injected_param__":"malicious_value","__proto__":{"admin":true}}');
|
|
4077
4122
|
const res = await rpc("tools/call", {
|
|
4078
4123
|
name: toolNames[0],
|
|
4079
|
-
arguments:
|
|
4124
|
+
arguments: maliciousArgs
|
|
4080
4125
|
});
|
|
4081
4126
|
const error = res.body?.error;
|
|
4082
4127
|
if (error) {
|
|
@@ -4483,7 +4528,7 @@ function registerTools(server) {
|
|
|
4483
4528
|
{
|
|
4484
4529
|
url: z.string().url().describe("The MCP server URL to test (must be HTTP or HTTPS)"),
|
|
4485
4530
|
auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
|
|
4486
|
-
headers: z.record(z.string()).optional().describe('Additional headers to include on all requests (e.g., {"X-Api-Key": "abc"})'),
|
|
4531
|
+
headers: z.record(z.string(), z.string()).optional().describe('Additional headers to include on all requests (e.g., {"X-Api-Key": "abc"})'),
|
|
4487
4532
|
timeout: z.number().int().min(1).max(3e5).optional().describe("Request timeout in milliseconds (default: 15000, max: 300000)"),
|
|
4488
4533
|
retries: z.number().int().min(0).max(10).optional().describe("Number of retries for failed tests (default: 0, max: 10)"),
|
|
4489
4534
|
only: z.array(z.string()).optional().describe("Only run tests matching these categories or test IDs"),
|
|
@@ -4549,7 +4594,7 @@ ${JSON.stringify(report, null, 2)}` }
|
|
|
4549
4594
|
{
|
|
4550
4595
|
url: z.string().url().describe("The MCP server URL to test"),
|
|
4551
4596
|
auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
|
|
4552
|
-
headers: z.record(z.string()).optional().describe("Additional headers to include on all requests"),
|
|
4597
|
+
headers: z.record(z.string(), z.string()).optional().describe("Additional headers to include on all requests"),
|
|
4553
4598
|
timeout: z.number().int().min(1).max(3e5).optional().describe("Request timeout in milliseconds (default: 15000, max: 300000)")
|
|
4554
4599
|
},
|
|
4555
4600
|
{
|
|
@@ -4645,25 +4690,7 @@ ${TEST_DEFINITIONS.map((t) => t.id).join(", ")}`
|
|
|
4645
4690
|
}
|
|
4646
4691
|
|
|
4647
4692
|
// src/mcp/server.ts
|
|
4648
|
-
|
|
4649
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
4650
|
-
const root = resolve2(dir, "..", "..", "..");
|
|
4651
|
-
while (dir !== root) {
|
|
4652
|
-
const pkgPath = join2(dir, "package.json");
|
|
4653
|
-
if (existsSync2(pkgPath)) {
|
|
4654
|
-
try {
|
|
4655
|
-
return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "0.0.0";
|
|
4656
|
-
} catch {
|
|
4657
|
-
return "0.0.0";
|
|
4658
|
-
}
|
|
4659
|
-
}
|
|
4660
|
-
const parent = dirname(dir);
|
|
4661
|
-
if (parent === dir) break;
|
|
4662
|
-
dir = parent;
|
|
4663
|
-
}
|
|
4664
|
-
return "0.0.0";
|
|
4665
|
-
}
|
|
4666
|
-
var version = findPackageVersion();
|
|
4693
|
+
var version = readPackageVersion(import.meta.url);
|
|
4667
4694
|
function createComplianceServer() {
|
|
4668
4695
|
const server = new McpServer({ name: "mcp-compliance", version });
|
|
4669
4696
|
registerTools(server);
|
|
@@ -4678,10 +4705,10 @@ function isInvokedDirectly() {
|
|
|
4678
4705
|
const argv1 = process.argv[1];
|
|
4679
4706
|
if (!argv1) return false;
|
|
4680
4707
|
try {
|
|
4681
|
-
const selfPath = realpathSync(
|
|
4708
|
+
const selfPath = realpathSync(fileURLToPath2(import.meta.url));
|
|
4682
4709
|
if (realpathSync(argv1) !== selfPath) return false;
|
|
4683
4710
|
const file = basename(selfPath);
|
|
4684
|
-
const parent = basename(
|
|
4711
|
+
const parent = basename(dirname2(selfPath));
|
|
4685
4712
|
return parent === "mcp" && (file === "server.js" || file === "server.ts");
|
|
4686
4713
|
} catch {
|
|
4687
4714
|
return false;
|
|
@@ -4970,8 +4997,7 @@ function formatSarif(report) {
|
|
|
4970
4997
|
{
|
|
4971
4998
|
physicalLocation: {
|
|
4972
4999
|
artifactLocation: {
|
|
4973
|
-
uri: report.url
|
|
4974
|
-
uriBaseId: "MCP_SERVER"
|
|
5000
|
+
uri: report.url
|
|
4975
5001
|
}
|
|
4976
5002
|
}
|
|
4977
5003
|
}
|
|
@@ -5235,14 +5261,13 @@ function splitStdioTarget(s) {
|
|
|
5235
5261
|
}
|
|
5236
5262
|
|
|
5237
5263
|
// src/token-store.ts
|
|
5238
|
-
import { createHash as createHash2 } from "crypto";
|
|
5239
5264
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
5240
5265
|
import { homedir } from "os";
|
|
5241
|
-
import { dirname as
|
|
5266
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
5242
5267
|
var STORE_DIR = join3(homedir(), ".mcp-compliance");
|
|
5243
5268
|
var STORE_PATH = join3(STORE_DIR, "tokens.json");
|
|
5244
5269
|
function hashUrl(url) {
|
|
5245
|
-
return
|
|
5270
|
+
return urlHash(url);
|
|
5246
5271
|
}
|
|
5247
5272
|
function readStore() {
|
|
5248
5273
|
if (!existsSync3(STORE_PATH)) return {};
|
|
@@ -5255,7 +5280,7 @@ function readStore() {
|
|
|
5255
5280
|
}
|
|
5256
5281
|
}
|
|
5257
5282
|
function writeStore(store) {
|
|
5258
|
-
const dir =
|
|
5283
|
+
const dir = dirname3(STORE_PATH);
|
|
5259
5284
|
try {
|
|
5260
5285
|
if (!existsSync3(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
|
|
5261
5286
|
writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), { mode: 384 });
|
|
@@ -5292,7 +5317,7 @@ function deleteToken(hash) {
|
|
|
5292
5317
|
}
|
|
5293
5318
|
|
|
5294
5319
|
// src/index.ts
|
|
5295
|
-
var require2 =
|
|
5320
|
+
var require2 = createRequire(import.meta.url);
|
|
5296
5321
|
var { version: version2 } = require2("../package.json");
|
|
5297
5322
|
function parseHeaderArg(value, prev) {
|
|
5298
5323
|
const idx = value.indexOf(":");
|
|
@@ -5432,8 +5457,9 @@ function isPrivateHost(urlStr) {
|
|
|
5432
5457
|
if (a === 192 && b === 168) return true;
|
|
5433
5458
|
if (a === 0) return true;
|
|
5434
5459
|
}
|
|
5435
|
-
|
|
5436
|
-
if (
|
|
5460
|
+
const v6 = host.replace(/^\[|\]$/g, "");
|
|
5461
|
+
if (v6 === "::1") return true;
|
|
5462
|
+
if (v6.includes(":") && (v6.startsWith("fe80:") || v6.startsWith("fc") || v6.startsWith("fd"))) return true;
|
|
5437
5463
|
return false;
|
|
5438
5464
|
}
|
|
5439
5465
|
async function promptYesNo(message) {
|
|
@@ -5452,7 +5478,14 @@ program.command("test").description("Run the full compliance test suite against
|
|
|
5452
5478
|
"[target]",
|
|
5453
5479
|
"Server URL, or command to spawn as a stdio server (optional when a config file defines 'target')"
|
|
5454
5480
|
).argument("[extraArgs...]", "Additional args passed to the stdio command").addOption(
|
|
5455
|
-
new Option("--format <format>", "Output format
|
|
5481
|
+
new Option("--format <format>", "Output format (default: terminal, or `format` in config)").choices([
|
|
5482
|
+
"terminal",
|
|
5483
|
+
"json",
|
|
5484
|
+
"sarif",
|
|
5485
|
+
"github",
|
|
5486
|
+
"markdown",
|
|
5487
|
+
"html"
|
|
5488
|
+
])
|
|
5456
5489
|
).option("--config <path>", "Load options from a config file (default: mcp-compliance.config.json in cwd)").option("--output <file>", "Write a local SVG badge to the given path after the run (works with any transport)").option("--list", "Print the test IDs that would run given current filters, then exit (no connection)").addOption(
|
|
5457
5490
|
new Option(
|
|
5458
5491
|
"--transport <kind>",
|
|
@@ -5473,8 +5506,7 @@ program.command("test").description("Run the full compliance test suite against
|
|
|
5473
5506
|
{}
|
|
5474
5507
|
).option("--auth <token>", 'Shorthand for -H "Authorization: <token>" (HTTP only)').option("-E, --env <var>", 'Set env var for stdio command ("KEY=VALUE", repeatable)', parseEnvVar, {}).option("--env-file <path>", "Load env vars from file (KEY=VALUE per line, stdio only)").option("--cwd <dir>", "Working directory for stdio command").option(
|
|
5475
5508
|
"--timeout <ms>",
|
|
5476
|
-
"Per-request timeout in
|
|
5477
|
-
"15000"
|
|
5509
|
+
"Per-request timeout in ms after the initial handshake (default: 15000, or `timeout` in config)"
|
|
5478
5510
|
).option(
|
|
5479
5511
|
"--startup-timeout <ms>",
|
|
5480
5512
|
"Deadline for the initial initialize handshake (default: max(--timeout, 60000); covers cold `npx` cache fetches before a stdio server starts)"
|
|
@@ -5482,7 +5514,7 @@ program.command("test").description("Run the full compliance test suite against
|
|
|
5482
5514
|
"--concurrency <n>",
|
|
5483
5515
|
"Max parallel-safe tests in flight (default 1; see docs/PERFORMANCE.md before raising)",
|
|
5484
5516
|
"1"
|
|
5485
|
-
).option("--preflight-timeout <ms>", "Preflight connectivity check timeout in milliseconds").option("--retries <n>", "Number of retries for failed tests
|
|
5517
|
+
).option("--preflight-timeout <ms>", "Preflight connectivity check timeout in milliseconds").option("--retries <n>", "Number of retries for failed tests (default: 0, or `retries` in config)").option(
|
|
5486
5518
|
"--only <items>",
|
|
5487
5519
|
'Only run matching categories or test IDs, comma-separated (e.g., "transport,lifecycle" or "transport-post,lifecycle-init")',
|
|
5488
5520
|
parseList
|
|
@@ -5531,6 +5563,11 @@ ${defs.length} tests would run for transport=${transportKind}`));
|
|
|
5531
5563
|
const skip = opts.skip ?? config?.skip;
|
|
5532
5564
|
const verbose = opts.verbose ?? config?.verbose;
|
|
5533
5565
|
const strict = opts.strict ?? config?.strict;
|
|
5566
|
+
opts.format = opts.format ?? config?.format ?? "terminal";
|
|
5567
|
+
let timeout = config?.timeout ?? 15e3;
|
|
5568
|
+
if (opts.timeout !== void 0) timeout = parsePositiveInt(opts.timeout, "--timeout", 1);
|
|
5569
|
+
let retries = config?.retries ?? 0;
|
|
5570
|
+
if (opts.retries !== void 0) retries = parsePositiveInt(opts.retries, "--retries");
|
|
5534
5571
|
async function runOnce() {
|
|
5535
5572
|
if (opts.format === "terminal") {
|
|
5536
5573
|
console.log(chalk2.dim(`
|
|
@@ -5538,10 +5575,10 @@ Testing ${describeTarget(transportTarget)}...
|
|
|
5538
5575
|
`));
|
|
5539
5576
|
}
|
|
5540
5577
|
const report2 = await runComplianceSuite(transportTarget, {
|
|
5541
|
-
timeout
|
|
5578
|
+
timeout,
|
|
5542
5579
|
startupTimeout: opts.startupTimeout ? parsePositiveInt(opts.startupTimeout, "--startup-timeout", 1) : config?.startupTimeout,
|
|
5543
5580
|
preflightTimeout: opts.preflightTimeout ? parsePositiveInt(opts.preflightTimeout, "--preflight-timeout", 1) : config?.preflightTimeout,
|
|
5544
|
-
retries
|
|
5581
|
+
retries,
|
|
5545
5582
|
concurrency: parsePositiveInt(opts.concurrency, "--concurrency", 1),
|
|
5546
5583
|
only,
|
|
5547
5584
|
skip,
|
|
@@ -5664,7 +5701,7 @@ program.command("badge").description("Run tests and publish a shareable complian
|
|
|
5664
5701
|
'Add header to all requests (format: "Key: Value", repeatable; HTTP only)',
|
|
5665
5702
|
parseHeaderArg,
|
|
5666
5703
|
{}
|
|
5667
|
-
).option("--auth <token>", 'Shorthand for -H "Authorization: <token>" (HTTP only)').option("-E, --env <var>", 'Set env var for stdio command ("KEY=VALUE", repeatable)', parseEnvVar, {}).option("--env-file <path>", "Load env vars from file (stdio only)").option("--cwd <dir>", "Working directory for stdio command").option("--timeout <ms>", "Request timeout in milliseconds
|
|
5704
|
+
).option("--auth <token>", 'Shorthand for -H "Authorization: <token>" (HTTP only)').option("-E, --env <var>", 'Set env var for stdio command ("KEY=VALUE", repeatable)', parseEnvVar, {}).option("--env-file <path>", "Load env vars from file (stdio only)").option("--cwd <dir>", "Working directory for stdio command").option("--timeout <ms>", "Request timeout in milliseconds (default: 15000, or `timeout` in config)").option("--no-publish", "Do not publish the report to mcp.hosting").option("--output <file>", "Write a local SVG badge to the given path (works for any transport)").option("--no-color", "Disable colored output (also honors NO_COLOR env var)").action(
|
|
5668
5705
|
async (target, extraArgs, opts) => {
|
|
5669
5706
|
if (opts.color === false) chalk2.level = 0;
|
|
5670
5707
|
try {
|
|
@@ -5698,8 +5735,10 @@ Warning: ${transportTarget.url} looks like a private/internal address. Publishin
|
|
|
5698
5735
|
console.log(chalk2.dim(`
|
|
5699
5736
|
Testing ${describeTarget(transportTarget)}...
|
|
5700
5737
|
`));
|
|
5738
|
+
let badgeTimeout = config?.timeout ?? 15e3;
|
|
5739
|
+
if (opts.timeout !== void 0) badgeTimeout = parsePositiveInt(opts.timeout, "--timeout", 1);
|
|
5701
5740
|
const report = await runComplianceSuite(transportTarget, {
|
|
5702
|
-
timeout:
|
|
5741
|
+
timeout: badgeTimeout
|
|
5703
5742
|
});
|
|
5704
5743
|
let markdown = report.badge.markdown;
|
|
5705
5744
|
if (shouldPublish && transportTarget.type === "http") {
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SPEC_BASE,
|
|
3
3
|
TEST_DEFINITIONS,
|
|
4
|
+
readPackageVersion,
|
|
4
5
|
runComplianceSuite
|
|
5
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-A3UG3J63.js";
|
|
6
7
|
|
|
7
8
|
// src/mcp/server.ts
|
|
8
|
-
import {
|
|
9
|
-
import { basename, dirname
|
|
9
|
+
import { realpathSync } from "fs";
|
|
10
|
+
import { basename, dirname } from "path";
|
|
10
11
|
import { fileURLToPath } from "url";
|
|
11
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -20,7 +21,7 @@ function registerTools(server) {
|
|
|
20
21
|
{
|
|
21
22
|
url: z.string().url().describe("The MCP server URL to test (must be HTTP or HTTPS)"),
|
|
22
23
|
auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
|
|
23
|
-
headers: z.record(z.string()).optional().describe('Additional headers to include on all requests (e.g., {"X-Api-Key": "abc"})'),
|
|
24
|
+
headers: z.record(z.string(), z.string()).optional().describe('Additional headers to include on all requests (e.g., {"X-Api-Key": "abc"})'),
|
|
24
25
|
timeout: z.number().int().min(1).max(3e5).optional().describe("Request timeout in milliseconds (default: 15000, max: 300000)"),
|
|
25
26
|
retries: z.number().int().min(0).max(10).optional().describe("Number of retries for failed tests (default: 0, max: 10)"),
|
|
26
27
|
only: z.array(z.string()).optional().describe("Only run tests matching these categories or test IDs"),
|
|
@@ -86,7 +87,7 @@ ${JSON.stringify(report, null, 2)}` }
|
|
|
86
87
|
{
|
|
87
88
|
url: z.string().url().describe("The MCP server URL to test"),
|
|
88
89
|
auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
|
|
89
|
-
headers: z.record(z.string()).optional().describe("Additional headers to include on all requests"),
|
|
90
|
+
headers: z.record(z.string(), z.string()).optional().describe("Additional headers to include on all requests"),
|
|
90
91
|
timeout: z.number().int().min(1).max(3e5).optional().describe("Request timeout in milliseconds (default: 15000, max: 300000)")
|
|
91
92
|
},
|
|
92
93
|
{
|
|
@@ -182,25 +183,7 @@ ${TEST_DEFINITIONS.map((t) => t.id).join(", ")}`
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
// src/mcp/server.ts
|
|
185
|
-
|
|
186
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
187
|
-
const root = resolve(dir, "..", "..", "..");
|
|
188
|
-
while (dir !== root) {
|
|
189
|
-
const pkgPath = join(dir, "package.json");
|
|
190
|
-
if (existsSync(pkgPath)) {
|
|
191
|
-
try {
|
|
192
|
-
return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
|
|
193
|
-
} catch {
|
|
194
|
-
return "0.0.0";
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const parent = dirname(dir);
|
|
198
|
-
if (parent === dir) break;
|
|
199
|
-
dir = parent;
|
|
200
|
-
}
|
|
201
|
-
return "0.0.0";
|
|
202
|
-
}
|
|
203
|
-
var version = findPackageVersion();
|
|
186
|
+
var version = readPackageVersion(import.meta.url);
|
|
204
187
|
function createComplianceServer() {
|
|
205
188
|
const server = new McpServer({ name: "mcp-compliance", version });
|
|
206
189
|
registerTools(server);
|
package/dist/runner.d.ts
CHANGED
|
@@ -92,24 +92,6 @@ type TransportTarget = {
|
|
|
92
92
|
/** All 88 test IDs with descriptions for the explain command */
|
|
93
93
|
declare const TEST_DEFINITIONS: TestDefinition[];
|
|
94
94
|
|
|
95
|
-
declare function computeGrade(score: number): Grade;
|
|
96
|
-
declare function computeScore(tests: TestResult[]): {
|
|
97
|
-
score: number;
|
|
98
|
-
grade: Grade;
|
|
99
|
-
overall: "pass" | "partial" | "fail";
|
|
100
|
-
summary: {
|
|
101
|
-
total: number;
|
|
102
|
-
passed: number;
|
|
103
|
-
failed: number;
|
|
104
|
-
required: number;
|
|
105
|
-
requiredPassed: number;
|
|
106
|
-
};
|
|
107
|
-
categories: Record<string, {
|
|
108
|
-
passed: number;
|
|
109
|
-
total: number;
|
|
110
|
-
}>;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
95
|
/**
|
|
114
96
|
* Generate a short, deterministic hash of a URL for badge paths.
|
|
115
97
|
* SHA-256 truncated to 24 hex chars (96 bits of entropy) — matches the
|
|
@@ -131,6 +113,24 @@ declare function generateBadge(url: string): {
|
|
|
131
113
|
html: string;
|
|
132
114
|
};
|
|
133
115
|
|
|
116
|
+
declare function computeGrade(score: number): Grade;
|
|
117
|
+
declare function computeScore(tests: TestResult[]): {
|
|
118
|
+
score: number;
|
|
119
|
+
grade: Grade;
|
|
120
|
+
overall: "pass" | "partial" | "fail";
|
|
121
|
+
summary: {
|
|
122
|
+
total: number;
|
|
123
|
+
passed: number;
|
|
124
|
+
failed: number;
|
|
125
|
+
required: number;
|
|
126
|
+
requiredPassed: number;
|
|
127
|
+
};
|
|
128
|
+
categories: Record<string, {
|
|
129
|
+
passed: number;
|
|
130
|
+
total: number;
|
|
131
|
+
}>;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
134
|
/**
|
|
135
135
|
* Parse a Server-Sent Events response body and extract the first
|
|
136
136
|
* JSON-RPC response message. Returns null if none found.
|
package/dist/runner.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/mcp-compliance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"mcpName": "io.github.YawLabs/mcp-compliance",
|
|
5
5
|
"description": "CLI tool and MCP server that tests MCP servers for spec compliance",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"main": "./dist/runner.js",
|
|
21
21
|
"types": "./dist/runner.d.ts",
|
|
22
22
|
"bin": {
|
|
23
|
-
"mcp-compliance": "
|
|
23
|
+
"mcp-compliance": "dist/index.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
@@ -45,17 +45,17 @@
|
|
|
45
45
|
"chalk": "^5.4.1",
|
|
46
46
|
"commander": "^14.0.3",
|
|
47
47
|
"undici": "^7.8.0",
|
|
48
|
-
"zod": "^
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@biomejs/biome": "^
|
|
51
|
+
"@biomejs/biome": "^2.4.16",
|
|
52
52
|
"@types/node": "^25.5.2",
|
|
53
53
|
"ajv": "^8.18.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
55
55
|
"tsup": "^8.4.0",
|
|
56
56
|
"tsx": "^4.21.0",
|
|
57
|
-
"typescript": "^
|
|
58
|
-
"vitest": "^
|
|
57
|
+
"typescript": "^6.0.3",
|
|
58
|
+
"vitest": "^4.1.8"
|
|
59
59
|
},
|
|
60
60
|
"engines": {
|
|
61
61
|
"node": ">=20"
|