@yawlabs/mcp-compliance 0.13.1 → 0.13.2

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.
@@ -37,12 +37,28 @@ function computeScore(tests) {
37
37
  const failed = total - passed;
38
38
  const requiredTests = tests.filter((t) => t.required);
39
39
  const requiredPassed = requiredTests.filter((t) => t.passed).length;
40
- const requiredScore = requiredTests.length > 0 ? requiredPassed / requiredTests.length * 70 : 70;
41
40
  const optionalTests = tests.filter((t) => !t.required);
42
41
  const optionalPassed = optionalTests.filter((t) => t.passed).length;
43
- const optionalScore = optionalTests.length > 0 ? optionalPassed / optionalTests.length * 30 : 30;
44
- const score = Math.round(requiredScore + optionalScore);
45
- const overall = requiredPassed === requiredTests.length ? passed === total ? "pass" : "partial" : "fail";
42
+ let score;
43
+ if (total === 0) {
44
+ score = 0;
45
+ } else if (requiredTests.length === 0) {
46
+ score = Math.round(optionalPassed / optionalTests.length * 100);
47
+ } else if (optionalTests.length === 0) {
48
+ score = Math.round(requiredPassed / requiredTests.length * 100);
49
+ } else {
50
+ score = Math.round(requiredPassed / requiredTests.length * 70 + optionalPassed / optionalTests.length * 30);
51
+ }
52
+ let overall;
53
+ if (total === 0) {
54
+ overall = "fail";
55
+ } else if (requiredPassed < requiredTests.length) {
56
+ overall = "fail";
57
+ } else if (passed === total) {
58
+ overall = "pass";
59
+ } else {
60
+ overall = "partial";
61
+ }
46
62
  const categories = {};
47
63
  for (const t of tests) {
48
64
  if (!categories[t.category]) categories[t.category] = { passed: 0, total: 0 };
@@ -198,6 +214,13 @@ function createHttpTransport(opts) {
198
214
 
199
215
  // src/transport/stdio.ts
200
216
  import { spawn } from "child_process";
217
+ function exitDiagnostic(code, signal) {
218
+ if (signal) return `server terminated by signal ${signal} before completing the request`;
219
+ if (code === 0) {
220
+ return "server exited cleanly (code 0) before completing the request. This usually means the command is a one-shot CLI, not a long-running MCP stdio server. If the server needs a subcommand to start (e.g. `serve`, `mcp`, `start`), include it in the command.";
221
+ }
222
+ return `server crashed with exit code ${code} before completing the request`;
223
+ }
201
224
  function createStdioTransport(opts) {
202
225
  const { command, args = [], env, cwd, verbose = false } = opts;
203
226
  const stderrBufferSize = opts.stderrBufferSize ?? 64 * 1024;
@@ -237,8 +260,7 @@ function createStdioTransport(opts) {
237
260
  exited = true;
238
261
  exitCode = code;
239
262
  if (pending.size > 0) {
240
- const reason = signal ? `child exited (signal ${signal})` : `child exited with code ${code}`;
241
- rejectAllPending(new Error(reason));
263
+ rejectAllPending(new Error(exitDiagnostic(code, signal)));
242
264
  }
243
265
  });
244
266
  child.stdout?.setEncoding("utf8");
@@ -313,7 +335,7 @@ function createStdioTransport(opts) {
313
335
  }
314
336
  }
315
337
  if (exited) {
316
- throw new Error(annotateWithStderr(`stdio transport: child has exited (code ${exitCode})`));
338
+ throw new Error(annotateWithStderr(`stdio transport: ${exitDiagnostic(exitCode, null)}`));
317
339
  }
318
340
  if (spawnError) throw new Error(annotateWithStderr(`stdio transport: spawn failed \u2014 ${spawnError.message}`));
319
341
  const stdin = child.stdin;
@@ -4052,7 +4074,7 @@ async function runComplianceSuite(target, options = {}) {
4052
4074
  warnings.length = 0;
4053
4075
  warnings.push(...capped);
4054
4076
  const { score, grade, overall, summary, categories } = computeScore(tests);
4055
- const badge = generateBadge(displayUrl);
4077
+ const badge = displayUrl.startsWith("stdio:") ? { imageUrl: "", reportUrl: "", markdown: "", html: "" } : generateBadge(displayUrl);
4056
4078
  return {
4057
4079
  schemaVersion: REPORT_SCHEMA_VERSION,
4058
4080
  specVersion: SPEC_VERSION,
package/dist/index.js CHANGED
@@ -204,6 +204,13 @@ function createHttpTransport(opts) {
204
204
 
205
205
  // src/transport/stdio.ts
206
206
  import { spawn } from "child_process";
207
+ function exitDiagnostic(code, signal) {
208
+ if (signal) return `server terminated by signal ${signal} before completing the request`;
209
+ if (code === 0) {
210
+ return "server exited cleanly (code 0) before completing the request. This usually means the command is a one-shot CLI, not a long-running MCP stdio server. If the server needs a subcommand to start (e.g. `serve`, `mcp`, `start`), include it in the command.";
211
+ }
212
+ return `server crashed with exit code ${code} before completing the request`;
213
+ }
207
214
  function createStdioTransport(opts) {
208
215
  const { command, args = [], env, cwd, verbose = false } = opts;
209
216
  const stderrBufferSize = opts.stderrBufferSize ?? 64 * 1024;
@@ -243,8 +250,7 @@ function createStdioTransport(opts) {
243
250
  exited = true;
244
251
  exitCode = code;
245
252
  if (pending.size > 0) {
246
- const reason = signal ? `child exited (signal ${signal})` : `child exited with code ${code}`;
247
- rejectAllPending(new Error(reason));
253
+ rejectAllPending(new Error(exitDiagnostic(code, signal)));
248
254
  }
249
255
  });
250
256
  child.stdout?.setEncoding("utf8");
@@ -319,7 +325,7 @@ function createStdioTransport(opts) {
319
325
  }
320
326
  }
321
327
  if (exited) {
322
- throw new Error(annotateWithStderr(`stdio transport: child has exited (code ${exitCode})`));
328
+ throw new Error(annotateWithStderr(`stdio transport: ${exitDiagnostic(exitCode, null)}`));
323
329
  }
324
330
  if (spawnError) throw new Error(annotateWithStderr(`stdio transport: spawn failed \u2014 ${spawnError.message}`));
325
331
  const stdin = child.stdin;
@@ -740,12 +746,28 @@ function computeScore(tests) {
740
746
  const failed = total - passed;
741
747
  const requiredTests = tests.filter((t) => t.required);
742
748
  const requiredPassed = requiredTests.filter((t) => t.passed).length;
743
- const requiredScore = requiredTests.length > 0 ? requiredPassed / requiredTests.length * 70 : 70;
744
749
  const optionalTests = tests.filter((t) => !t.required);
745
750
  const optionalPassed = optionalTests.filter((t) => t.passed).length;
746
- const optionalScore = optionalTests.length > 0 ? optionalPassed / optionalTests.length * 30 : 30;
747
- const score = Math.round(requiredScore + optionalScore);
748
- const overall = requiredPassed === requiredTests.length ? passed === total ? "pass" : "partial" : "fail";
751
+ let score;
752
+ if (total === 0) {
753
+ score = 0;
754
+ } else if (requiredTests.length === 0) {
755
+ score = Math.round(optionalPassed / optionalTests.length * 100);
756
+ } else if (optionalTests.length === 0) {
757
+ score = Math.round(requiredPassed / requiredTests.length * 100);
758
+ } else {
759
+ score = Math.round(requiredPassed / requiredTests.length * 70 + optionalPassed / optionalTests.length * 30);
760
+ }
761
+ let overall;
762
+ if (total === 0) {
763
+ overall = "fail";
764
+ } else if (requiredPassed < requiredTests.length) {
765
+ overall = "fail";
766
+ } else if (passed === total) {
767
+ overall = "pass";
768
+ } else {
769
+ overall = "partial";
770
+ }
749
771
  const categories = {};
750
772
  for (const t of tests) {
751
773
  if (!categories[t.category]) categories[t.category] = { passed: 0, total: 0 };
@@ -4408,7 +4430,7 @@ async function runComplianceSuite(target, options = {}) {
4408
4430
  warnings.length = 0;
4409
4431
  warnings.push(...capped);
4410
4432
  const { score, grade, overall, summary, categories } = computeScore(tests);
4411
- const badge = generateBadge(displayUrl);
4433
+ const badge = displayUrl.startsWith("stdio:") ? { imageUrl: "", reportUrl: "", markdown: "", html: "" } : generateBadge(displayUrl);
4412
4434
  return {
4413
4435
  schemaVersion: REPORT_SCHEMA_VERSION,
4414
4436
  specVersion: SPEC_VERSION,
@@ -4956,7 +4978,7 @@ function formatGithub(report) {
4956
4978
  }
4957
4979
  function formatMarkdown(report) {
4958
4980
  const lines = [];
4959
- const gradeEmoji = { A: "\u{1F7E2}", B: "\u{1F7E2}", C: "\u{1F7E1}", D: "\u{1F7E0}", F: "\u{1F534}" };
4981
+ const gradeEmoji = { A: "\u{1F7E2}", B: "\u{1F535}", C: "\u{1F7E1}", D: "\u{1F7E0}", F: "\u{1F534}" };
4960
4982
  lines.push("# MCP Compliance Report");
4961
4983
  lines.push("");
4962
4984
  lines.push(
@@ -5286,6 +5308,21 @@ function resolveTarget(cliTarget, cliExtraArgs, cliOpts, config) {
5286
5308
  if (config?.target) return config.target;
5287
5309
  throw new Error("No target specified. Pass a URL or command, or add 'target' to mcp-compliance.config.json.");
5288
5310
  }
5311
+ var PRIVATE_TLD_SUFFIXES = [
5312
+ ".local",
5313
+ ".localhost",
5314
+ ".internal",
5315
+ ".corp",
5316
+ ".home",
5317
+ ".home.arpa",
5318
+ ".lan",
5319
+ ".intranet",
5320
+ ".private",
5321
+ ".test",
5322
+ ".invalid",
5323
+ ".example",
5324
+ ".onion"
5325
+ ];
5289
5326
  function isPrivateHost(urlStr) {
5290
5327
  let host;
5291
5328
  try {
@@ -5293,7 +5330,10 @@ function isPrivateHost(urlStr) {
5293
5330
  } catch {
5294
5331
  return false;
5295
5332
  }
5296
- if (host === "localhost" || host.endsWith(".localhost")) return true;
5333
+ if (host === "localhost") return true;
5334
+ for (const suffix of PRIVATE_TLD_SUFFIXES) {
5335
+ if (host === suffix.slice(1) || host.endsWith(suffix)) return true;
5336
+ }
5297
5337
  const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
5298
5338
  if (v4) {
5299
5339
  const [a, b] = v4.slice(1).map(Number);
@@ -2,7 +2,7 @@ import {
2
2
  SPEC_BASE,
3
3
  TEST_DEFINITIONS,
4
4
  runComplianceSuite
5
- } from "../chunk-BX22BHC5.js";
5
+ } from "../chunk-X5CVUDPW.js";
6
6
 
7
7
  // src/mcp/server.ts
8
8
  import { existsSync, readFileSync, realpathSync } from "fs";
package/dist/runner.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  previewTests,
11
11
  runComplianceSuite,
12
12
  urlHash
13
- } from "./chunk-BX22BHC5.js";
13
+ } from "./chunk-X5CVUDPW.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.13.1",
3
+ "version": "0.13.2",
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)",