@yawlabs/mcp-compliance 0.14.4 → 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/dist/{chunk-2CXRMEZ3.js → chunk-A3UG3J63.js} +42 -14
- package/dist/index.js +90 -67
- package/dist/mcp/server.js +7 -24
- package/dist/runner.d.ts +18 -18
- package/dist/runner.js +1 -1
- package/package.json +5 -5
|
@@ -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
|
|
|
@@ -399,23 +419,30 @@ function createStdioTransport(opts) {
|
|
|
399
419
|
child.stdin?.end();
|
|
400
420
|
} catch {
|
|
401
421
|
}
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
const timer = setTimeout(() => {
|
|
422
|
+
const treeKill = (force) => {
|
|
423
|
+
if (isWindows && child.pid !== void 0) {
|
|
405
424
|
try {
|
|
406
|
-
child.
|
|
425
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", ...force ? ["/f"] : []], { stdio: "ignore" });
|
|
407
426
|
} catch {
|
|
408
427
|
}
|
|
428
|
+
} else {
|
|
429
|
+
try {
|
|
430
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const gracePeriodMs = 2e3;
|
|
436
|
+
await new Promise((resolve) => {
|
|
437
|
+
const timer = setTimeout(() => {
|
|
438
|
+
treeKill(true);
|
|
409
439
|
resolve();
|
|
410
440
|
}, gracePeriodMs);
|
|
411
441
|
child.once("exit", () => {
|
|
412
442
|
clearTimeout(timer);
|
|
413
443
|
resolve();
|
|
414
444
|
});
|
|
415
|
-
|
|
416
|
-
child.kill(isWindows ? void 0 : "SIGTERM");
|
|
417
|
-
} catch {
|
|
418
|
-
}
|
|
445
|
+
treeKill(false);
|
|
419
446
|
});
|
|
420
447
|
rejectAllPending(new Error("stdio transport: closed"));
|
|
421
448
|
},
|
|
@@ -1254,8 +1281,7 @@ var TEST_DEFINITIONS = [
|
|
|
1254
1281
|
|
|
1255
1282
|
// src/runner.ts
|
|
1256
1283
|
var TEST_DEFINITIONS_MAP = new Map(TEST_DEFINITIONS.map((t) => [t.id, t]));
|
|
1257
|
-
var
|
|
1258
|
-
var { version: TOOL_VERSION } = _require("../package.json");
|
|
1284
|
+
var TOOL_VERSION = readPackageVersion(import.meta.url);
|
|
1259
1285
|
var SPEC_VERSION = "2025-11-25";
|
|
1260
1286
|
var SPEC_BASE = `https://modelcontextprotocol.io/specification/${SPEC_VERSION}`;
|
|
1261
1287
|
var VALID_CONTENT_TYPES = ["text", "image", "audio", "resource", "resource_link"];
|
|
@@ -2475,7 +2501,7 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
2475
2501
|
issues.push("Tool missing name");
|
|
2476
2502
|
continue;
|
|
2477
2503
|
}
|
|
2478
|
-
if (tool.name.length > 128 || !/^[A-Za-z0-9_
|
|
2504
|
+
if (tool.name.length > 128 || !/^[A-Za-z0-9_.-]+$/.test(tool.name)) {
|
|
2479
2505
|
issues.push(`${tool.name}: name format invalid`);
|
|
2480
2506
|
}
|
|
2481
2507
|
if (!tool.description) warnings.push(`Tool "${tool.name}" missing description`);
|
|
@@ -3231,7 +3257,7 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3231
3257
|
"basic/authorization",
|
|
3232
3258
|
async () => {
|
|
3233
3259
|
if (!hasAuth) {
|
|
3234
|
-
return { passed:
|
|
3260
|
+
return { passed: true, details: "Skipped: server does not require auth" };
|
|
3235
3261
|
}
|
|
3236
3262
|
const malformedHeaders = {
|
|
3237
3263
|
Authorization: "Bearer INVALID_GARBAGE_TOKEN_!@#$%^&*()"
|
|
@@ -3733,9 +3759,10 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3733
3759
|
return { passed: true, details: "No tools available to test (skipped)" };
|
|
3734
3760
|
}
|
|
3735
3761
|
try {
|
|
3762
|
+
const maliciousArgs = JSON.parse('{"__injected_param__":"malicious_value","__proto__":{"admin":true}}');
|
|
3736
3763
|
const res = await rpc("tools/call", {
|
|
3737
3764
|
name: toolNames[0],
|
|
3738
|
-
arguments:
|
|
3765
|
+
arguments: maliciousArgs
|
|
3739
3766
|
});
|
|
3740
3767
|
const error = res.body?.error;
|
|
3741
3768
|
if (error) {
|
|
@@ -4139,6 +4166,7 @@ export {
|
|
|
4139
4166
|
generateBadge,
|
|
4140
4167
|
computeGrade,
|
|
4141
4168
|
computeScore,
|
|
4169
|
+
readPackageVersion,
|
|
4142
4170
|
parseSSEResponse,
|
|
4143
4171
|
TEST_DEFINITIONS,
|
|
4144
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";
|
|
@@ -238,10 +238,10 @@ function createStdioTransport(opts) {
|
|
|
238
238
|
const pending = /* @__PURE__ */ new Map();
|
|
239
239
|
let stdoutBuffer = "";
|
|
240
240
|
let stderrBuffer = "";
|
|
241
|
-
const spawnReady = new Promise((
|
|
241
|
+
const spawnReady = new Promise((resolve2, reject) => {
|
|
242
242
|
child.once("spawn", () => {
|
|
243
243
|
spawned = true;
|
|
244
|
-
|
|
244
|
+
resolve2();
|
|
245
245
|
});
|
|
246
246
|
child.once("error", (err) => {
|
|
247
247
|
if (!spawned) reject(err);
|
|
@@ -337,9 +337,9 @@ function createStdioTransport(opts) {
|
|
|
337
337
|
if (spawnError) throw new Error(annotateWithStderr(`stdio transport: spawn failed \u2014 ${spawnError.message}`));
|
|
338
338
|
const stdin = child.stdin;
|
|
339
339
|
if (!stdin || stdin.destroyed) throw new Error(annotateWithStderr("stdio transport: stdin is closed"));
|
|
340
|
-
return new Promise((
|
|
340
|
+
return new Promise((resolve2, reject) => {
|
|
341
341
|
stdin.write(`${line}
|
|
342
|
-
`, "utf8", (err) => err ? reject(err) :
|
|
342
|
+
`, "utf8", (err) => err ? reject(err) : resolve2());
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
345
|
const transport = {
|
|
@@ -361,7 +361,7 @@ function createStdioTransport(opts) {
|
|
|
361
361
|
async request(method, params, nextId, init) {
|
|
362
362
|
const id = nextId();
|
|
363
363
|
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
364
|
-
return new Promise((
|
|
364
|
+
return new Promise((resolve2, reject) => {
|
|
365
365
|
const timer = setTimeout(() => {
|
|
366
366
|
pending.delete(id);
|
|
367
367
|
reject(
|
|
@@ -370,7 +370,7 @@ function createStdioTransport(opts) {
|
|
|
370
370
|
)
|
|
371
371
|
);
|
|
372
372
|
}, init.timeout);
|
|
373
|
-
pending.set(id, { resolve:
|
|
373
|
+
pending.set(id, { resolve: resolve2, reject, id, timer });
|
|
374
374
|
writeLine(body).catch((err) => {
|
|
375
375
|
clearTimeout(timer);
|
|
376
376
|
pending.delete(id);
|
|
@@ -389,23 +389,30 @@ function createStdioTransport(opts) {
|
|
|
389
389
|
child.stdin?.end();
|
|
390
390
|
} catch {
|
|
391
391
|
}
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
392
|
+
const treeKill = (force) => {
|
|
393
|
+
if (isWindows && child.pid !== void 0) {
|
|
394
|
+
try {
|
|
395
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", ...force ? ["/f"] : []], { stdio: "ignore" });
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
395
399
|
try {
|
|
396
|
-
child.kill("SIGKILL");
|
|
400
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
397
401
|
} catch {
|
|
398
402
|
}
|
|
399
|
-
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const gracePeriodMs = 2e3;
|
|
406
|
+
await new Promise((resolve2) => {
|
|
407
|
+
const timer = setTimeout(() => {
|
|
408
|
+
treeKill(true);
|
|
409
|
+
resolve2();
|
|
400
410
|
}, gracePeriodMs);
|
|
401
411
|
child.once("exit", () => {
|
|
402
412
|
clearTimeout(timer);
|
|
403
|
-
|
|
413
|
+
resolve2();
|
|
404
414
|
});
|
|
405
|
-
|
|
406
|
-
child.kill(isWindows ? void 0 : "SIGTERM");
|
|
407
|
-
} catch {
|
|
408
|
-
}
|
|
415
|
+
treeKill(false);
|
|
409
416
|
});
|
|
410
417
|
rejectAllPending(new Error("stdio transport: closed"));
|
|
411
418
|
},
|
|
@@ -669,7 +676,9 @@ function diffReports(baseline, current) {
|
|
|
669
676
|
}
|
|
670
677
|
function formatDiff(summary) {
|
|
671
678
|
const lines = [];
|
|
672
|
-
|
|
679
|
+
let arrow = "\u2192";
|
|
680
|
+
if (summary.currentScore > summary.baselineScore) arrow = "\u2191";
|
|
681
|
+
else if (summary.currentScore < summary.baselineScore) arrow = "\u2193";
|
|
673
682
|
lines.push(
|
|
674
683
|
`Grade ${summary.baselineGrade} (${summary.baselineScore}%) ${arrow} ${summary.currentGrade} (${summary.currentScore}%)`
|
|
675
684
|
);
|
|
@@ -706,17 +715,37 @@ function hasRegressions(summary) {
|
|
|
706
715
|
}
|
|
707
716
|
|
|
708
717
|
// src/mcp/server.ts
|
|
709
|
-
import {
|
|
710
|
-
import { basename, dirname
|
|
711
|
-
import { fileURLToPath } from "url";
|
|
718
|
+
import { realpathSync } from "fs";
|
|
719
|
+
import { basename, dirname as dirname2 } from "path";
|
|
720
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
712
721
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
713
722
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
714
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
|
+
|
|
715
745
|
// src/mcp/tools.ts
|
|
716
746
|
import { z } from "zod";
|
|
717
747
|
|
|
718
748
|
// src/runner.ts
|
|
719
|
-
import { createRequire } from "module";
|
|
720
749
|
import { request as request2 } from "undici";
|
|
721
750
|
|
|
722
751
|
// src/badge.ts
|
|
@@ -1611,8 +1640,7 @@ var TEST_DEFINITIONS = [
|
|
|
1611
1640
|
|
|
1612
1641
|
// src/runner.ts
|
|
1613
1642
|
var TEST_DEFINITIONS_MAP = new Map(TEST_DEFINITIONS.map((t) => [t.id, t]));
|
|
1614
|
-
var
|
|
1615
|
-
var { version: TOOL_VERSION } = _require("../package.json");
|
|
1643
|
+
var TOOL_VERSION = readPackageVersion(import.meta.url);
|
|
1616
1644
|
var SPEC_VERSION = "2025-11-25";
|
|
1617
1645
|
var SPEC_BASE = `https://modelcontextprotocol.io/specification/${SPEC_VERSION}`;
|
|
1618
1646
|
var VALID_CONTENT_TYPES = ["text", "image", "audio", "resource", "resource_link"];
|
|
@@ -2832,7 +2860,7 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
2832
2860
|
issues.push("Tool missing name");
|
|
2833
2861
|
continue;
|
|
2834
2862
|
}
|
|
2835
|
-
if (tool.name.length > 128 || !/^[A-Za-z0-9_
|
|
2863
|
+
if (tool.name.length > 128 || !/^[A-Za-z0-9_.-]+$/.test(tool.name)) {
|
|
2836
2864
|
issues.push(`${tool.name}: name format invalid`);
|
|
2837
2865
|
}
|
|
2838
2866
|
if (!tool.description) warnings.push(`Tool "${tool.name}" missing description`);
|
|
@@ -3588,7 +3616,7 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
3588
3616
|
"basic/authorization",
|
|
3589
3617
|
async () => {
|
|
3590
3618
|
if (!hasAuth) {
|
|
3591
|
-
return { passed:
|
|
3619
|
+
return { passed: true, details: "Skipped: server does not require auth" };
|
|
3592
3620
|
}
|
|
3593
3621
|
const malformedHeaders = {
|
|
3594
3622
|
Authorization: "Bearer INVALID_GARBAGE_TOKEN_!@#$%^&*()"
|
|
@@ -4090,9 +4118,10 @@ async function runComplianceSuite(target, options = {}) {
|
|
|
4090
4118
|
return { passed: true, details: "No tools available to test (skipped)" };
|
|
4091
4119
|
}
|
|
4092
4120
|
try {
|
|
4121
|
+
const maliciousArgs = JSON.parse('{"__injected_param__":"malicious_value","__proto__":{"admin":true}}');
|
|
4093
4122
|
const res = await rpc("tools/call", {
|
|
4094
4123
|
name: toolNames[0],
|
|
4095
|
-
arguments:
|
|
4124
|
+
arguments: maliciousArgs
|
|
4096
4125
|
});
|
|
4097
4126
|
const error = res.body?.error;
|
|
4098
4127
|
if (error) {
|
|
@@ -4499,7 +4528,7 @@ function registerTools(server) {
|
|
|
4499
4528
|
{
|
|
4500
4529
|
url: z.string().url().describe("The MCP server URL to test (must be HTTP or HTTPS)"),
|
|
4501
4530
|
auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
|
|
4502
|
-
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"})'),
|
|
4503
4532
|
timeout: z.number().int().min(1).max(3e5).optional().describe("Request timeout in milliseconds (default: 15000, max: 300000)"),
|
|
4504
4533
|
retries: z.number().int().min(0).max(10).optional().describe("Number of retries for failed tests (default: 0, max: 10)"),
|
|
4505
4534
|
only: z.array(z.string()).optional().describe("Only run tests matching these categories or test IDs"),
|
|
@@ -4565,7 +4594,7 @@ ${JSON.stringify(report, null, 2)}` }
|
|
|
4565
4594
|
{
|
|
4566
4595
|
url: z.string().url().describe("The MCP server URL to test"),
|
|
4567
4596
|
auth: z.string().optional().describe('Authorization header value (e.g., "Bearer tok123")'),
|
|
4568
|
-
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"),
|
|
4569
4598
|
timeout: z.number().int().min(1).max(3e5).optional().describe("Request timeout in milliseconds (default: 15000, max: 300000)")
|
|
4570
4599
|
},
|
|
4571
4600
|
{
|
|
@@ -4661,25 +4690,7 @@ ${TEST_DEFINITIONS.map((t) => t.id).join(", ")}`
|
|
|
4661
4690
|
}
|
|
4662
4691
|
|
|
4663
4692
|
// src/mcp/server.ts
|
|
4664
|
-
|
|
4665
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
4666
|
-
const root = resolve2(dir, "..", "..", "..");
|
|
4667
|
-
while (dir !== root) {
|
|
4668
|
-
const pkgPath = join2(dir, "package.json");
|
|
4669
|
-
if (existsSync2(pkgPath)) {
|
|
4670
|
-
try {
|
|
4671
|
-
return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "0.0.0";
|
|
4672
|
-
} catch {
|
|
4673
|
-
return "0.0.0";
|
|
4674
|
-
}
|
|
4675
|
-
}
|
|
4676
|
-
const parent = dirname(dir);
|
|
4677
|
-
if (parent === dir) break;
|
|
4678
|
-
dir = parent;
|
|
4679
|
-
}
|
|
4680
|
-
return "0.0.0";
|
|
4681
|
-
}
|
|
4682
|
-
var version = findPackageVersion();
|
|
4693
|
+
var version = readPackageVersion(import.meta.url);
|
|
4683
4694
|
function createComplianceServer() {
|
|
4684
4695
|
const server = new McpServer({ name: "mcp-compliance", version });
|
|
4685
4696
|
registerTools(server);
|
|
@@ -4694,10 +4705,10 @@ function isInvokedDirectly() {
|
|
|
4694
4705
|
const argv1 = process.argv[1];
|
|
4695
4706
|
if (!argv1) return false;
|
|
4696
4707
|
try {
|
|
4697
|
-
const selfPath = realpathSync(
|
|
4708
|
+
const selfPath = realpathSync(fileURLToPath2(import.meta.url));
|
|
4698
4709
|
if (realpathSync(argv1) !== selfPath) return false;
|
|
4699
4710
|
const file = basename(selfPath);
|
|
4700
|
-
const parent = basename(
|
|
4711
|
+
const parent = basename(dirname2(selfPath));
|
|
4701
4712
|
return parent === "mcp" && (file === "server.js" || file === "server.ts");
|
|
4702
4713
|
} catch {
|
|
4703
4714
|
return false;
|
|
@@ -4986,8 +4997,7 @@ function formatSarif(report) {
|
|
|
4986
4997
|
{
|
|
4987
4998
|
physicalLocation: {
|
|
4988
4999
|
artifactLocation: {
|
|
4989
|
-
uri: report.url
|
|
4990
|
-
uriBaseId: "MCP_SERVER"
|
|
5000
|
+
uri: report.url
|
|
4991
5001
|
}
|
|
4992
5002
|
}
|
|
4993
5003
|
}
|
|
@@ -5251,14 +5261,13 @@ function splitStdioTarget(s) {
|
|
|
5251
5261
|
}
|
|
5252
5262
|
|
|
5253
5263
|
// src/token-store.ts
|
|
5254
|
-
import { createHash as createHash2 } from "crypto";
|
|
5255
5264
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
5256
5265
|
import { homedir } from "os";
|
|
5257
|
-
import { dirname as
|
|
5266
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
5258
5267
|
var STORE_DIR = join3(homedir(), ".mcp-compliance");
|
|
5259
5268
|
var STORE_PATH = join3(STORE_DIR, "tokens.json");
|
|
5260
5269
|
function hashUrl(url) {
|
|
5261
|
-
return
|
|
5270
|
+
return urlHash(url);
|
|
5262
5271
|
}
|
|
5263
5272
|
function readStore() {
|
|
5264
5273
|
if (!existsSync3(STORE_PATH)) return {};
|
|
@@ -5271,7 +5280,7 @@ function readStore() {
|
|
|
5271
5280
|
}
|
|
5272
5281
|
}
|
|
5273
5282
|
function writeStore(store) {
|
|
5274
|
-
const dir =
|
|
5283
|
+
const dir = dirname3(STORE_PATH);
|
|
5275
5284
|
try {
|
|
5276
5285
|
if (!existsSync3(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
|
|
5277
5286
|
writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), { mode: 384 });
|
|
@@ -5308,7 +5317,7 @@ function deleteToken(hash) {
|
|
|
5308
5317
|
}
|
|
5309
5318
|
|
|
5310
5319
|
// src/index.ts
|
|
5311
|
-
var require2 =
|
|
5320
|
+
var require2 = createRequire(import.meta.url);
|
|
5312
5321
|
var { version: version2 } = require2("../package.json");
|
|
5313
5322
|
function parseHeaderArg(value, prev) {
|
|
5314
5323
|
const idx = value.indexOf(":");
|
|
@@ -5448,8 +5457,9 @@ function isPrivateHost(urlStr) {
|
|
|
5448
5457
|
if (a === 192 && b === 168) return true;
|
|
5449
5458
|
if (a === 0) return true;
|
|
5450
5459
|
}
|
|
5451
|
-
|
|
5452
|
-
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;
|
|
5453
5463
|
return false;
|
|
5454
5464
|
}
|
|
5455
5465
|
async function promptYesNo(message) {
|
|
@@ -5468,7 +5478,14 @@ program.command("test").description("Run the full compliance test suite against
|
|
|
5468
5478
|
"[target]",
|
|
5469
5479
|
"Server URL, or command to spawn as a stdio server (optional when a config file defines 'target')"
|
|
5470
5480
|
).argument("[extraArgs...]", "Additional args passed to the stdio command").addOption(
|
|
5471
|
-
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
|
+
])
|
|
5472
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(
|
|
5473
5490
|
new Option(
|
|
5474
5491
|
"--transport <kind>",
|
|
@@ -5489,8 +5506,7 @@ program.command("test").description("Run the full compliance test suite against
|
|
|
5489
5506
|
{}
|
|
5490
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(
|
|
5491
5508
|
"--timeout <ms>",
|
|
5492
|
-
"Per-request timeout in
|
|
5493
|
-
"15000"
|
|
5509
|
+
"Per-request timeout in ms after the initial handshake (default: 15000, or `timeout` in config)"
|
|
5494
5510
|
).option(
|
|
5495
5511
|
"--startup-timeout <ms>",
|
|
5496
5512
|
"Deadline for the initial initialize handshake (default: max(--timeout, 60000); covers cold `npx` cache fetches before a stdio server starts)"
|
|
@@ -5498,7 +5514,7 @@ program.command("test").description("Run the full compliance test suite against
|
|
|
5498
5514
|
"--concurrency <n>",
|
|
5499
5515
|
"Max parallel-safe tests in flight (default 1; see docs/PERFORMANCE.md before raising)",
|
|
5500
5516
|
"1"
|
|
5501
|
-
).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(
|
|
5502
5518
|
"--only <items>",
|
|
5503
5519
|
'Only run matching categories or test IDs, comma-separated (e.g., "transport,lifecycle" or "transport-post,lifecycle-init")',
|
|
5504
5520
|
parseList
|
|
@@ -5547,6 +5563,11 @@ ${defs.length} tests would run for transport=${transportKind}`));
|
|
|
5547
5563
|
const skip = opts.skip ?? config?.skip;
|
|
5548
5564
|
const verbose = opts.verbose ?? config?.verbose;
|
|
5549
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");
|
|
5550
5571
|
async function runOnce() {
|
|
5551
5572
|
if (opts.format === "terminal") {
|
|
5552
5573
|
console.log(chalk2.dim(`
|
|
@@ -5554,10 +5575,10 @@ Testing ${describeTarget(transportTarget)}...
|
|
|
5554
5575
|
`));
|
|
5555
5576
|
}
|
|
5556
5577
|
const report2 = await runComplianceSuite(transportTarget, {
|
|
5557
|
-
timeout
|
|
5578
|
+
timeout,
|
|
5558
5579
|
startupTimeout: opts.startupTimeout ? parsePositiveInt(opts.startupTimeout, "--startup-timeout", 1) : config?.startupTimeout,
|
|
5559
5580
|
preflightTimeout: opts.preflightTimeout ? parsePositiveInt(opts.preflightTimeout, "--preflight-timeout", 1) : config?.preflightTimeout,
|
|
5560
|
-
retries
|
|
5581
|
+
retries,
|
|
5561
5582
|
concurrency: parsePositiveInt(opts.concurrency, "--concurrency", 1),
|
|
5562
5583
|
only,
|
|
5563
5584
|
skip,
|
|
@@ -5680,7 +5701,7 @@ program.command("badge").description("Run tests and publish a shareable complian
|
|
|
5680
5701
|
'Add header to all requests (format: "Key: Value", repeatable; HTTP only)',
|
|
5681
5702
|
parseHeaderArg,
|
|
5682
5703
|
{}
|
|
5683
|
-
).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(
|
|
5684
5705
|
async (target, extraArgs, opts) => {
|
|
5685
5706
|
if (opts.color === false) chalk2.level = 0;
|
|
5686
5707
|
try {
|
|
@@ -5714,8 +5735,10 @@ Warning: ${transportTarget.url} looks like a private/internal address. Publishin
|
|
|
5714
5735
|
console.log(chalk2.dim(`
|
|
5715
5736
|
Testing ${describeTarget(transportTarget)}...
|
|
5716
5737
|
`));
|
|
5738
|
+
let badgeTimeout = config?.timeout ?? 15e3;
|
|
5739
|
+
if (opts.timeout !== void 0) badgeTimeout = parsePositiveInt(opts.timeout, "--timeout", 1);
|
|
5717
5740
|
const report = await runComplianceSuite(transportTarget, {
|
|
5718
|
-
timeout:
|
|
5741
|
+
timeout: badgeTimeout
|
|
5719
5742
|
});
|
|
5720
5743
|
let markdown = report.badge.markdown;
|
|
5721
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",
|
|
@@ -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"
|