@yawlabs/mcp-compliance 0.8.0 → 0.9.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/README.md +160 -34
- package/dist/{chunk-DOIOJVEE.js → chunk-T26GLXNK.js} +544 -150
- package/dist/index.js +1244 -183
- package/dist/mcp/server.js +23 -4
- package/dist/runner.d.ts +44 -9
- package/dist/runner.js +3 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/runner.ts
|
|
2
2
|
import { createRequire } from "module";
|
|
3
|
-
import { request } from "undici";
|
|
3
|
+
import { request as request2 } from "undici";
|
|
4
4
|
|
|
5
5
|
// src/badge.ts
|
|
6
6
|
import { createHash } from "crypto";
|
|
@@ -9,8 +9,8 @@ function urlHash(url) {
|
|
|
9
9
|
}
|
|
10
10
|
function generateBadge(url) {
|
|
11
11
|
const hash = urlHash(url);
|
|
12
|
-
const imageUrl = `https://mcp.hosting/api/compliance/${hash}/badge`;
|
|
13
|
-
const reportUrl = `https://mcp.hosting/compliance/${hash}`;
|
|
12
|
+
const imageUrl = `https://mcp.hosting/api/compliance/ext/${hash}/badge`;
|
|
13
|
+
const reportUrl = `https://mcp.hosting/compliance/ext/${hash}`;
|
|
14
14
|
return {
|
|
15
15
|
imageUrl,
|
|
16
16
|
reportUrl,
|
|
@@ -54,6 +54,307 @@ function computeScore(tests) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// src/transport/http.ts
|
|
58
|
+
import { request } from "undici";
|
|
59
|
+
|
|
60
|
+
// src/sse.ts
|
|
61
|
+
function parseSSEResponse(text) {
|
|
62
|
+
const lines = text.split("\n");
|
|
63
|
+
let firstJsonRpcResponse = null;
|
|
64
|
+
let currentData = [];
|
|
65
|
+
function flushEvent() {
|
|
66
|
+
if (currentData.length === 0) return;
|
|
67
|
+
const data = currentData.join("\n");
|
|
68
|
+
currentData = [];
|
|
69
|
+
if (!data.trim()) return;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(data);
|
|
72
|
+
if (!firstJsonRpcResponse && parsed.jsonrpc === "2.0" && parsed.id !== void 0) {
|
|
73
|
+
firstJsonRpcResponse = parsed;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
if (line.startsWith("data:")) {
|
|
80
|
+
const content = line.slice(5);
|
|
81
|
+
currentData.push(content.startsWith(" ") ? content.slice(1) : content);
|
|
82
|
+
} else if (line.trim() === "") {
|
|
83
|
+
flushEvent();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
flushEvent();
|
|
87
|
+
return firstJsonRpcResponse;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/transport/http.ts
|
|
91
|
+
function createHttpTransport(opts) {
|
|
92
|
+
const { url } = opts;
|
|
93
|
+
const userHeaders = { ...opts.headers ?? {} };
|
|
94
|
+
let sessionId = null;
|
|
95
|
+
let protocolVersion = null;
|
|
96
|
+
function sessionHeaders() {
|
|
97
|
+
const h = { ...userHeaders };
|
|
98
|
+
if (sessionId) h["mcp-session-id"] = sessionId;
|
|
99
|
+
if (protocolVersion) h["mcp-protocol-version"] = protocolVersion;
|
|
100
|
+
return h;
|
|
101
|
+
}
|
|
102
|
+
function normalizeHeaders(raw) {
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
105
|
+
if (typeof v === "string") out[k] = v;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
async function doRawRequest(method, body, extraHeaders, timeout) {
|
|
110
|
+
const headers = {
|
|
111
|
+
Accept: "application/json, text/event-stream",
|
|
112
|
+
...sessionHeaders(),
|
|
113
|
+
...extraHeaders
|
|
114
|
+
};
|
|
115
|
+
if (body !== void 0 && !("Content-Type" in headers) && !("content-type" in headers)) {
|
|
116
|
+
headers["Content-Type"] = "application/json";
|
|
117
|
+
}
|
|
118
|
+
const res = await request(url, {
|
|
119
|
+
method,
|
|
120
|
+
headers,
|
|
121
|
+
body,
|
|
122
|
+
signal: AbortSignal.timeout(timeout)
|
|
123
|
+
});
|
|
124
|
+
const text = await res.body.text();
|
|
125
|
+
return {
|
|
126
|
+
statusCode: res.statusCode,
|
|
127
|
+
body: text,
|
|
128
|
+
headers: normalizeHeaders(res.headers)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const transport = {
|
|
132
|
+
kind: "http",
|
|
133
|
+
url,
|
|
134
|
+
async request(method, params, nextId, init) {
|
|
135
|
+
const id = nextId();
|
|
136
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
137
|
+
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout);
|
|
138
|
+
const contentType = (raw.headers["content-type"] || "").toLowerCase();
|
|
139
|
+
let parsed;
|
|
140
|
+
if (contentType.includes("text/event-stream")) {
|
|
141
|
+
const sseParsed = parseSSEResponse(raw.body);
|
|
142
|
+
if (sseParsed) {
|
|
143
|
+
parsed = sseParsed;
|
|
144
|
+
} else {
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(raw.body);
|
|
147
|
+
} catch {
|
|
148
|
+
parsed = { _raw: raw.body };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(raw.body);
|
|
154
|
+
} catch {
|
|
155
|
+
parsed = { _raw: raw.body };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
body: parsed,
|
|
160
|
+
requestId: id,
|
|
161
|
+
statusCode: raw.statusCode,
|
|
162
|
+
headers: raw.headers
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
async notify(method, params, init) {
|
|
166
|
+
const body = JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} });
|
|
167
|
+
const raw = await doRawRequest("POST", body, init.headers ?? {}, init.timeout);
|
|
168
|
+
return { statusCode: raw.statusCode, headers: raw.headers };
|
|
169
|
+
},
|
|
170
|
+
async close() {
|
|
171
|
+
},
|
|
172
|
+
setSessionId(id) {
|
|
173
|
+
sessionId = id;
|
|
174
|
+
},
|
|
175
|
+
setProtocolVersion(v) {
|
|
176
|
+
protocolVersion = v;
|
|
177
|
+
},
|
|
178
|
+
getSessionId() {
|
|
179
|
+
return sessionId;
|
|
180
|
+
},
|
|
181
|
+
getProtocolVersion() {
|
|
182
|
+
return protocolVersion;
|
|
183
|
+
},
|
|
184
|
+
rawPost(body, extraHeaders, timeout) {
|
|
185
|
+
return doRawRequest("POST", body, extraHeaders, timeout);
|
|
186
|
+
},
|
|
187
|
+
rawRequest(method, body, extraHeaders, timeout) {
|
|
188
|
+
return doRawRequest(method, body, extraHeaders, timeout);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
return transport;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/transport/stdio.ts
|
|
195
|
+
import { spawn } from "child_process";
|
|
196
|
+
function createStdioTransport(opts) {
|
|
197
|
+
const { command, args = [], env, cwd, verbose = false } = opts;
|
|
198
|
+
const stderrBufferSize = opts.stderrBufferSize ?? 64 * 1024;
|
|
199
|
+
const isWindows = process.platform === "win32";
|
|
200
|
+
const child = spawn(command, args, {
|
|
201
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
202
|
+
cwd,
|
|
203
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
204
|
+
// Windows .cmd/.bat shims (npx, npm) need shell:true to launch.
|
|
205
|
+
shell: isWindows
|
|
206
|
+
});
|
|
207
|
+
let protocolVersion = null;
|
|
208
|
+
let exited = false;
|
|
209
|
+
let exitCode = null;
|
|
210
|
+
let spawnError = null;
|
|
211
|
+
const pending = /* @__PURE__ */ new Map();
|
|
212
|
+
let stdoutBuffer = "";
|
|
213
|
+
let stderrBuffer = "";
|
|
214
|
+
child.on("error", (err) => {
|
|
215
|
+
spawnError = err;
|
|
216
|
+
rejectAllPending(err);
|
|
217
|
+
});
|
|
218
|
+
child.on("exit", (code, signal) => {
|
|
219
|
+
exited = true;
|
|
220
|
+
exitCode = code;
|
|
221
|
+
if (pending.size > 0) {
|
|
222
|
+
const reason = signal ? `child exited (signal ${signal})` : `child exited with code ${code}`;
|
|
223
|
+
rejectAllPending(new Error(reason));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
child.stdout?.setEncoding("utf8");
|
|
227
|
+
child.stdout?.on("data", (chunk) => {
|
|
228
|
+
stdoutBuffer += chunk;
|
|
229
|
+
let idx;
|
|
230
|
+
while ((idx = stdoutBuffer.indexOf("\n")) !== -1) {
|
|
231
|
+
const line = stdoutBuffer.slice(0, idx);
|
|
232
|
+
stdoutBuffer = stdoutBuffer.slice(idx + 1);
|
|
233
|
+
handleLine(line);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
child.stderr?.setEncoding("utf8");
|
|
237
|
+
child.stderr?.on("data", (chunk) => {
|
|
238
|
+
if (verbose) process.stderr.write(chunk);
|
|
239
|
+
stderrBuffer += chunk;
|
|
240
|
+
if (stderrBuffer.length > stderrBufferSize) {
|
|
241
|
+
stderrBuffer = stderrBuffer.slice(stderrBuffer.length - stderrBufferSize);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
function handleLine(line) {
|
|
245
|
+
const trimmed = line.trim();
|
|
246
|
+
if (!trimmed) return;
|
|
247
|
+
let parsed;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(trimmed);
|
|
250
|
+
} catch {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
254
|
+
const msg = parsed;
|
|
255
|
+
if (typeof msg.id === "number" && pending.has(msg.id)) {
|
|
256
|
+
const p = pending.get(msg.id);
|
|
257
|
+
if (!p) return;
|
|
258
|
+
clearTimeout(p.timer);
|
|
259
|
+
pending.delete(msg.id);
|
|
260
|
+
p.resolve({ body: parsed, requestId: msg.id });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function rejectAllPending(err) {
|
|
264
|
+
for (const p of pending.values()) {
|
|
265
|
+
clearTimeout(p.timer);
|
|
266
|
+
p.reject(err);
|
|
267
|
+
}
|
|
268
|
+
pending.clear();
|
|
269
|
+
}
|
|
270
|
+
async function writeLine(line) {
|
|
271
|
+
if (exited) throw new Error("stdio transport: child has exited");
|
|
272
|
+
if (spawnError) throw new Error(`stdio transport: spawn failed \u2014 ${spawnError.message}`);
|
|
273
|
+
const stdin = child.stdin;
|
|
274
|
+
if (!stdin || stdin.destroyed) throw new Error("stdio transport: stdin is closed");
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
stdin.write(`${line}
|
|
277
|
+
`, "utf8", (err) => err ? reject(err) : resolve());
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const transport = {
|
|
281
|
+
kind: "stdio",
|
|
282
|
+
command,
|
|
283
|
+
args,
|
|
284
|
+
get pid() {
|
|
285
|
+
return child.pid;
|
|
286
|
+
},
|
|
287
|
+
get exited() {
|
|
288
|
+
return exited;
|
|
289
|
+
},
|
|
290
|
+
get exitCode() {
|
|
291
|
+
return exitCode;
|
|
292
|
+
},
|
|
293
|
+
stderrTail() {
|
|
294
|
+
return stderrBuffer;
|
|
295
|
+
},
|
|
296
|
+
async request(method, params, nextId, init) {
|
|
297
|
+
const id = nextId();
|
|
298
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
const timer = setTimeout(() => {
|
|
301
|
+
pending.delete(id);
|
|
302
|
+
reject(new Error(`stdio transport: request timed out after ${init.timeout}ms (method=${method})`));
|
|
303
|
+
}, init.timeout);
|
|
304
|
+
pending.set(id, { resolve, reject, id, timer });
|
|
305
|
+
writeLine(body).catch((err) => {
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
pending.delete(id);
|
|
308
|
+
reject(err);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
async notify(method, params, _init) {
|
|
313
|
+
const body = JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} });
|
|
314
|
+
await writeLine(body);
|
|
315
|
+
return {};
|
|
316
|
+
},
|
|
317
|
+
async close() {
|
|
318
|
+
if (exited) return;
|
|
319
|
+
try {
|
|
320
|
+
child.stdin?.end();
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
const gracePeriodMs = 2e3;
|
|
324
|
+
await new Promise((resolve) => {
|
|
325
|
+
const timer = setTimeout(() => {
|
|
326
|
+
try {
|
|
327
|
+
child.kill("SIGKILL");
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
resolve();
|
|
331
|
+
}, gracePeriodMs);
|
|
332
|
+
child.once("exit", () => {
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
resolve();
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
child.kill(isWindows ? void 0 : "SIGTERM");
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
rejectAllPending(new Error("stdio transport: closed"));
|
|
342
|
+
},
|
|
343
|
+
setSessionId(_id) {
|
|
344
|
+
},
|
|
345
|
+
setProtocolVersion(v) {
|
|
346
|
+
protocolVersion = v;
|
|
347
|
+
},
|
|
348
|
+
getSessionId() {
|
|
349
|
+
return null;
|
|
350
|
+
},
|
|
351
|
+
getProtocolVersion() {
|
|
352
|
+
return protocolVersion;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
return transport;
|
|
356
|
+
}
|
|
357
|
+
|
|
57
358
|
// src/types.ts
|
|
58
359
|
var TEST_DEFINITIONS = [
|
|
59
360
|
// ── Transport (13 tests) ─────────────────────────────────────────
|
|
@@ -174,6 +475,37 @@ var TEST_DEFINITIONS = [
|
|
|
174
475
|
description: "Sends a request with Accept: text/event-stream and checks that SSE responses include the event: message field. Per spec, servers MUST set event: message for JSON-RPC messages in SSE streams.",
|
|
175
476
|
recommendation: 'Include "event: message" before each "data:" line in your SSE responses. This is required by the MCP spec for JSON-RPC messages sent over SSE.'
|
|
176
477
|
},
|
|
478
|
+
// ── Stdio transport (stdio-only) ─────────────────────────────────
|
|
479
|
+
{
|
|
480
|
+
id: "stdio-framing",
|
|
481
|
+
name: "Newline-delimited JSON framing",
|
|
482
|
+
category: "transport",
|
|
483
|
+
required: true,
|
|
484
|
+
specRef: "basic/transports#stdio",
|
|
485
|
+
description: "Fires several JSON-RPC requests in rapid succession and verifies the server frames each response with a trailing newline per the MCP stdio transport spec.",
|
|
486
|
+
recommendation: "Emit one JSON message per line on stdout, terminated by \\n. Do not split a single message across multiple lines or merge multiple messages onto one line.",
|
|
487
|
+
transports: ["stdio"]
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
id: "stdio-unicode",
|
|
491
|
+
name: "UTF-8 unicode roundtrip",
|
|
492
|
+
category: "transport",
|
|
493
|
+
required: false,
|
|
494
|
+
specRef: "basic/transports#stdio",
|
|
495
|
+
description: "Sends a request with non-ASCII (CJK + emoji) parameters and verifies the response preserves the characters. Catches byte-level encoding mistakes in framing/parsing.",
|
|
496
|
+
recommendation: "Decode stdin as UTF-8 and encode stdout as UTF-8. Avoid latin-1 or platform-default encodings on Windows. Most JSON libraries handle this correctly if you don't override defaults.",
|
|
497
|
+
transports: ["stdio"]
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: "stdio-unknown-method-recovers",
|
|
501
|
+
name: "Recovers after unknown method",
|
|
502
|
+
category: "transport",
|
|
503
|
+
required: false,
|
|
504
|
+
specRef: "basic/transports#stdio",
|
|
505
|
+
description: "Sends an unknown method, then a valid ping immediately after. Verifies the server returns a JSON-RPC error for the unknown method and continues serving the subsequent request without crashing.",
|
|
506
|
+
recommendation: "Return JSON-RPC error -32601 (Method not found) for unknown methods. Do not exit the process or disconnect \u2014 the client should be able to keep using the session after an error.",
|
|
507
|
+
transports: ["stdio"]
|
|
508
|
+
},
|
|
177
509
|
// ── Lifecycle (17 tests) ─────────────────────────────────────────
|
|
178
510
|
{
|
|
179
511
|
id: "lifecycle-init",
|
|
@@ -859,127 +1191,97 @@ function createIdCounter(start = 0) {
|
|
|
859
1191
|
let id = start;
|
|
860
1192
|
return () => ++id;
|
|
861
1193
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
flushEvent();
|
|
888
|
-
return firstJsonRpcResponse;
|
|
1194
|
+
var STDIO_INCOMPATIBLE_IDS = /* @__PURE__ */ new Set([
|
|
1195
|
+
// Lifecycle tests that use raw undici for HTTP-specific checks
|
|
1196
|
+
"lifecycle-string-id",
|
|
1197
|
+
// Error tests that send hand-crafted malformed bytes via raw HTTP
|
|
1198
|
+
// (JSON-RPC layer would reject them before they hit the wire). Could
|
|
1199
|
+
// be reimplemented for stdio later by writing raw bytes to stdin.
|
|
1200
|
+
"error-invalid-jsonrpc",
|
|
1201
|
+
"error-invalid-json",
|
|
1202
|
+
"error-parse-code",
|
|
1203
|
+
"error-invalid-request-code",
|
|
1204
|
+
// Security tests that are inherently HTTP-layer
|
|
1205
|
+
"security-tls-required",
|
|
1206
|
+
"security-oauth-metadata",
|
|
1207
|
+
"security-token-in-uri",
|
|
1208
|
+
"security-rate-limit",
|
|
1209
|
+
"security-cors",
|
|
1210
|
+
"security-origin-validation"
|
|
1211
|
+
]);
|
|
1212
|
+
function supportsTransport(def, kind) {
|
|
1213
|
+
if (!def) return true;
|
|
1214
|
+
if (def.transports) return def.transports.includes(kind);
|
|
1215
|
+
if (kind === "http") return true;
|
|
1216
|
+
if (def.category === "transport") return false;
|
|
1217
|
+
if (STDIO_INCOMPATIBLE_IDS.has(def.id)) return false;
|
|
1218
|
+
return true;
|
|
889
1219
|
}
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
params: params || {}
|
|
897
|
-
});
|
|
898
|
-
const headers = {
|
|
899
|
-
"Content-Type": "application/json",
|
|
900
|
-
Accept: "application/json, text/event-stream",
|
|
901
|
-
...extraHeaders
|
|
902
|
-
};
|
|
903
|
-
const res = await request(backendUrl, {
|
|
904
|
-
method: "POST",
|
|
905
|
-
headers,
|
|
906
|
-
body,
|
|
907
|
-
signal: AbortSignal.timeout(timeout)
|
|
908
|
-
});
|
|
909
|
-
const text = await res.body.text();
|
|
910
|
-
const responseHeaders = {};
|
|
911
|
-
for (const [k, v] of Object.entries(res.headers)) {
|
|
912
|
-
if (typeof v === "string") responseHeaders[k] = v;
|
|
913
|
-
}
|
|
914
|
-
const contentType = (responseHeaders["content-type"] || "").toLowerCase();
|
|
915
|
-
if (contentType.includes("text/event-stream")) {
|
|
916
|
-
const parsed = parseSSEResponse(text);
|
|
917
|
-
if (parsed) {
|
|
918
|
-
return { statusCode: res.statusCode, body: parsed, headers: responseHeaders, requestId: id };
|
|
1220
|
+
function previewTests(opts = {}) {
|
|
1221
|
+
const transport = opts.transport ?? "http";
|
|
1222
|
+
return TEST_DEFINITIONS.filter((def) => {
|
|
1223
|
+
if (!supportsTransport(def, transport)) return false;
|
|
1224
|
+
if (opts.only?.length) {
|
|
1225
|
+
if (!opts.only.includes(def.category) && !opts.only.includes(def.id)) return false;
|
|
919
1226
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
} catch {
|
|
923
|
-
return { statusCode: res.statusCode, body: { _raw: text }, headers: responseHeaders, requestId: id };
|
|
1227
|
+
if (opts.skip?.length) {
|
|
1228
|
+
if (opts.skip.includes(def.category) || opts.skip.includes(def.id)) return false;
|
|
924
1229
|
}
|
|
925
|
-
|
|
926
|
-
try {
|
|
927
|
-
return { statusCode: res.statusCode, body: JSON.parse(text), headers: responseHeaders, requestId: id };
|
|
928
|
-
} catch {
|
|
929
|
-
return { statusCode: res.statusCode, body: { _raw: text }, headers: responseHeaders, requestId: id };
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
async function mcpNotification(backendUrl, method, params, extraHeaders, timeout) {
|
|
933
|
-
const headers = {
|
|
934
|
-
"Content-Type": "application/json",
|
|
935
|
-
Accept: "application/json, text/event-stream",
|
|
936
|
-
...extraHeaders
|
|
937
|
-
};
|
|
938
|
-
const res = await request(backendUrl, {
|
|
939
|
-
method: "POST",
|
|
940
|
-
headers,
|
|
941
|
-
body: JSON.stringify({ jsonrpc: "2.0", method, ...params ? { params } : {} }),
|
|
942
|
-
signal: AbortSignal.timeout(timeout)
|
|
1230
|
+
return true;
|
|
943
1231
|
});
|
|
944
|
-
await res.body.text();
|
|
945
|
-
const responseHeaders = {};
|
|
946
|
-
for (const [k, v] of Object.entries(res.headers)) {
|
|
947
|
-
if (typeof v === "string") responseHeaders[k] = v;
|
|
948
|
-
}
|
|
949
|
-
return { statusCode: res.statusCode, headers: responseHeaders };
|
|
950
1232
|
}
|
|
951
|
-
async function runComplianceSuite(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1233
|
+
async function runComplianceSuite(target, options = {}) {
|
|
1234
|
+
const resolvedTarget = typeof target === "string" ? { type: "http", url: target, headers: options.headers } : target;
|
|
1235
|
+
if (resolvedTarget.type === "http") {
|
|
1236
|
+
try {
|
|
1237
|
+
const parsed = new URL(resolvedTarget.url);
|
|
1238
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
1239
|
+
throw new Error("Only HTTP and HTTPS URLs are supported");
|
|
1240
|
+
}
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
if (e instanceof Error && e.message.includes("Only HTTP")) throw e;
|
|
1243
|
+
throw new Error(`Invalid URL: ${resolvedTarget.url}`);
|
|
956
1244
|
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
throw new Error(`Invalid URL: ${url}`);
|
|
1245
|
+
} else if (!resolvedTarget.command) {
|
|
1246
|
+
throw new Error("stdio target requires a command");
|
|
960
1247
|
}
|
|
961
|
-
const
|
|
1248
|
+
const transport = resolvedTarget.type === "http" ? createHttpTransport({
|
|
1249
|
+
url: resolvedTarget.url,
|
|
1250
|
+
headers: resolvedTarget.headers ?? options.headers
|
|
1251
|
+
}) : createStdioTransport({
|
|
1252
|
+
command: resolvedTarget.command,
|
|
1253
|
+
args: resolvedTarget.args,
|
|
1254
|
+
env: resolvedTarget.env,
|
|
1255
|
+
cwd: resolvedTarget.cwd,
|
|
1256
|
+
verbose: resolvedTarget.verbose
|
|
1257
|
+
});
|
|
1258
|
+
const backendUrl = resolvedTarget.type === "http" ? resolvedTarget.url : "";
|
|
1259
|
+
const userHeaders = resolvedTarget.type === "http" ? resolvedTarget.headers ?? options.headers ?? {} : {};
|
|
1260
|
+
const displayUrl = resolvedTarget.type === "http" ? resolvedTarget.url : `stdio:${resolvedTarget.command}${resolvedTarget.args?.length ? ` ${resolvedTarget.args.join(" ")}` : ""}`;
|
|
962
1261
|
let serverReachable = true;
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1262
|
+
if (resolvedTarget.type === "http") {
|
|
1263
|
+
try {
|
|
1264
|
+
const preflightTimeout = options.preflightTimeout ?? Math.min(options.timeout || 15e3, 1e4);
|
|
1265
|
+
const preflight = await request2(resolvedTarget.url, {
|
|
1266
|
+
method: "POST",
|
|
1267
|
+
headers: {
|
|
1268
|
+
"Content-Type": "application/json",
|
|
1269
|
+
Accept: "application/json, text/event-stream",
|
|
1270
|
+
...userHeaders
|
|
1271
|
+
},
|
|
1272
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "ping" }),
|
|
1273
|
+
signal: AbortSignal.timeout(preflightTimeout)
|
|
1274
|
+
});
|
|
1275
|
+
await preflight.body.text();
|
|
1276
|
+
} catch {
|
|
1277
|
+
serverReachable = false;
|
|
1278
|
+
}
|
|
977
1279
|
}
|
|
978
1280
|
const tests = [];
|
|
979
1281
|
const warnings = [];
|
|
980
1282
|
if (!serverReachable) {
|
|
981
1283
|
warnings.push(
|
|
982
|
-
`Server at ${
|
|
1284
|
+
`Server at ${displayUrl} is unreachable \u2014 all tests will fail. Check the URL or command and ensure the server is running.`
|
|
983
1285
|
);
|
|
984
1286
|
}
|
|
985
1287
|
const nextId = createIdCounter(1e3);
|
|
@@ -987,15 +1289,35 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
987
1289
|
const retries = options.retries || 0;
|
|
988
1290
|
let sessionId = null;
|
|
989
1291
|
let negotiatedProtocolVersion = null;
|
|
990
|
-
const userHeaders = options.headers || {};
|
|
991
1292
|
function buildHeaders() {
|
|
992
1293
|
const h = { ...userHeaders };
|
|
993
1294
|
if (sessionId) h["mcp-session-id"] = sessionId;
|
|
994
1295
|
if (negotiatedProtocolVersion) h["mcp-protocol-version"] = negotiatedProtocolVersion;
|
|
995
1296
|
return h;
|
|
996
1297
|
}
|
|
1298
|
+
async function mcpRequest(_backendUrl, method, params, idCounter, extraHeaders, timeoutMs) {
|
|
1299
|
+
const res = await transport.request(method, params, idCounter, {
|
|
1300
|
+
timeout: timeoutMs,
|
|
1301
|
+
headers: extraHeaders
|
|
1302
|
+
});
|
|
1303
|
+
return {
|
|
1304
|
+
statusCode: res.statusCode ?? 200,
|
|
1305
|
+
body: res.body,
|
|
1306
|
+
headers: res.headers ?? {},
|
|
1307
|
+
requestId: res.requestId
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
async function mcpNotification(_backendUrl, method, params, extraHeaders, timeoutMs) {
|
|
1311
|
+
const res = await transport.notify(method, params, {
|
|
1312
|
+
timeout: timeoutMs,
|
|
1313
|
+
headers: extraHeaders
|
|
1314
|
+
});
|
|
1315
|
+
return { statusCode: res.statusCode ?? 200, headers: res.headers ?? {} };
|
|
1316
|
+
}
|
|
997
1317
|
const rpc = (method, params) => mcpRequest(backendUrl, method, params, nextId, buildHeaders(), timeout);
|
|
998
1318
|
function shouldRun(id, category) {
|
|
1319
|
+
const def = TEST_DEFINITIONS_MAP.get(id);
|
|
1320
|
+
if (!supportsTransport(def, transport.kind)) return false;
|
|
999
1321
|
if (options.only && options.only.length > 0) {
|
|
1000
1322
|
return options.only.includes(category) || options.only.includes(id);
|
|
1001
1323
|
}
|
|
@@ -1050,7 +1372,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1050
1372
|
true,
|
|
1051
1373
|
"basic/transports#streamable-http",
|
|
1052
1374
|
async () => {
|
|
1053
|
-
const res = await
|
|
1375
|
+
const res = await request2(backendUrl, {
|
|
1054
1376
|
method: "POST",
|
|
1055
1377
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...userHeaders },
|
|
1056
1378
|
body: JSON.stringify({ jsonrpc: "2.0", id: 99901, method: "ping" }),
|
|
@@ -1082,7 +1404,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1082
1404
|
true,
|
|
1083
1405
|
"basic/transports#streamable-http",
|
|
1084
1406
|
async () => {
|
|
1085
|
-
const res = await
|
|
1407
|
+
const res = await request2(backendUrl, {
|
|
1086
1408
|
method: "POST",
|
|
1087
1409
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...userHeaders },
|
|
1088
1410
|
body: JSON.stringify({ jsonrpc: "2.0", id: 99902, method: "ping" }),
|
|
@@ -1102,7 +1424,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1102
1424
|
false,
|
|
1103
1425
|
"basic/transports#streamable-http",
|
|
1104
1426
|
async () => {
|
|
1105
|
-
const res = await
|
|
1427
|
+
const res = await request2(backendUrl, {
|
|
1106
1428
|
method: "POST",
|
|
1107
1429
|
headers: { "Content-Type": "text/plain", Accept: "application/json, text/event-stream", ...userHeaders },
|
|
1108
1430
|
body: JSON.stringify({ jsonrpc: "2.0", id: 99905, method: "ping" }),
|
|
@@ -1129,7 +1451,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1129
1451
|
"basic/transports#streamable-http",
|
|
1130
1452
|
async () => {
|
|
1131
1453
|
const getHeaders = { Accept: "text/event-stream", ...buildHeaders() };
|
|
1132
|
-
const res = await
|
|
1454
|
+
const res = await request2(backendUrl, {
|
|
1133
1455
|
method: "GET",
|
|
1134
1456
|
headers: getHeaders,
|
|
1135
1457
|
signal: AbortSignal.timeout(timeout)
|
|
@@ -1166,7 +1488,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1166
1488
|
true,
|
|
1167
1489
|
"basic/transports#streamable-http",
|
|
1168
1490
|
async () => {
|
|
1169
|
-
const res = await
|
|
1491
|
+
const res = await request2(backendUrl, {
|
|
1170
1492
|
method: "POST",
|
|
1171
1493
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...userHeaders },
|
|
1172
1494
|
body: JSON.stringify([
|
|
@@ -1206,8 +1528,14 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1206
1528
|
serverInfo.version = result.serverInfo?.version || null;
|
|
1207
1529
|
serverInfo.capabilities = result.capabilities || {};
|
|
1208
1530
|
const sid = initRes.headers["mcp-session-id"];
|
|
1209
|
-
if (sid)
|
|
1210
|
-
|
|
1531
|
+
if (sid) {
|
|
1532
|
+
sessionId = sid;
|
|
1533
|
+
transport.setSessionId(sid);
|
|
1534
|
+
}
|
|
1535
|
+
if (result.protocolVersion) {
|
|
1536
|
+
negotiatedProtocolVersion = result.protocolVersion;
|
|
1537
|
+
transport.setProtocolVersion(result.protocolVersion);
|
|
1538
|
+
}
|
|
1211
1539
|
}
|
|
1212
1540
|
} catch {
|
|
1213
1541
|
}
|
|
@@ -1215,6 +1543,9 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1215
1543
|
await mcpNotification(backendUrl, "notifications/initialized", void 0, buildHeaders(), timeout);
|
|
1216
1544
|
} catch {
|
|
1217
1545
|
}
|
|
1546
|
+
const hasTools = !!serverInfo.capabilities.tools;
|
|
1547
|
+
const hasResources = !!serverInfo.capabilities.resources;
|
|
1548
|
+
const hasPrompts = !!serverInfo.capabilities.prompts;
|
|
1218
1549
|
await test(
|
|
1219
1550
|
"lifecycle-init",
|
|
1220
1551
|
"Initialize handshake",
|
|
@@ -1362,7 +1693,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1362
1693
|
await test("lifecycle-string-id", "Supports string request IDs", "lifecycle", false, "basic", async () => {
|
|
1363
1694
|
const stringId = "compliance-test-string-id";
|
|
1364
1695
|
const body = JSON.stringify({ jsonrpc: "2.0", id: stringId, method: "ping", params: {} });
|
|
1365
|
-
const res = await
|
|
1696
|
+
const res = await request2(backendUrl, {
|
|
1366
1697
|
method: "POST",
|
|
1367
1698
|
headers: {
|
|
1368
1699
|
"Content-Type": "application/json",
|
|
@@ -1577,7 +1908,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1577
1908
|
}
|
|
1578
1909
|
});
|
|
1579
1910
|
try {
|
|
1580
|
-
const res = await
|
|
1911
|
+
const res = await request2(backendUrl, {
|
|
1581
1912
|
method: "POST",
|
|
1582
1913
|
headers: {
|
|
1583
1914
|
"Content-Type": "application/json",
|
|
@@ -1625,7 +1956,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1625
1956
|
false,
|
|
1626
1957
|
"basic/transports#streamable-http",
|
|
1627
1958
|
async () => {
|
|
1628
|
-
const res = await
|
|
1959
|
+
const res = await request2(backendUrl, {
|
|
1629
1960
|
method: "POST",
|
|
1630
1961
|
headers: {
|
|
1631
1962
|
"Content-Type": "application/json",
|
|
@@ -1717,7 +2048,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1717
2048
|
if (!sessionId) {
|
|
1718
2049
|
return { passed: true, details: "No session ID \u2014 server-initiated messages not applicable" };
|
|
1719
2050
|
}
|
|
1720
|
-
const res = await
|
|
2051
|
+
const res = await request2(backendUrl, {
|
|
1721
2052
|
method: "GET",
|
|
1722
2053
|
headers: { Accept: "text/event-stream", ...buildHeaders() },
|
|
1723
2054
|
signal: AbortSignal.timeout(Math.min(timeout, 3e3))
|
|
@@ -1752,7 +2083,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1752
2083
|
async () => {
|
|
1753
2084
|
const ids = [createIdCounter(99930)(), createIdCounter(99931)(), createIdCounter(99932)()];
|
|
1754
2085
|
const promises = ids.map(
|
|
1755
|
-
(id) =>
|
|
2086
|
+
(id) => request2(backendUrl, {
|
|
1756
2087
|
method: "POST",
|
|
1757
2088
|
headers: {
|
|
1758
2089
|
"Content-Type": "application/json",
|
|
@@ -1798,7 +2129,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1798
2129
|
false,
|
|
1799
2130
|
"basic/transports#streamable-http",
|
|
1800
2131
|
async () => {
|
|
1801
|
-
const res = await
|
|
2132
|
+
const res = await request2(backendUrl, {
|
|
1802
2133
|
method: "POST",
|
|
1803
2134
|
headers: {
|
|
1804
2135
|
"Content-Type": "application/json",
|
|
@@ -1827,7 +2158,6 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
1827
2158
|
return { passed: true, details: "SSE response empty or no data fields \u2014 check not applicable" };
|
|
1828
2159
|
}
|
|
1829
2160
|
);
|
|
1830
|
-
const hasTools = !!serverInfo.capabilities.tools;
|
|
1831
2161
|
let cachedToolsList = null;
|
|
1832
2162
|
await test(
|
|
1833
2163
|
"tools-list",
|
|
@@ -2071,8 +2401,8 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2071
2401
|
}
|
|
2072
2402
|
);
|
|
2073
2403
|
}
|
|
2074
|
-
const
|
|
2075
|
-
const hasSubscribe = !!
|
|
2404
|
+
const resourcesCap = serverInfo.capabilities.resources;
|
|
2405
|
+
const hasSubscribe = !!(typeof resourcesCap === "object" && resourcesCap !== null && "subscribe" in resourcesCap && resourcesCap.subscribe);
|
|
2076
2406
|
if (hasResources) {
|
|
2077
2407
|
let cachedResourcesList = null;
|
|
2078
2408
|
await test(
|
|
@@ -2234,7 +2564,6 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2234
2564
|
);
|
|
2235
2565
|
}
|
|
2236
2566
|
}
|
|
2237
|
-
const hasPrompts = !!serverInfo.capabilities.prompts;
|
|
2238
2567
|
if (hasPrompts) {
|
|
2239
2568
|
let cachedPromptsList = null;
|
|
2240
2569
|
await test(
|
|
@@ -2349,7 +2678,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2349
2678
|
}
|
|
2350
2679
|
);
|
|
2351
2680
|
await test("error-invalid-jsonrpc", "Handles malformed JSON-RPC", "errors", true, "basic", async () => {
|
|
2352
|
-
const res = await
|
|
2681
|
+
const res = await request2(backendUrl, {
|
|
2353
2682
|
method: "POST",
|
|
2354
2683
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2355
2684
|
body: JSON.stringify({ not: "a valid jsonrpc message" }),
|
|
@@ -2372,7 +2701,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2372
2701
|
return { passed: false, details: `HTTP ${res.statusCode} \u2014 expected JSON-RPC error or 4xx status` };
|
|
2373
2702
|
});
|
|
2374
2703
|
await test("error-invalid-json", "Handles invalid JSON body", "errors", false, "basic", async () => {
|
|
2375
|
-
const res = await
|
|
2704
|
+
const res = await request2(backendUrl, {
|
|
2376
2705
|
method: "POST",
|
|
2377
2706
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2378
2707
|
body: "{this is not valid json!!!",
|
|
@@ -2410,7 +2739,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2410
2739
|
}
|
|
2411
2740
|
);
|
|
2412
2741
|
await test("error-parse-code", "Returns -32700 for invalid JSON", "errors", false, "basic", async () => {
|
|
2413
|
-
const res = await
|
|
2742
|
+
const res = await request2(backendUrl, {
|
|
2414
2743
|
method: "POST",
|
|
2415
2744
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2416
2745
|
body: "<<<not json>>>",
|
|
@@ -2433,7 +2762,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2433
2762
|
return { passed: false, details: `HTTP ${res.statusCode} \u2014 expected error code -32700` };
|
|
2434
2763
|
});
|
|
2435
2764
|
await test("error-invalid-request-code", "Returns -32600 for invalid request", "errors", false, "basic", async () => {
|
|
2436
|
-
const res = await
|
|
2765
|
+
const res = await request2(backendUrl, {
|
|
2437
2766
|
method: "POST",
|
|
2438
2767
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2439
2768
|
body: JSON.stringify({ jsonrpc: "2.0", id: 99999 }),
|
|
@@ -2598,13 +2927,13 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2598
2927
|
}
|
|
2599
2928
|
);
|
|
2600
2929
|
await test("security-tls-required", "Enforces HTTPS/TLS", "security", false, "basic/authorization", async () => {
|
|
2601
|
-
const parsedUrl = new URL(
|
|
2930
|
+
const parsedUrl = new URL(backendUrl);
|
|
2602
2931
|
if (parsedUrl.protocol !== "https:") {
|
|
2603
2932
|
return { passed: false, details: `Server URL uses ${parsedUrl.protocol} \u2014 production servers should use HTTPS` };
|
|
2604
2933
|
}
|
|
2605
|
-
const httpUrl =
|
|
2934
|
+
const httpUrl = backendUrl.replace(/^https:/, "http:");
|
|
2606
2935
|
try {
|
|
2607
|
-
const res = await
|
|
2936
|
+
const res = await request2(httpUrl, {
|
|
2608
2937
|
method: "POST",
|
|
2609
2938
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2610
2939
|
body: JSON.stringify({ jsonrpc: "2.0", id: 99950, method: "ping" }),
|
|
@@ -2697,10 +3026,10 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2697
3026
|
if (!hasAuth) {
|
|
2698
3027
|
return { passed: true, details: "Skipped: server does not require auth" };
|
|
2699
3028
|
}
|
|
2700
|
-
const parsedUrl = new URL(
|
|
3029
|
+
const parsedUrl = new URL(backendUrl);
|
|
2701
3030
|
const prmUrl = `${parsedUrl.protocol}//${parsedUrl.host}/.well-known/oauth-protected-resource`;
|
|
2702
3031
|
try {
|
|
2703
|
-
const res = await
|
|
3032
|
+
const res = await request2(prmUrl, {
|
|
2704
3033
|
method: "GET",
|
|
2705
3034
|
headers: { Accept: "application/json" },
|
|
2706
3035
|
signal: AbortSignal.timeout(Math.min(timeout, 5e3))
|
|
@@ -2725,7 +3054,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2725
3054
|
}
|
|
2726
3055
|
const legacyUrl = `${parsedUrl.protocol}//${parsedUrl.host}/.well-known/oauth-authorization-server`;
|
|
2727
3056
|
try {
|
|
2728
|
-
const legacyRes = await
|
|
3057
|
+
const legacyRes = await request2(legacyUrl, {
|
|
2729
3058
|
method: "GET",
|
|
2730
3059
|
headers: { Accept: "application/json" },
|
|
2731
3060
|
signal: AbortSignal.timeout(Math.min(timeout, 5e3))
|
|
@@ -2772,7 +3101,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2772
3101
|
if (!token) {
|
|
2773
3102
|
return { passed: true, details: "Skipped: could not extract token from auth header" };
|
|
2774
3103
|
}
|
|
2775
|
-
const uriWithToken = `${
|
|
3104
|
+
const uriWithToken = `${backendUrl}${backendUrl.includes("?") ? "&" : "?"}access_token=${encodeURIComponent(token)}`;
|
|
2776
3105
|
try {
|
|
2777
3106
|
const noAuthHeaders = {};
|
|
2778
3107
|
if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
|
|
@@ -2800,7 +3129,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2800
3129
|
"basic/transports#streamable-http",
|
|
2801
3130
|
async () => {
|
|
2802
3131
|
try {
|
|
2803
|
-
const res = await
|
|
3132
|
+
const res = await request2(backendUrl, {
|
|
2804
3133
|
method: "OPTIONS",
|
|
2805
3134
|
headers: {
|
|
2806
3135
|
Origin: "https://evil.example.com",
|
|
@@ -2837,7 +3166,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2837
3166
|
"basic/transports#streamable-http",
|
|
2838
3167
|
async () => {
|
|
2839
3168
|
try {
|
|
2840
|
-
const res = await
|
|
3169
|
+
const res = await request2(backendUrl, {
|
|
2841
3170
|
method: "POST",
|
|
2842
3171
|
headers: {
|
|
2843
3172
|
"Content-Type": "application/json",
|
|
@@ -3007,7 +3336,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3007
3336
|
async () => {
|
|
3008
3337
|
const largeValue = "A".repeat(1048576);
|
|
3009
3338
|
try {
|
|
3010
|
-
const res = await
|
|
3339
|
+
const res = await request2(backendUrl, {
|
|
3011
3340
|
method: "POST",
|
|
3012
3341
|
headers: {
|
|
3013
3342
|
"Content-Type": "application/json",
|
|
@@ -3209,7 +3538,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3209
3538
|
];
|
|
3210
3539
|
for (const payload of errorPayloads) {
|
|
3211
3540
|
try {
|
|
3212
|
-
const res = await
|
|
3541
|
+
const res = await request2(backendUrl, {
|
|
3213
3542
|
method: "POST",
|
|
3214
3543
|
headers: {
|
|
3215
3544
|
"Content-Type": "application/json",
|
|
@@ -3248,7 +3577,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3248
3577
|
"basic",
|
|
3249
3578
|
async () => {
|
|
3250
3579
|
try {
|
|
3251
|
-
const res = await
|
|
3580
|
+
const res = await request2(backendUrl, {
|
|
3252
3581
|
method: "POST",
|
|
3253
3582
|
headers: {
|
|
3254
3583
|
"Content-Type": "application/json",
|
|
@@ -3312,7 +3641,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3312
3641
|
"basic/transports#streamable-http",
|
|
3313
3642
|
async () => {
|
|
3314
3643
|
const deleteHeaders = { ...buildHeaders() };
|
|
3315
|
-
const res = await
|
|
3644
|
+
const res = await request2(backendUrl, {
|
|
3316
3645
|
method: "DELETE",
|
|
3317
3646
|
headers: deleteHeaders,
|
|
3318
3647
|
signal: AbortSignal.timeout(timeout)
|
|
@@ -3353,17 +3682,81 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3353
3682
|
return { passed: false, details: `HTTP ${res.statusCode}` };
|
|
3354
3683
|
}
|
|
3355
3684
|
);
|
|
3356
|
-
|
|
3685
|
+
await test(
|
|
3686
|
+
"stdio-framing",
|
|
3687
|
+
"Newline-delimited JSON framing",
|
|
3688
|
+
"transport",
|
|
3689
|
+
true,
|
|
3690
|
+
"basic/transports#stdio",
|
|
3691
|
+
async () => {
|
|
3692
|
+
const results = await Promise.all(
|
|
3693
|
+
Array.from({ length: 5 }, () => rpc("ping").catch((e) => ({ _err: e.message })))
|
|
3694
|
+
);
|
|
3695
|
+
const failed = results.filter((r) => "_err" in r);
|
|
3696
|
+
if (failed.length) {
|
|
3697
|
+
return { passed: false, details: `${failed.length}/5 rapid pings failed \u2014 framing likely broken` };
|
|
3698
|
+
}
|
|
3699
|
+
return { passed: true, details: "5/5 rapid pings returned cleanly" };
|
|
3700
|
+
}
|
|
3701
|
+
);
|
|
3702
|
+
await test("stdio-unicode", "UTF-8 unicode roundtrip", "transport", false, "basic/transports#stdio", async () => {
|
|
3703
|
+
const probe = "h\xE9llo \u4E16\u754C \u{1F680}";
|
|
3704
|
+
if (hasTools && toolNames.length > 0) {
|
|
3705
|
+
try {
|
|
3706
|
+
const res2 = await rpc("tools/call", {
|
|
3707
|
+
name: toolNames[0],
|
|
3708
|
+
arguments: { message: probe, text: probe, input: probe, query: probe }
|
|
3709
|
+
});
|
|
3710
|
+
const serialized = JSON.stringify(res2.body);
|
|
3711
|
+
if (serialized.includes(probe)) {
|
|
3712
|
+
return { passed: true, details: "Unicode string round-tripped through tool call" };
|
|
3713
|
+
}
|
|
3714
|
+
return { passed: true, details: "Tool echoed something, but not the exact probe \u2014 likely still UTF-8-safe" };
|
|
3715
|
+
} catch (err) {
|
|
3716
|
+
return { passed: false, details: `tools/call threw \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
const res = await rpc("tools/list");
|
|
3720
|
+
if (res.body.error) {
|
|
3721
|
+
return { passed: false, details: "tools/list returned error" };
|
|
3722
|
+
}
|
|
3723
|
+
return { passed: true, details: "tools/list returned successfully (no tools to probe with unicode)" };
|
|
3724
|
+
});
|
|
3725
|
+
await test(
|
|
3726
|
+
"stdio-unknown-method-recovers",
|
|
3727
|
+
"Recovers after unknown method",
|
|
3728
|
+
"transport",
|
|
3729
|
+
false,
|
|
3730
|
+
"basic/transports#stdio",
|
|
3731
|
+
async () => {
|
|
3732
|
+
const errRes = await rpc("this/method/does/not/exist-xyzzy");
|
|
3733
|
+
const errBody = errRes.body;
|
|
3734
|
+
if (!errBody.error) {
|
|
3735
|
+
return { passed: false, details: "Unknown method did not produce a JSON-RPC error" };
|
|
3736
|
+
}
|
|
3737
|
+
const okRes = await rpc("ping");
|
|
3738
|
+
const okBody = okRes.body;
|
|
3739
|
+
if (okBody.error) {
|
|
3740
|
+
return {
|
|
3741
|
+
passed: false,
|
|
3742
|
+
details: "Server responded with error to ping after unknown method \u2014 may have desynced"
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
return { passed: true, details: "Unknown method returned JSON-RPC error; subsequent ping succeeded" };
|
|
3746
|
+
}
|
|
3747
|
+
);
|
|
3748
|
+
const MAX_WARNINGS = 100;
|
|
3357
3749
|
if (warnings.length > MAX_WARNINGS) {
|
|
3358
3750
|
const truncated = warnings.length - MAX_WARNINGS;
|
|
3359
3751
|
warnings.splice(MAX_WARNINGS, truncated, `... and ${truncated} more warning(s) suppressed`);
|
|
3360
3752
|
}
|
|
3361
3753
|
const { score, grade, overall, summary, categories } = computeScore(tests);
|
|
3362
|
-
const badge = generateBadge(
|
|
3754
|
+
const badge = generateBadge(displayUrl);
|
|
3755
|
+
await transport.close();
|
|
3363
3756
|
return {
|
|
3364
3757
|
specVersion: SPEC_VERSION,
|
|
3365
3758
|
toolVersion: TOOL_VERSION,
|
|
3366
|
-
url,
|
|
3759
|
+
url: displayUrl,
|
|
3367
3760
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3368
3761
|
score,
|
|
3369
3762
|
grade,
|
|
@@ -3387,9 +3780,10 @@ export {
|
|
|
3387
3780
|
generateBadge,
|
|
3388
3781
|
computeGrade,
|
|
3389
3782
|
computeScore,
|
|
3783
|
+
parseSSEResponse,
|
|
3390
3784
|
TEST_DEFINITIONS,
|
|
3391
3785
|
SPEC_VERSION,
|
|
3392
3786
|
SPEC_BASE,
|
|
3393
|
-
|
|
3787
|
+
previewTests,
|
|
3394
3788
|
runComplianceSuite
|
|
3395
3789
|
};
|