@yawlabs/mcp-compliance 0.8.1 → 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-4RNSPRSQ.js → chunk-T26GLXNK.js} +541 -148
- package/dist/index.js +1241 -182
- package/dist/mcp/server.js +23 -4
- package/dist/runner.d.ts +42 -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,7 +2401,6 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2071
2401
|
}
|
|
2072
2402
|
);
|
|
2073
2403
|
}
|
|
2074
|
-
const hasResources = !!serverInfo.capabilities.resources;
|
|
2075
2404
|
const resourcesCap = serverInfo.capabilities.resources;
|
|
2076
2405
|
const hasSubscribe = !!(typeof resourcesCap === "object" && resourcesCap !== null && "subscribe" in resourcesCap && resourcesCap.subscribe);
|
|
2077
2406
|
if (hasResources) {
|
|
@@ -2235,7 +2564,6 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2235
2564
|
);
|
|
2236
2565
|
}
|
|
2237
2566
|
}
|
|
2238
|
-
const hasPrompts = !!serverInfo.capabilities.prompts;
|
|
2239
2567
|
if (hasPrompts) {
|
|
2240
2568
|
let cachedPromptsList = null;
|
|
2241
2569
|
await test(
|
|
@@ -2350,7 +2678,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2350
2678
|
}
|
|
2351
2679
|
);
|
|
2352
2680
|
await test("error-invalid-jsonrpc", "Handles malformed JSON-RPC", "errors", true, "basic", async () => {
|
|
2353
|
-
const res = await
|
|
2681
|
+
const res = await request2(backendUrl, {
|
|
2354
2682
|
method: "POST",
|
|
2355
2683
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2356
2684
|
body: JSON.stringify({ not: "a valid jsonrpc message" }),
|
|
@@ -2373,7 +2701,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2373
2701
|
return { passed: false, details: `HTTP ${res.statusCode} \u2014 expected JSON-RPC error or 4xx status` };
|
|
2374
2702
|
});
|
|
2375
2703
|
await test("error-invalid-json", "Handles invalid JSON body", "errors", false, "basic", async () => {
|
|
2376
|
-
const res = await
|
|
2704
|
+
const res = await request2(backendUrl, {
|
|
2377
2705
|
method: "POST",
|
|
2378
2706
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2379
2707
|
body: "{this is not valid json!!!",
|
|
@@ -2411,7 +2739,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2411
2739
|
}
|
|
2412
2740
|
);
|
|
2413
2741
|
await test("error-parse-code", "Returns -32700 for invalid JSON", "errors", false, "basic", async () => {
|
|
2414
|
-
const res = await
|
|
2742
|
+
const res = await request2(backendUrl, {
|
|
2415
2743
|
method: "POST",
|
|
2416
2744
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2417
2745
|
body: "<<<not json>>>",
|
|
@@ -2434,7 +2762,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2434
2762
|
return { passed: false, details: `HTTP ${res.statusCode} \u2014 expected error code -32700` };
|
|
2435
2763
|
});
|
|
2436
2764
|
await test("error-invalid-request-code", "Returns -32600 for invalid request", "errors", false, "basic", async () => {
|
|
2437
|
-
const res = await
|
|
2765
|
+
const res = await request2(backendUrl, {
|
|
2438
2766
|
method: "POST",
|
|
2439
2767
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...buildHeaders() },
|
|
2440
2768
|
body: JSON.stringify({ jsonrpc: "2.0", id: 99999 }),
|
|
@@ -2599,13 +2927,13 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2599
2927
|
}
|
|
2600
2928
|
);
|
|
2601
2929
|
await test("security-tls-required", "Enforces HTTPS/TLS", "security", false, "basic/authorization", async () => {
|
|
2602
|
-
const parsedUrl = new URL(
|
|
2930
|
+
const parsedUrl = new URL(backendUrl);
|
|
2603
2931
|
if (parsedUrl.protocol !== "https:") {
|
|
2604
2932
|
return { passed: false, details: `Server URL uses ${parsedUrl.protocol} \u2014 production servers should use HTTPS` };
|
|
2605
2933
|
}
|
|
2606
|
-
const httpUrl =
|
|
2934
|
+
const httpUrl = backendUrl.replace(/^https:/, "http:");
|
|
2607
2935
|
try {
|
|
2608
|
-
const res = await
|
|
2936
|
+
const res = await request2(httpUrl, {
|
|
2609
2937
|
method: "POST",
|
|
2610
2938
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2611
2939
|
body: JSON.stringify({ jsonrpc: "2.0", id: 99950, method: "ping" }),
|
|
@@ -2698,10 +3026,10 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2698
3026
|
if (!hasAuth) {
|
|
2699
3027
|
return { passed: true, details: "Skipped: server does not require auth" };
|
|
2700
3028
|
}
|
|
2701
|
-
const parsedUrl = new URL(
|
|
3029
|
+
const parsedUrl = new URL(backendUrl);
|
|
2702
3030
|
const prmUrl = `${parsedUrl.protocol}//${parsedUrl.host}/.well-known/oauth-protected-resource`;
|
|
2703
3031
|
try {
|
|
2704
|
-
const res = await
|
|
3032
|
+
const res = await request2(prmUrl, {
|
|
2705
3033
|
method: "GET",
|
|
2706
3034
|
headers: { Accept: "application/json" },
|
|
2707
3035
|
signal: AbortSignal.timeout(Math.min(timeout, 5e3))
|
|
@@ -2726,7 +3054,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2726
3054
|
}
|
|
2727
3055
|
const legacyUrl = `${parsedUrl.protocol}//${parsedUrl.host}/.well-known/oauth-authorization-server`;
|
|
2728
3056
|
try {
|
|
2729
|
-
const legacyRes = await
|
|
3057
|
+
const legacyRes = await request2(legacyUrl, {
|
|
2730
3058
|
method: "GET",
|
|
2731
3059
|
headers: { Accept: "application/json" },
|
|
2732
3060
|
signal: AbortSignal.timeout(Math.min(timeout, 5e3))
|
|
@@ -2773,7 +3101,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2773
3101
|
if (!token) {
|
|
2774
3102
|
return { passed: true, details: "Skipped: could not extract token from auth header" };
|
|
2775
3103
|
}
|
|
2776
|
-
const uriWithToken = `${
|
|
3104
|
+
const uriWithToken = `${backendUrl}${backendUrl.includes("?") ? "&" : "?"}access_token=${encodeURIComponent(token)}`;
|
|
2777
3105
|
try {
|
|
2778
3106
|
const noAuthHeaders = {};
|
|
2779
3107
|
if (sessionId) noAuthHeaders["mcp-session-id"] = sessionId;
|
|
@@ -2801,7 +3129,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2801
3129
|
"basic/transports#streamable-http",
|
|
2802
3130
|
async () => {
|
|
2803
3131
|
try {
|
|
2804
|
-
const res = await
|
|
3132
|
+
const res = await request2(backendUrl, {
|
|
2805
3133
|
method: "OPTIONS",
|
|
2806
3134
|
headers: {
|
|
2807
3135
|
Origin: "https://evil.example.com",
|
|
@@ -2838,7 +3166,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
2838
3166
|
"basic/transports#streamable-http",
|
|
2839
3167
|
async () => {
|
|
2840
3168
|
try {
|
|
2841
|
-
const res = await
|
|
3169
|
+
const res = await request2(backendUrl, {
|
|
2842
3170
|
method: "POST",
|
|
2843
3171
|
headers: {
|
|
2844
3172
|
"Content-Type": "application/json",
|
|
@@ -3008,7 +3336,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3008
3336
|
async () => {
|
|
3009
3337
|
const largeValue = "A".repeat(1048576);
|
|
3010
3338
|
try {
|
|
3011
|
-
const res = await
|
|
3339
|
+
const res = await request2(backendUrl, {
|
|
3012
3340
|
method: "POST",
|
|
3013
3341
|
headers: {
|
|
3014
3342
|
"Content-Type": "application/json",
|
|
@@ -3210,7 +3538,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3210
3538
|
];
|
|
3211
3539
|
for (const payload of errorPayloads) {
|
|
3212
3540
|
try {
|
|
3213
|
-
const res = await
|
|
3541
|
+
const res = await request2(backendUrl, {
|
|
3214
3542
|
method: "POST",
|
|
3215
3543
|
headers: {
|
|
3216
3544
|
"Content-Type": "application/json",
|
|
@@ -3249,7 +3577,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3249
3577
|
"basic",
|
|
3250
3578
|
async () => {
|
|
3251
3579
|
try {
|
|
3252
|
-
const res = await
|
|
3580
|
+
const res = await request2(backendUrl, {
|
|
3253
3581
|
method: "POST",
|
|
3254
3582
|
headers: {
|
|
3255
3583
|
"Content-Type": "application/json",
|
|
@@ -3313,7 +3641,7 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3313
3641
|
"basic/transports#streamable-http",
|
|
3314
3642
|
async () => {
|
|
3315
3643
|
const deleteHeaders = { ...buildHeaders() };
|
|
3316
|
-
const res = await
|
|
3644
|
+
const res = await request2(backendUrl, {
|
|
3317
3645
|
method: "DELETE",
|
|
3318
3646
|
headers: deleteHeaders,
|
|
3319
3647
|
signal: AbortSignal.timeout(timeout)
|
|
@@ -3354,17 +3682,81 @@ async function runComplianceSuite(url, options = {}) {
|
|
|
3354
3682
|
return { passed: false, details: `HTTP ${res.statusCode}` };
|
|
3355
3683
|
}
|
|
3356
3684
|
);
|
|
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
|
+
);
|
|
3357
3748
|
const MAX_WARNINGS = 100;
|
|
3358
3749
|
if (warnings.length > MAX_WARNINGS) {
|
|
3359
3750
|
const truncated = warnings.length - MAX_WARNINGS;
|
|
3360
3751
|
warnings.splice(MAX_WARNINGS, truncated, `... and ${truncated} more warning(s) suppressed`);
|
|
3361
3752
|
}
|
|
3362
3753
|
const { score, grade, overall, summary, categories } = computeScore(tests);
|
|
3363
|
-
const badge = generateBadge(
|
|
3754
|
+
const badge = generateBadge(displayUrl);
|
|
3755
|
+
await transport.close();
|
|
3364
3756
|
return {
|
|
3365
3757
|
specVersion: SPEC_VERSION,
|
|
3366
3758
|
toolVersion: TOOL_VERSION,
|
|
3367
|
-
url,
|
|
3759
|
+
url: displayUrl,
|
|
3368
3760
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3369
3761
|
score,
|
|
3370
3762
|
grade,
|
|
@@ -3388,9 +3780,10 @@ export {
|
|
|
3388
3780
|
generateBadge,
|
|
3389
3781
|
computeGrade,
|
|
3390
3782
|
computeScore,
|
|
3783
|
+
parseSSEResponse,
|
|
3391
3784
|
TEST_DEFINITIONS,
|
|
3392
3785
|
SPEC_VERSION,
|
|
3393
3786
|
SPEC_BASE,
|
|
3394
|
-
|
|
3787
|
+
previewTests,
|
|
3395
3788
|
runComplianceSuite
|
|
3396
3789
|
};
|