@yawlabs/mcp-compliance 0.14.4 → 0.15.1

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.
@@ -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 gracePeriodMs = 2e3;
403
- await new Promise((resolve) => {
404
- const timer = setTimeout(() => {
422
+ const treeKill = (force) => {
423
+ if (isWindows && child.pid !== void 0) {
405
424
  try {
406
- child.kill("SIGKILL");
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
- try {
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 _require = createRequire(import.meta.url);
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_.\-]+$/.test(tool.name)) {
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`);
@@ -3183,7 +3209,7 @@ async function runComplianceSuite(target, options = {}) {
3183
3209
  return { passed: true, details: `HTTP ${res.statusCode} (unauthenticated request rejected)` };
3184
3210
  }
3185
3211
  return { passed: false, details: `HTTP ${res.statusCode} \u2014 server accepted unauthenticated request` };
3186
- } catch (err) {
3212
+ } catch (_err) {
3187
3213
  return { passed: true, details: "Connection rejected (acceptable)" };
3188
3214
  }
3189
3215
  }
@@ -3231,7 +3257,7 @@ async function runComplianceSuite(target, options = {}) {
3231
3257
  "basic/authorization",
3232
3258
  async () => {
3233
3259
  if (!hasAuth) {
3234
- return { passed: false, details: "Skipped: server does not require auth" };
3260
+ return { passed: true, details: "Skipped: server does not require auth" };
3235
3261
  }
3236
3262
  const malformedHeaders = {
3237
3263
  Authorization: "Bearer INVALID_GARBAGE_TOKEN_!@#$%^&*()"
@@ -3245,7 +3271,7 @@ async function runComplianceSuite(target, options = {}) {
3245
3271
  return { passed: true, details: `HTTP ${res.statusCode} (malformed auth rejected)` };
3246
3272
  }
3247
3273
  return { passed: false, details: `HTTP ${res.statusCode} \u2014 server accepted malformed auth token` };
3248
- } catch (err) {
3274
+ } catch (_err) {
3249
3275
  return { passed: true, details: "Connection rejected (acceptable)" };
3250
3276
  }
3251
3277
  }
@@ -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: { __injected_param__: "malicious_value", __proto__: { admin: true } }
3765
+ arguments: maliciousArgs
3739
3766
  });
3740
3767
  const error = res.body?.error;
3741
3768
  if (error) {
@@ -3757,7 +3784,7 @@ async function runComplianceSuite(target, options = {}) {
3757
3784
  if (!toolsListOk) return { passed: true, details: "Skipped: tools/list not available" };
3758
3785
  const tools = cachedToolsList ?? [];
3759
3786
  if (tools.length === 0) return { passed: true, details: "No tools to validate" };
3760
- const missing = tools.filter((t) => !t.inputSchema || t.inputSchema.type !== "object");
3787
+ const missing = tools.filter((t) => t.inputSchema?.type !== "object");
3761
3788
  if (missing.length > 0) {
3762
3789
  return {
3763
3790
  passed: false,
@@ -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 as createRequire2 } from "module";
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((resolve3, reject) => {
241
+ const spawnReady = new Promise((resolve2, reject) => {
242
242
  child.once("spawn", () => {
243
243
  spawned = true;
244
- resolve3();
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((resolve3, reject) => {
340
+ return new Promise((resolve2, reject) => {
341
341
  stdin.write(`${line}
342
- `, "utf8", (err) => err ? reject(err) : resolve3());
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((resolve3, reject) => {
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: resolve3, reject, id, timer });
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 gracePeriodMs = 2e3;
393
- await new Promise((resolve3) => {
394
- const timer = setTimeout(() => {
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
- resolve3();
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
- resolve3();
413
+ resolve2();
404
414
  });
405
- try {
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
- const arrow = summary.baselineGrade === summary.currentGrade ? "\u2192" : "\u2192";
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 { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "fs";
710
- import { basename, dirname, join as join2, resolve as resolve2 } from "path";
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 _require = createRequire(import.meta.url);
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_.\-]+$/.test(tool.name)) {
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`);
@@ -3540,7 +3568,7 @@ async function runComplianceSuite(target, options = {}) {
3540
3568
  return { passed: true, details: `HTTP ${res.statusCode} (unauthenticated request rejected)` };
3541
3569
  }
3542
3570
  return { passed: false, details: `HTTP ${res.statusCode} \u2014 server accepted unauthenticated request` };
3543
- } catch (err) {
3571
+ } catch (_err) {
3544
3572
  return { passed: true, details: "Connection rejected (acceptable)" };
3545
3573
  }
3546
3574
  }
@@ -3588,7 +3616,7 @@ async function runComplianceSuite(target, options = {}) {
3588
3616
  "basic/authorization",
3589
3617
  async () => {
3590
3618
  if (!hasAuth) {
3591
- return { passed: false, details: "Skipped: server does not require auth" };
3619
+ return { passed: true, details: "Skipped: server does not require auth" };
3592
3620
  }
3593
3621
  const malformedHeaders = {
3594
3622
  Authorization: "Bearer INVALID_GARBAGE_TOKEN_!@#$%^&*()"
@@ -3602,7 +3630,7 @@ async function runComplianceSuite(target, options = {}) {
3602
3630
  return { passed: true, details: `HTTP ${res.statusCode} (malformed auth rejected)` };
3603
3631
  }
3604
3632
  return { passed: false, details: `HTTP ${res.statusCode} \u2014 server accepted malformed auth token` };
3605
- } catch (err) {
3633
+ } catch (_err) {
3606
3634
  return { passed: true, details: "Connection rejected (acceptable)" };
3607
3635
  }
3608
3636
  }
@@ -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: { __injected_param__: "malicious_value", __proto__: { admin: true } }
4124
+ arguments: maliciousArgs
4096
4125
  });
4097
4126
  const error = res.body?.error;
4098
4127
  if (error) {
@@ -4114,7 +4143,7 @@ async function runComplianceSuite(target, options = {}) {
4114
4143
  if (!toolsListOk) return { passed: true, details: "Skipped: tools/list not available" };
4115
4144
  const tools = cachedToolsList ?? [];
4116
4145
  if (tools.length === 0) return { passed: true, details: "No tools to validate" };
4117
- const missing = tools.filter((t) => !t.inputSchema || t.inputSchema.type !== "object");
4146
+ const missing = tools.filter((t) => t.inputSchema?.type !== "object");
4118
4147
  if (missing.length > 0) {
4119
4148
  return {
4120
4149
  passed: false,
@@ -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
- function findPackageVersion() {
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(fileURLToPath(import.meta.url));
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(dirname(selfPath));
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 dirname2, join as join3 } from "path";
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 createHash2("sha256").update(url).digest("hex").slice(0, 24);
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 = dirname2(STORE_PATH);
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 = createRequire2(import.meta.url);
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
- if (host === "::1" || host === "[::1]") return true;
5452
- if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) return true;
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").choices(["terminal", "json", "sarif", "github", "markdown", "html"]).default("terminal")
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 milliseconds (applies to every test request after the initial handshake)",
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", "0").option(
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: parsePositiveInt(opts.timeout, "--timeout", 1),
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: parsePositiveInt(opts.retries, "--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", "15000").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(
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: parsePositiveInt(opts.timeout, "--timeout", 1)
5741
+ timeout: badgeTimeout
5719
5742
  });
5720
5743
  let markdown = report.badge.markdown;
5721
5744
  if (shouldPublish && transportTarget.type === "http") {
@@ -5775,7 +5798,7 @@ Error: ${message}
5775
5798
  }
5776
5799
  }
5777
5800
  );
5778
- program.command("unpublish").description("Remove a previously-published compliance report from mcp.hosting").argument("<url>", "MCP server URL whose report should be removed").action(async (url) => {
5801
+ program.command("unpublish").description("Remove a previously-published compliance report from mcp.hosting").argument("<url>", "MCP server URL whose report should be removed").option("-y, --yes", "Skip the confirmation prompt (for automation)").action(async (url, opts) => {
5779
5802
  try {
5780
5803
  const stored = getTokenForUrl(url);
5781
5804
  if (!stored) {
@@ -5788,6 +5811,13 @@ No delete token found locally for ${url} \u2014 nothing to unpublish from this m
5788
5811
  );
5789
5812
  return;
5790
5813
  }
5814
+ if (!opts.yes) {
5815
+ const ok = await promptYesNo(chalk2.yellow(`Remove the published report for ${url}? This cannot be undone.`));
5816
+ if (!ok) {
5817
+ console.error(chalk2.dim("\nAborted. Re-run with --yes to skip this prompt.\n"));
5818
+ return;
5819
+ }
5820
+ }
5791
5821
  await unpublishReport(stored.hash, stored.entry.deleteToken);
5792
5822
  deleteToken(stored.hash);
5793
5823
  console.log(chalk2.green(`
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  SPEC_BASE,
3
3
  TEST_DEFINITIONS,
4
+ readPackageVersion,
4
5
  runComplianceSuite
5
- } from "../chunk-2CXRMEZ3.js";
6
+ } from "../chunk-B5HSDK4K.js";
6
7
 
7
8
  // src/mcp/server.ts
8
- import { existsSync, readFileSync, realpathSync } from "fs";
9
- import { basename, dirname, join, resolve } from "path";
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
- function findPackageVersion() {
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
@@ -10,7 +10,7 @@ import {
10
10
  previewTests,
11
11
  runComplianceSuite,
12
12
  urlHash
13
- } from "./chunk-2CXRMEZ3.js";
13
+ } from "./chunk-B5HSDK4K.js";
14
14
  export {
15
15
  SPEC_BASE,
16
16
  SPEC_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp-compliance",
3
- "version": "0.14.4",
3
+ "version": "0.15.1",
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": "^3.24.4"
48
+ "zod": "^4.4.3"
49
49
  },
50
50
  "devDependencies": {
51
- "@biomejs/biome": "^1.9.4",
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": "^5.8.3",
58
- "vitest": "^3.1.1"
57
+ "typescript": "^6.0.3",
58
+ "vitest": "^4.1.8"
59
59
  },
60
60
  "engines": {
61
61
  "node": ">=20"