@yawlabs/mcp-compliance 0.13.4 → 0.13.5

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.
Files changed (2) hide show
  1. package/dist/index.js +138 -86
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4772,18 +4772,25 @@ var CATEGORY_LABELS = {
4772
4772
  security: "Security"
4773
4773
  };
4774
4774
  var CATEGORY_ORDER = ["transport", "lifecycle", "tools", "resources", "prompts", "errors", "schema", "security"];
4775
+ var GRADE_ART = {
4776
+ A: [" \u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u255D"],
4777
+ B: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D", "\u255A\u2550\u2550\u2550\u2550\u2550\u255D "],
4778
+ C: [" \u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D", "\u2588\u2588\u2551 ", "\u2588\u2588\u2551 ", "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557", " \u255A\u2550\u2550\u2550\u2550\u2550\u255D"],
4779
+ D: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D", "\u255A\u2550\u2550\u2550\u2550\u2550\u255D "],
4780
+ F: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D", "\u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u255D ", "\u2588\u2588\u2551 ", "\u255A\u2550\u255D "]
4781
+ };
4775
4782
  function gradeColor(grade) {
4776
4783
  switch (grade) {
4777
4784
  case "A":
4778
- return chalk.green.bold(grade);
4785
+ return (s) => chalk.green.bold(s);
4779
4786
  case "B":
4780
- return chalk.greenBright.bold(grade);
4787
+ return (s) => chalk.greenBright.bold(s);
4781
4788
  case "C":
4782
- return chalk.yellow.bold(grade);
4789
+ return (s) => chalk.yellow.bold(s);
4783
4790
  case "D":
4784
- return chalk.rgb(255, 165, 0).bold(grade);
4791
+ return (s) => chalk.rgb(255, 165, 0).bold(s);
4785
4792
  case "F":
4786
- return chalk.red.bold(grade);
4793
+ return (s) => chalk.red.bold(s);
4787
4794
  }
4788
4795
  }
4789
4796
  function overallColor(overall) {
@@ -4798,104 +4805,136 @@ function overallColor(overall) {
4798
4805
  return overall;
4799
4806
  }
4800
4807
  }
4801
- function testLine(t) {
4802
- const icon = t.passed ? chalk.green(" PASS") : chalk.red(" FAIL");
4803
- const req = t.required ? chalk.dim(" (required)") : "";
4804
- const dur = chalk.dim(` ${t.durationMs}ms`);
4805
- let line = `${icon} ${t.name}${req}${dur}
4806
- ${chalk.dim(` ${t.details}`)}`;
4807
- if (!t.passed) {
4808
- const def = TEST_DEFINITIONS.find((d) => d.id === t.id);
4809
- if (def?.recommendation) {
4810
- line += `
4811
- ${chalk.cyan(` Fix: ${def.recommendation}`)}`;
4812
- }
4813
- }
4814
- return line;
4808
+ function makeBar(passed, total, width = 24) {
4809
+ if (total === 0) return { filled: "", rest: "\u2500".repeat(width) };
4810
+ const n = Math.max(0, Math.min(width, Math.round(passed / total * width)));
4811
+ return { filled: "\u2588".repeat(n), rest: "\u2591".repeat(width - n) };
4815
4812
  }
4813
+ function barColor(passed, total) {
4814
+ if (total === 0) return (s) => chalk.dim(s);
4815
+ const pct2 = passed / total;
4816
+ if (pct2 >= 1) return (s) => chalk.green(s);
4817
+ if (pct2 >= 0.85) return (s) => chalk.greenBright(s);
4818
+ if (pct2 >= 0.6) return (s) => chalk.yellow(s);
4819
+ return (s) => chalk.red(s);
4820
+ }
4821
+ function padRight(s, n) {
4822
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
4823
+ }
4824
+ function padLeft(s, n) {
4825
+ return s.length >= n ? s : " ".repeat(n - s.length) + s;
4826
+ }
4827
+ var RULE = "\u2500".repeat(62);
4828
+ var HEAVY_RULE = "\u2550".repeat(62);
4816
4829
  function formatTerminal(report) {
4817
- const lines = [];
4818
- lines.push("");
4819
- lines.push(chalk.bold("MCP Compliance Report"));
4820
- lines.push(chalk.dim(`Spec: ${report.specVersion} | Tool: v${report.toolVersion} | ${report.timestamp}`));
4821
- lines.push(chalk.dim(`URL: ${report.url}`));
4830
+ const out = [];
4831
+ const color = gradeColor(report.grade);
4832
+ const art = GRADE_ART[report.grade];
4833
+ out.push("");
4834
+ out.push(chalk.bold(" MCP COMPLIANCE REPORT"));
4835
+ out.push(chalk.dim(` ${HEAVY_RULE}`));
4822
4836
  if (report.serverInfo.name) {
4823
- lines.push(
4824
- chalk.dim(
4825
- `Server: ${report.serverInfo.name} v${report.serverInfo.version || "?"} (protocol ${report.serverInfo.protocolVersion || "?"})`
4826
- )
4827
- );
4837
+ const v = report.serverInfo.version ? ` v${report.serverInfo.version}` : "";
4838
+ const proto = report.serverInfo.protocolVersion ? ` (protocol ${report.serverInfo.protocolVersion})` : "";
4839
+ out.push(chalk.dim(` Server: ${report.serverInfo.name}${v}${proto}`));
4828
4840
  }
4829
- lines.push("");
4830
- lines.push(
4831
- ` Grade: ${gradeColor(report.grade)} Score: ${chalk.bold(String(report.score))}% Overall: ${overallColor(report.overall)}`
4832
- );
4833
- lines.push(
4834
- ` Tests: ${chalk.green(String(report.summary.passed))} passed / ${chalk.red(String(report.summary.failed))} failed / ${report.summary.total} total`
4835
- );
4836
- lines.push(` Required: ${report.summary.requiredPassed}/${report.summary.required} passed`);
4837
- const grouped = {};
4838
- for (const t of report.tests) {
4839
- if (!grouped[t.category]) grouped[t.category] = [];
4840
- grouped[t.category].push(t);
4841
+ out.push(chalk.dim(` Target: ${report.url}`));
4842
+ out.push(chalk.dim(` Spec: ${report.specVersion} \xB7 Tool v${report.toolVersion} \xB7 ${report.timestamp}`));
4843
+ out.push("");
4844
+ const reqOk = report.summary.requiredPassed === report.summary.required;
4845
+ const infoRows = [
4846
+ "",
4847
+ "",
4848
+ `${chalk.bold("GRADE")} ${color(report.grade)} ${chalk.bold(`${report.score}%`)}`,
4849
+ `${chalk.dim("Overall ")}${overallColor(report.overall)}`,
4850
+ `${chalk.dim("Tests ")}${chalk.green(String(report.summary.passed))}${chalk.dim("/")}${report.summary.total}${report.summary.failed > 0 ? chalk.dim(" (") + chalk.red(`${report.summary.failed} failed`) + chalk.dim(")") : ""}`,
4851
+ `${chalk.dim("Required ")}${reqOk ? chalk.green(`${report.summary.requiredPassed}/${report.summary.required} \u2713`) : chalk.red(`${report.summary.requiredPassed}/${report.summary.required}`)}`
4852
+ ];
4853
+ for (let i = 0; i < 6; i++) {
4854
+ out.push(` ${color(art[i])} ${infoRows[i] || ""}`);
4841
4855
  }
4842
- for (const cat of CATEGORY_ORDER) {
4843
- const catTests = grouped[cat];
4844
- if (!catTests || catTests.length === 0) continue;
4845
- const catStats = report.categories[cat];
4856
+ out.push("");
4857
+ out.push(chalk.bold(" CATEGORY BREAKDOWN"));
4858
+ out.push(chalk.dim(` ${RULE}`));
4859
+ const cats = CATEGORY_ORDER.filter((c) => report.categories[c] && report.categories[c].total > 0);
4860
+ const maxLabel = Math.max(...cats.map((c) => (CATEGORY_LABELS[c] || c).length));
4861
+ for (const cat of cats) {
4862
+ const stats = report.categories[cat];
4846
4863
  const label = CATEGORY_LABELS[cat] || cat;
4847
- const catColor = catStats && catStats.passed === catStats.total ? chalk.green : chalk.yellow;
4848
- lines.push("");
4849
- lines.push(catColor(` ${label} (${catStats?.passed || 0}/${catStats?.total || 0})`));
4850
- for (const t of catTests) {
4851
- lines.push(testLine(t));
4852
- }
4853
- }
4854
- const caps = report.serverInfo.capabilities;
4855
- const declared = Object.keys(caps).filter((k) => caps[k] !== void 0);
4856
- if (declared.length > 0) {
4857
- lines.push("");
4858
- lines.push(chalk.dim(` Capabilities: ${declared.join(", ")}`));
4859
- }
4860
- if (report.toolCount > 0) {
4861
- lines.push(
4862
- chalk.dim(
4863
- ` Tools (${report.toolCount}): ${report.toolNames.slice(0, 10).join(", ")}${report.toolCount > 10 ? "..." : ""}`
4864
- )
4864
+ const { filled, rest } = makeBar(stats.passed, stats.total, 24);
4865
+ const colorFn = barColor(stats.passed, stats.total);
4866
+ const pct2 = stats.total === 0 ? 0 : Math.round(stats.passed / stats.total * 100);
4867
+ const ratio = `${stats.passed}/${stats.total}`;
4868
+ out.push(
4869
+ ` ${padRight(label, maxLabel)} ${colorFn(filled)}${chalk.dim(rest)} ${padLeft(ratio, 7)} ${padLeft(`${pct2}%`, 4)}`
4865
4870
  );
4866
4871
  }
4867
- if (report.resourceCount > 0) {
4868
- lines.push(
4869
- chalk.dim(
4870
- ` Resources (${report.resourceCount}): ${report.resourceNames.slice(0, 10).join(", ")}${report.resourceCount > 10 ? "..." : ""}`
4871
- )
4872
- );
4873
- }
4874
- if (report.promptCount > 0) {
4875
- lines.push(
4876
- chalk.dim(
4877
- ` Prompts (${report.promptCount}): ${report.promptNames.slice(0, 10).join(", ")}${report.promptCount > 10 ? "..." : ""}`
4878
- )
4879
- );
4872
+ out.push("");
4873
+ const failed = report.tests.filter((t) => !t.passed);
4874
+ if (failed.length > 0) {
4875
+ out.push(chalk.bold.red(` FAILED TESTS (${failed.length})`));
4876
+ out.push(chalk.dim(` ${RULE}`));
4877
+ for (const t of failed) {
4878
+ const req = t.required ? chalk.red("required") : chalk.dim("optional");
4879
+ out.push(
4880
+ ` ${chalk.red("\u2717")} ${chalk.bold(t.name)} ${chalk.dim(`[${t.id}]`)} ${req} ${chalk.dim(`${t.durationMs}ms`)}`
4881
+ );
4882
+ out.push(` ${t.details}`);
4883
+ const def = TEST_DEFINITIONS.find((d) => d.id === t.id);
4884
+ if (def?.recommendation) {
4885
+ out.push(` ${chalk.cyan(`\u2192 ${def.recommendation}`)}`);
4886
+ }
4887
+ if (t.specRef) {
4888
+ out.push(chalk.dim(` spec: ${t.specRef}`));
4889
+ }
4890
+ out.push("");
4891
+ }
4892
+ } else {
4893
+ out.push(` ${chalk.green.bold("\u2713 All tests passed")}`);
4894
+ out.push("");
4880
4895
  }
4881
4896
  if (report.warnings.length > 0) {
4882
- lines.push("");
4883
- lines.push(chalk.yellow(` Warnings (${report.warnings.length}):`));
4897
+ out.push(chalk.bold.yellow(` WARNINGS (${report.warnings.length})`));
4898
+ out.push(chalk.dim(` ${RULE}`));
4884
4899
  for (const w of report.warnings) {
4885
- lines.push(chalk.yellow(` - ${w}`));
4900
+ out.push(` ${chalk.yellow("!")} ${w}`);
4886
4901
  }
4902
+ out.push("");
4903
+ }
4904
+ const caps = report.serverInfo.capabilities;
4905
+ const declared = Object.keys(caps).filter((k) => caps[k] !== void 0);
4906
+ const hasContext = declared.length > 0 || report.toolCount > 0 || report.resourceCount > 0 || report.promptCount > 0;
4907
+ if (hasContext) {
4908
+ out.push(chalk.bold(" SERVER CONTEXT"));
4909
+ out.push(chalk.dim(` ${RULE}`));
4910
+ if (declared.length > 0) {
4911
+ out.push(chalk.dim(` Capabilities: ${declared.join(", ")}`));
4912
+ }
4913
+ if (report.toolCount > 0) {
4914
+ const more = report.toolCount > 10 ? ", ..." : "";
4915
+ out.push(chalk.dim(` Tools (${report.toolCount}): ${report.toolNames.slice(0, 10).join(", ")}${more}`));
4916
+ }
4917
+ if (report.resourceCount > 0) {
4918
+ const more = report.resourceCount > 10 ? ", ..." : "";
4919
+ out.push(
4920
+ chalk.dim(` Resources (${report.resourceCount}): ${report.resourceNames.slice(0, 10).join(", ")}${more}`)
4921
+ );
4922
+ }
4923
+ if (report.promptCount > 0) {
4924
+ const more = report.promptCount > 10 ? ", ..." : "";
4925
+ out.push(chalk.dim(` Prompts (${report.promptCount}): ${report.promptNames.slice(0, 10).join(", ")}${more}`));
4926
+ }
4927
+ out.push("");
4887
4928
  }
4888
- lines.push("");
4889
4929
  if (report.url.startsWith("stdio:")) {
4890
- lines.push(
4891
- chalk.dim(" Badge: stdio servers can't be published. Use `--output badge.svg` for a local badge image.")
4930
+ out.push(
4931
+ chalk.dim(" Badge: stdio targets aren't published. Run with --output badge.svg for a local badge image.")
4892
4932
  );
4893
4933
  } else {
4894
- lines.push(chalk.dim(" Badge markdown:"));
4895
- lines.push(` ${report.badge.markdown}`);
4934
+ out.push(chalk.dim(` Badge: ${report.badge.markdown}`));
4896
4935
  }
4897
- lines.push("");
4898
- return lines.join("\n");
4936
+ out.push("");
4937
+ return out.join("\n");
4899
4938
  }
4900
4939
  function formatJson(report) {
4901
4940
  return JSON.stringify(report, null, 2);
@@ -5271,6 +5310,16 @@ function parsePositiveInt(value, name, min = 0) {
5271
5310
  function parseList(value) {
5272
5311
  return value.split(",").map((s) => s.trim()).filter(Boolean);
5273
5312
  }
5313
+ function ttyHint(format) {
5314
+ if (!process.stdout.isTTY) return;
5315
+ process.stderr.write(
5316
+ chalk2.dim(
5317
+ ` [tip] --format=${format} is machine-readable. Redirect with \`> report.${format === "sarif" ? "sarif" : format}\` or drop --format for the human report.
5318
+
5319
+ `
5320
+ )
5321
+ );
5322
+ }
5274
5323
  function parseEnvVar(value, prev) {
5275
5324
  const idx = value.indexOf("=");
5276
5325
  if (idx === -1) throw new Error(`Invalid env var: "${value}" (expected "KEY=VALUE")`);
@@ -5503,14 +5552,17 @@ Testing ${describeTarget(transportTarget)}...
5503
5552
  console.log("");
5504
5553
  }
5505
5554
  if (opts.format === "json") {
5555
+ ttyHint("json");
5506
5556
  console.log(formatJson(report2));
5507
5557
  } else if (opts.format === "sarif") {
5558
+ ttyHint("sarif");
5508
5559
  console.log(formatSarif(report2));
5509
5560
  } else if (opts.format === "github") {
5510
5561
  console.log(formatGithub(report2));
5511
5562
  } else if (opts.format === "markdown") {
5512
5563
  console.log(formatMarkdown(report2));
5513
5564
  } else if (opts.format === "html") {
5565
+ ttyHint("html");
5514
5566
  console.log(formatHtml(report2));
5515
5567
  } else {
5516
5568
  console.log(formatTerminal(report2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp-compliance",
3
- "version": "0.13.4",
3
+ "version": "0.13.5",
4
4
  "description": "CLI tool and MCP server that tests MCP servers for spec compliance",
5
5
  "license": "MIT",
6
6
  "author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",