@bryan-thompson/inspector-assessment-cli 1.25.9 → 1.26.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.
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Transport Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for HTTP transport functionality with actual MCP servers.
|
|
5
|
+
* Tests include server connection, MCP protocol communication, error handling,
|
|
6
|
+
* and HTTP-specific features like headers and status codes.
|
|
7
|
+
*
|
|
8
|
+
* Note: Tests skip gracefully when testbed servers are unavailable.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeAll } from "@jest/globals";
|
|
11
|
+
import { createTransport } from "../transport.js";
|
|
12
|
+
// Testbed server URLs
|
|
13
|
+
const VULNERABLE_MCP_URL = "http://localhost:10900/mcp";
|
|
14
|
+
const HARDENED_MCP_URL = "http://localhost:10901/mcp";
|
|
15
|
+
const UNAVAILABLE_URL = "http://localhost:19999/mcp";
|
|
16
|
+
/**
|
|
17
|
+
* Default headers required by MCP HTTP servers
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_HEADERS = {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Accept: "application/json, text/event-stream",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Check if a server is available by sending a basic HTTP request
|
|
25
|
+
*/
|
|
26
|
+
async function checkServerAvailable(url) {
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(url, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: DEFAULT_HEADERS,
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
method: "initialize",
|
|
34
|
+
params: {
|
|
35
|
+
protocolVersion: "2024-11-05",
|
|
36
|
+
capabilities: {},
|
|
37
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
38
|
+
},
|
|
39
|
+
id: 1,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
// Accept any response (200 or error) as indication server is up
|
|
43
|
+
return response.status < 500;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse SSE response to extract JSON data
|
|
51
|
+
* MCP streamable HTTP returns Server-Sent Events format
|
|
52
|
+
*/
|
|
53
|
+
async function parseSSEResponse(response) {
|
|
54
|
+
const text = await response.text();
|
|
55
|
+
// If it's plain JSON, parse directly
|
|
56
|
+
if (text.trim().startsWith("{")) {
|
|
57
|
+
return JSON.parse(text);
|
|
58
|
+
}
|
|
59
|
+
// Parse SSE format: "event: message\ndata: {...}\n\n"
|
|
60
|
+
const lines = text.split("\n");
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (line.startsWith("data:")) {
|
|
63
|
+
const jsonStr = line.slice(5).trim();
|
|
64
|
+
if (jsonStr) {
|
|
65
|
+
return JSON.parse(jsonStr);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Unable to parse SSE response: ${text.slice(0, 100)}`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Send an MCP JSON-RPC request and parse response
|
|
73
|
+
*/
|
|
74
|
+
async function sendMcpRequest(url, method, params = {}, headers = {}) {
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
...DEFAULT_HEADERS,
|
|
79
|
+
...headers,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
jsonrpc: "2.0",
|
|
83
|
+
method,
|
|
84
|
+
params,
|
|
85
|
+
id: Date.now(),
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
let data = null;
|
|
89
|
+
if (response.ok) {
|
|
90
|
+
try {
|
|
91
|
+
data = await parseSSEResponse(response.clone());
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Response might not be parseable
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { response, data };
|
|
98
|
+
}
|
|
99
|
+
describe("HTTP Transport Integration", () => {
|
|
100
|
+
let vulnerableServerAvailable = false;
|
|
101
|
+
let hardenedServerAvailable = false;
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
vulnerableServerAvailable = await checkServerAvailable(VULNERABLE_MCP_URL);
|
|
104
|
+
hardenedServerAvailable = await checkServerAvailable(HARDENED_MCP_URL);
|
|
105
|
+
if (!vulnerableServerAvailable && !hardenedServerAvailable) {
|
|
106
|
+
console.log("\n⚠️ Skipping HTTP transport integration tests - no testbed servers available");
|
|
107
|
+
console.log(" Start servers with:");
|
|
108
|
+
console.log(" - vulnerable-mcp: http://localhost:10900/mcp");
|
|
109
|
+
console.log(" - hardened-mcp: http://localhost:10901/mcp\n");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
describe("HTTP Transport Creation (Unit-level)", () => {
|
|
113
|
+
it("should create transport with valid HTTP URL", () => {
|
|
114
|
+
const options = {
|
|
115
|
+
transportType: "http",
|
|
116
|
+
url: "http://localhost:3000/mcp",
|
|
117
|
+
};
|
|
118
|
+
const transport = createTransport(options);
|
|
119
|
+
expect(transport).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
it("should create transport with custom headers", () => {
|
|
122
|
+
const options = {
|
|
123
|
+
transportType: "http",
|
|
124
|
+
url: "http://localhost:3000/mcp",
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: "Bearer test-token",
|
|
127
|
+
"X-API-Key": "secret",
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const transport = createTransport(options);
|
|
131
|
+
expect(transport).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
it("should create transport with HTTPS URL", () => {
|
|
134
|
+
const options = {
|
|
135
|
+
transportType: "http",
|
|
136
|
+
url: "https://api.example.com/mcp",
|
|
137
|
+
};
|
|
138
|
+
const transport = createTransport(options);
|
|
139
|
+
expect(transport).toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
it("should throw error when URL is missing", () => {
|
|
142
|
+
const options = {
|
|
143
|
+
transportType: "http",
|
|
144
|
+
};
|
|
145
|
+
expect(() => createTransport(options)).toThrow(/URL must be provided for SSE or HTTP transport types/);
|
|
146
|
+
});
|
|
147
|
+
it("should throw error for invalid URL format", () => {
|
|
148
|
+
const options = {
|
|
149
|
+
transportType: "http",
|
|
150
|
+
url: ":::invalid",
|
|
151
|
+
};
|
|
152
|
+
expect(() => createTransport(options)).toThrow(/Failed to create transport/);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe("Server Connection Tests (Integration)", () => {
|
|
156
|
+
it("should connect to vulnerable-mcp server", async () => {
|
|
157
|
+
if (!vulnerableServerAvailable) {
|
|
158
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { response, data } = await sendMcpRequest(VULNERABLE_MCP_URL, "initialize", {
|
|
162
|
+
protocolVersion: "2024-11-05",
|
|
163
|
+
capabilities: {},
|
|
164
|
+
clientInfo: {
|
|
165
|
+
name: "inspector-test",
|
|
166
|
+
version: "1.0.0",
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
expect(response.ok).toBe(true);
|
|
170
|
+
expect(response.status).toBe(200);
|
|
171
|
+
expect(data).toHaveProperty("jsonrpc", "2.0");
|
|
172
|
+
expect(data).toHaveProperty("result");
|
|
173
|
+
});
|
|
174
|
+
it("should connect to hardened-mcp server", async () => {
|
|
175
|
+
if (!hardenedServerAvailable) {
|
|
176
|
+
console.log("⏩ Skipping: hardened-mcp not available");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const { response, data } = await sendMcpRequest(HARDENED_MCP_URL, "initialize", {
|
|
180
|
+
protocolVersion: "2024-11-05",
|
|
181
|
+
capabilities: {},
|
|
182
|
+
clientInfo: {
|
|
183
|
+
name: "inspector-test",
|
|
184
|
+
version: "1.0.0",
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
expect(response.ok).toBe(true);
|
|
188
|
+
expect(response.status).toBe(200);
|
|
189
|
+
expect(data).toHaveProperty("jsonrpc", "2.0");
|
|
190
|
+
expect(data).toHaveProperty("result");
|
|
191
|
+
});
|
|
192
|
+
it("should handle connection to unavailable port", async () => {
|
|
193
|
+
try {
|
|
194
|
+
await sendMcpRequest(UNAVAILABLE_URL, "initialize", {});
|
|
195
|
+
// Should not reach here
|
|
196
|
+
expect(true).toBe(false);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// Expected to throw connection error
|
|
200
|
+
expect(error).toBeDefined();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe("MCP Protocol Communication", () => {
|
|
205
|
+
it("should receive capabilities from initialize request", async () => {
|
|
206
|
+
if (!vulnerableServerAvailable) {
|
|
207
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const { data } = await sendMcpRequest(VULNERABLE_MCP_URL, "initialize", {
|
|
211
|
+
protocolVersion: "2024-11-05",
|
|
212
|
+
capabilities: {},
|
|
213
|
+
clientInfo: {
|
|
214
|
+
name: "inspector-test",
|
|
215
|
+
version: "1.0.0",
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
expect(data).toBeDefined();
|
|
219
|
+
const result = data.result;
|
|
220
|
+
expect(result).toHaveProperty("capabilities");
|
|
221
|
+
expect(result).toHaveProperty("serverInfo");
|
|
222
|
+
expect(result.serverInfo).toHaveProperty("name");
|
|
223
|
+
});
|
|
224
|
+
it("should list available tools", async () => {
|
|
225
|
+
if (!vulnerableServerAvailable) {
|
|
226
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// First initialize the session
|
|
230
|
+
const { response: initResponse } = await sendMcpRequest(VULNERABLE_MCP_URL, "initialize", {
|
|
231
|
+
protocolVersion: "2024-11-05",
|
|
232
|
+
capabilities: {},
|
|
233
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
234
|
+
});
|
|
235
|
+
if (!initResponse.ok) {
|
|
236
|
+
console.log("⏩ Skipping: server initialization failed");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Now list tools
|
|
240
|
+
const { response, data } = await sendMcpRequest(VULNERABLE_MCP_URL, "tools/list");
|
|
241
|
+
// Server may require session state; if not OK, skip
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
console.log("⏩ Skipping: tools/list requires session state");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
expect(data).toBeDefined();
|
|
247
|
+
const result = data.result;
|
|
248
|
+
expect(result).toHaveProperty("tools");
|
|
249
|
+
expect(Array.isArray(result.tools)).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
it("should handle malformed request", async () => {
|
|
252
|
+
if (!vulnerableServerAvailable) {
|
|
253
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const response = await fetch(VULNERABLE_MCP_URL, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: DEFAULT_HEADERS,
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
jsonrpc: "2.0",
|
|
261
|
+
method: "invalid_method_name",
|
|
262
|
+
id: 1,
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
// Try to parse SSE response
|
|
266
|
+
let data = null;
|
|
267
|
+
try {
|
|
268
|
+
data = await parseSSEResponse(response.clone());
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Response may not be parseable
|
|
272
|
+
}
|
|
273
|
+
// Should either return error in JSON-RPC format or HTTP error
|
|
274
|
+
if (response.ok && data) {
|
|
275
|
+
expect(data).toHaveProperty("error");
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
expect(response.status).toBeGreaterThanOrEqual(400);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
it("should handle missing required parameters", async () => {
|
|
282
|
+
if (!vulnerableServerAvailable) {
|
|
283
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const { data } = await sendMcpRequest(VULNERABLE_MCP_URL, "initialize");
|
|
287
|
+
expect(data).toBeDefined();
|
|
288
|
+
expect(data).toHaveProperty("jsonrpc", "2.0");
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
describe("HTTP Error Handling", () => {
|
|
292
|
+
it("should handle connection timeout", async () => {
|
|
293
|
+
const timeoutUrl = "http://localhost:19998/mcp";
|
|
294
|
+
try {
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
|
297
|
+
await fetch(timeoutUrl, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: DEFAULT_HEADERS,
|
|
300
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
|
|
301
|
+
signal: controller.signal,
|
|
302
|
+
});
|
|
303
|
+
clearTimeout(timeoutId);
|
|
304
|
+
expect(true).toBe(false); // Should not reach here
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
expect(error).toBeDefined();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
it("should detect non-JSON response", async () => {
|
|
311
|
+
if (!vulnerableServerAvailable) {
|
|
312
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Send a GET request which might return HTML or non-JSON
|
|
316
|
+
try {
|
|
317
|
+
const response = await fetch(VULNERABLE_MCP_URL, { method: "GET" });
|
|
318
|
+
const text = await response.text();
|
|
319
|
+
// Verify it's not JSON by trying to parse
|
|
320
|
+
if (text.trim().startsWith("{") || text.trim().startsWith("[")) {
|
|
321
|
+
// Valid JSON
|
|
322
|
+
expect(JSON.parse(text)).toBeDefined();
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// Non-JSON response expected
|
|
326
|
+
expect(text.length).toBeGreaterThan(0);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
// Accept connection errors for GET requests
|
|
331
|
+
expect(error).toBeDefined();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
describe("Header Handling", () => {
|
|
336
|
+
it("should send Content-Type header correctly", async () => {
|
|
337
|
+
if (!vulnerableServerAvailable) {
|
|
338
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const { response } = await sendMcpRequest(VULNERABLE_MCP_URL, "initialize", {
|
|
342
|
+
protocolVersion: "2024-11-05",
|
|
343
|
+
capabilities: {},
|
|
344
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
345
|
+
}, { "Content-Type": "application/json" });
|
|
346
|
+
expect(response.ok).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
it("should send custom headers", async () => {
|
|
349
|
+
if (!vulnerableServerAvailable) {
|
|
350
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const { response } = await sendMcpRequest(VULNERABLE_MCP_URL, "initialize", {
|
|
354
|
+
protocolVersion: "2024-11-05",
|
|
355
|
+
capabilities: {},
|
|
356
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
357
|
+
}, {
|
|
358
|
+
"X-Test-Header": "test-value",
|
|
359
|
+
"X-Client-ID": "integration-test",
|
|
360
|
+
});
|
|
361
|
+
// Server should accept request even with custom headers
|
|
362
|
+
expect(response.status).toBeLessThan(500);
|
|
363
|
+
});
|
|
364
|
+
it("should handle response headers", async () => {
|
|
365
|
+
if (!vulnerableServerAvailable) {
|
|
366
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const { response } = await sendMcpRequest(VULNERABLE_MCP_URL, "tools/list");
|
|
370
|
+
// Check that response has standard headers
|
|
371
|
+
expect(response.headers.get("content-type")).toBeTruthy();
|
|
372
|
+
});
|
|
373
|
+
it("should require Accept header with proper values", async () => {
|
|
374
|
+
if (!vulnerableServerAvailable) {
|
|
375
|
+
console.log("⏩ Skipping: vulnerable-mcp not available");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Test without Accept header
|
|
379
|
+
const responseWithoutAccept = await fetch(VULNERABLE_MCP_URL, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: {
|
|
382
|
+
"Content-Type": "application/json",
|
|
383
|
+
},
|
|
384
|
+
body: JSON.stringify({
|
|
385
|
+
jsonrpc: "2.0",
|
|
386
|
+
method: "initialize",
|
|
387
|
+
params: {
|
|
388
|
+
protocolVersion: "2024-11-05",
|
|
389
|
+
capabilities: {},
|
|
390
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
391
|
+
},
|
|
392
|
+
id: 1,
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
// Should fail without proper Accept header
|
|
396
|
+
expect(responseWithoutAccept.ok).toBe(false);
|
|
397
|
+
// Test with correct Accept header
|
|
398
|
+
const { response: responseWithAccept } = await sendMcpRequest(VULNERABLE_MCP_URL, "initialize", {
|
|
399
|
+
protocolVersion: "2024-11-05",
|
|
400
|
+
capabilities: {},
|
|
401
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
402
|
+
});
|
|
403
|
+
// Should succeed with proper headers
|
|
404
|
+
expect(responseWithAccept.ok).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
describe("Transport Type Detection", () => {
|
|
408
|
+
it("should recognize HTTP URLs", () => {
|
|
409
|
+
const httpUrls = [
|
|
410
|
+
"http://localhost:3000/mcp",
|
|
411
|
+
"http://127.0.0.1:8080/api",
|
|
412
|
+
"http://example.com/mcp",
|
|
413
|
+
];
|
|
414
|
+
httpUrls.forEach((url) => {
|
|
415
|
+
const options = {
|
|
416
|
+
transportType: "http",
|
|
417
|
+
url,
|
|
418
|
+
};
|
|
419
|
+
const transport = createTransport(options);
|
|
420
|
+
expect(transport).toBeDefined();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
it("should recognize HTTPS URLs", () => {
|
|
424
|
+
const httpsUrls = [
|
|
425
|
+
"https://api.example.com/mcp",
|
|
426
|
+
"https://localhost:3000/secure",
|
|
427
|
+
"https://mcp.service.com/api",
|
|
428
|
+
];
|
|
429
|
+
httpsUrls.forEach((url) => {
|
|
430
|
+
const options = {
|
|
431
|
+
transportType: "http",
|
|
432
|
+
url,
|
|
433
|
+
};
|
|
434
|
+
const transport = createTransport(options);
|
|
435
|
+
expect(transport).toBeDefined();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
it("should handle URLs with ports", () => {
|
|
439
|
+
const urlsWithPorts = [
|
|
440
|
+
"http://localhost:10900/mcp",
|
|
441
|
+
"https://example.com:8443/api",
|
|
442
|
+
"http://127.0.0.1:3000/mcp",
|
|
443
|
+
];
|
|
444
|
+
urlsWithPorts.forEach((url) => {
|
|
445
|
+
const options = {
|
|
446
|
+
transportType: "http",
|
|
447
|
+
url,
|
|
448
|
+
};
|
|
449
|
+
const transport = createTransport(options);
|
|
450
|
+
expect(transport).toBeDefined();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|