@yawlabs/mcp-compliance 0.13.1 → 0.13.3
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-BX22BHC5.js → chunk-X5CVUDPW.js} +30 -8
- package/dist/index.js +73 -12
- package/dist/mcp/server.js +1 -1
- package/dist/runner.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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{
|
|
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(
|
|
@@ -5139,6 +5161,20 @@ function formatHtml(report) {
|
|
|
5139
5161
|
</html>`;
|
|
5140
5162
|
}
|
|
5141
5163
|
|
|
5164
|
+
// src/stdio-split.ts
|
|
5165
|
+
function splitStdioTarget(s) {
|
|
5166
|
+
if (/["'`]/.test(s)) {
|
|
5167
|
+
throw new Error(
|
|
5168
|
+
"stdio command contains quote characters \u2014 pass the command and its args as separate tokens (e.g. `mcp-compliance test node dist/index.js serve`) instead of wrapping them in one quoted string."
|
|
5169
|
+
);
|
|
5170
|
+
}
|
|
5171
|
+
const tokens = s.trim().split(/\s+/).filter(Boolean);
|
|
5172
|
+
if (tokens.length === 0) {
|
|
5173
|
+
throw new Error("stdio command is empty");
|
|
5174
|
+
}
|
|
5175
|
+
return { command: tokens[0], args: tokens.slice(1) };
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5142
5178
|
// src/token-store.ts
|
|
5143
5179
|
import { createHash as createHash2 } from "crypto";
|
|
5144
5180
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
@@ -5268,10 +5304,17 @@ function buildTarget(positional, extraArgs, opts) {
|
|
|
5268
5304
|
...opts.envFile ? readEnvFile(opts.envFile) : {},
|
|
5269
5305
|
...opts.env ?? {}
|
|
5270
5306
|
};
|
|
5307
|
+
let command = positional;
|
|
5308
|
+
let args = extraArgs;
|
|
5309
|
+
if (extraArgs.length === 0 && /\s/.test(positional)) {
|
|
5310
|
+
const split = splitStdioTarget(positional);
|
|
5311
|
+
command = split.command;
|
|
5312
|
+
args = split.args;
|
|
5313
|
+
}
|
|
5271
5314
|
return {
|
|
5272
5315
|
type: "stdio",
|
|
5273
|
-
command
|
|
5274
|
-
args
|
|
5316
|
+
command,
|
|
5317
|
+
args,
|
|
5275
5318
|
env: Object.keys(env).length ? env : void 0,
|
|
5276
5319
|
cwd: opts.cwd,
|
|
5277
5320
|
verbose: opts.verbose
|
|
@@ -5286,6 +5329,21 @@ function resolveTarget(cliTarget, cliExtraArgs, cliOpts, config) {
|
|
|
5286
5329
|
if (config?.target) return config.target;
|
|
5287
5330
|
throw new Error("No target specified. Pass a URL or command, or add 'target' to mcp-compliance.config.json.");
|
|
5288
5331
|
}
|
|
5332
|
+
var PRIVATE_TLD_SUFFIXES = [
|
|
5333
|
+
".local",
|
|
5334
|
+
".localhost",
|
|
5335
|
+
".internal",
|
|
5336
|
+
".corp",
|
|
5337
|
+
".home",
|
|
5338
|
+
".home.arpa",
|
|
5339
|
+
".lan",
|
|
5340
|
+
".intranet",
|
|
5341
|
+
".private",
|
|
5342
|
+
".test",
|
|
5343
|
+
".invalid",
|
|
5344
|
+
".example",
|
|
5345
|
+
".onion"
|
|
5346
|
+
];
|
|
5289
5347
|
function isPrivateHost(urlStr) {
|
|
5290
5348
|
let host;
|
|
5291
5349
|
try {
|
|
@@ -5293,7 +5351,10 @@ function isPrivateHost(urlStr) {
|
|
|
5293
5351
|
} catch {
|
|
5294
5352
|
return false;
|
|
5295
5353
|
}
|
|
5296
|
-
if (host === "localhost"
|
|
5354
|
+
if (host === "localhost") return true;
|
|
5355
|
+
for (const suffix of PRIVATE_TLD_SUFFIXES) {
|
|
5356
|
+
if (host === suffix.slice(1) || host.endsWith(suffix)) return true;
|
|
5357
|
+
}
|
|
5297
5358
|
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
5298
5359
|
if (v4) {
|
|
5299
5360
|
const [a, b] = v4.slice(1).map(Number);
|
package/dist/mcp/server.js
CHANGED
package/dist/runner.js
CHANGED
package/package.json
CHANGED