@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
+ });