mcp-proxy 5.5.6 → 5.6.1
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 +60 -0
- package/dist/bin/mcp-proxy.d.ts +1 -2
- package/dist/bin/mcp-proxy.js +55 -91
- package/dist/bin/mcp-proxy.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +66 -61
- package/dist/index.js.map +1 -1
- package/dist/{stdio-CQWnvum1.js → stdio-D0Lv8ytu.js} +544 -482
- package/dist/stdio-D0Lv8ytu.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +9 -9
- package/src/authentication.test.ts +117 -0
- package/src/authentication.ts +43 -0
- package/src/bin/mcp-proxy.ts +5 -0
- package/src/startHTTPServer.test.ts +374 -0
- package/src/startHTTPServer.ts +13 -0
- package/dist/stdio-CQWnvum1.js.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-proxy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsdown",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@eslint/js": "^9.
|
|
42
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
41
|
+
"@eslint/js": "^9.36.0",
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.18.1",
|
|
43
43
|
"@sebbo2002/semantic-release-jsr": "^3.0.1",
|
|
44
44
|
"@tsconfig/node22": "^22.0.2",
|
|
45
45
|
"@types/express": "^5.0.3",
|
|
46
|
-
"@types/node": "^24.
|
|
46
|
+
"@types/node": "^24.5.2",
|
|
47
47
|
"@types/yargs": "^17.0.33",
|
|
48
|
-
"eslint": "^9.
|
|
48
|
+
"eslint": "^9.36.0",
|
|
49
49
|
"eslint-config-prettier": "^10.1.8",
|
|
50
50
|
"eslint-plugin-perfectionist": "^4.15.0",
|
|
51
51
|
"eventsource": "^4.0.0",
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"jiti": "^2.5.1",
|
|
55
55
|
"jsr": "^0.13.5",
|
|
56
56
|
"prettier": "^3.6.2",
|
|
57
|
-
"semantic-release": "^24.2.
|
|
58
|
-
"tsdown": "^0.
|
|
59
|
-
"tsx": "^4.20.
|
|
57
|
+
"semantic-release": "^24.2.8",
|
|
58
|
+
"tsdown": "^0.15.2",
|
|
59
|
+
"tsx": "^4.20.5",
|
|
60
60
|
"typescript": "^5.9.2",
|
|
61
|
-
"typescript-eslint": "^8.
|
|
61
|
+
"typescript-eslint": "^8.44.0",
|
|
62
62
|
"vitest": "^3.2.4",
|
|
63
63
|
"yargs": "^18.0.0"
|
|
64
64
|
},
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { IncomingMessage } from "http";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { AuthenticationMiddleware } from "./authentication.js";
|
|
5
|
+
|
|
6
|
+
describe("AuthenticationMiddleware", () => {
|
|
7
|
+
const createMockRequest = (headers: Record<string, string> = {}): IncomingMessage => {
|
|
8
|
+
// Simulate Node.js http module behavior which converts all header names to lowercase
|
|
9
|
+
const lowercaseHeaders: Record<string, string> = {};
|
|
10
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
11
|
+
lowercaseHeaders[key.toLowerCase()] = value;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
headers: lowercaseHeaders,
|
|
15
|
+
} as IncomingMessage;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe("when no auth is configured", () => {
|
|
19
|
+
it("should allow all requests", () => {
|
|
20
|
+
const middleware = new AuthenticationMiddleware({});
|
|
21
|
+
const req = createMockRequest();
|
|
22
|
+
|
|
23
|
+
expect(middleware.validateRequest(req)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should allow requests even with headers", () => {
|
|
27
|
+
const middleware = new AuthenticationMiddleware({});
|
|
28
|
+
const req = createMockRequest({ "x-api-key": "some-key" });
|
|
29
|
+
|
|
30
|
+
expect(middleware.validateRequest(req)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("X-API-Key validation", () => {
|
|
35
|
+
const apiKey = "test-api-key-123";
|
|
36
|
+
|
|
37
|
+
it("should accept valid API key", () => {
|
|
38
|
+
const middleware = new AuthenticationMiddleware({ apiKey });
|
|
39
|
+
const req = createMockRequest({ "x-api-key": apiKey });
|
|
40
|
+
|
|
41
|
+
expect(middleware.validateRequest(req)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should reject missing API key", () => {
|
|
45
|
+
const middleware = new AuthenticationMiddleware({ apiKey });
|
|
46
|
+
const req = createMockRequest();
|
|
47
|
+
|
|
48
|
+
expect(middleware.validateRequest(req)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should reject incorrect API key", () => {
|
|
52
|
+
const middleware = new AuthenticationMiddleware({ apiKey });
|
|
53
|
+
const req = createMockRequest({ "x-api-key": "wrong-key" });
|
|
54
|
+
|
|
55
|
+
expect(middleware.validateRequest(req)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should reject empty API key", () => {
|
|
59
|
+
const middleware = new AuthenticationMiddleware({ apiKey });
|
|
60
|
+
const req = createMockRequest({ "x-api-key": "" });
|
|
61
|
+
|
|
62
|
+
expect(middleware.validateRequest(req)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should be case-insensitive for header names", () => {
|
|
66
|
+
const middleware = new AuthenticationMiddleware({ apiKey });
|
|
67
|
+
const req = createMockRequest({ "X-API-KEY": apiKey });
|
|
68
|
+
|
|
69
|
+
expect(middleware.validateRequest(req)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should work with mixed case header names", () => {
|
|
73
|
+
const middleware = new AuthenticationMiddleware({ apiKey });
|
|
74
|
+
const req = createMockRequest({ "X-Api-Key": apiKey });
|
|
75
|
+
|
|
76
|
+
expect(middleware.validateRequest(req)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle array headers (if multiple same headers)", () => {
|
|
80
|
+
const middleware = new AuthenticationMiddleware({ apiKey });
|
|
81
|
+
const req = {
|
|
82
|
+
headers: {
|
|
83
|
+
"x-api-key": [apiKey, "another-key"],
|
|
84
|
+
},
|
|
85
|
+
} as unknown as IncomingMessage;
|
|
86
|
+
|
|
87
|
+
// Should fail because header is an array, not a string
|
|
88
|
+
expect(middleware.validateRequest(req)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("getUnauthorizedResponse", () => {
|
|
93
|
+
it("should return proper unauthorized response", () => {
|
|
94
|
+
const middleware = new AuthenticationMiddleware({ apiKey: "test" });
|
|
95
|
+
const response = middleware.getUnauthorizedResponse();
|
|
96
|
+
|
|
97
|
+
expect(response.headers["Content-Type"]).toBe("application/json");
|
|
98
|
+
|
|
99
|
+
const body = JSON.parse(response.body);
|
|
100
|
+
expect(body.error.code).toBe(401);
|
|
101
|
+
expect(body.error.message).toBe("Unauthorized: Invalid or missing API key");
|
|
102
|
+
expect(body.jsonrpc).toBe("2.0");
|
|
103
|
+
expect(body.id).toBe(null);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should have consistent format regardless of configuration", () => {
|
|
107
|
+
const middleware1 = new AuthenticationMiddleware({});
|
|
108
|
+
const middleware2 = new AuthenticationMiddleware({ apiKey: "test" });
|
|
109
|
+
|
|
110
|
+
const response1 = middleware1.getUnauthorizedResponse();
|
|
111
|
+
const response2 = middleware2.getUnauthorizedResponse();
|
|
112
|
+
|
|
113
|
+
expect(response1.headers).toEqual(response2.headers);
|
|
114
|
+
expect(response1.body).toEqual(response2.body);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IncomingMessage } from "http";
|
|
2
|
+
|
|
3
|
+
export interface AuthConfig {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class AuthenticationMiddleware {
|
|
8
|
+
constructor(private config: AuthConfig = {}) {}
|
|
9
|
+
|
|
10
|
+
getUnauthorizedResponse() {
|
|
11
|
+
return {
|
|
12
|
+
body: JSON.stringify({
|
|
13
|
+
error: {
|
|
14
|
+
code: 401,
|
|
15
|
+
message: "Unauthorized: Invalid or missing API key",
|
|
16
|
+
},
|
|
17
|
+
id: null,
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
}),
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validateRequest(req: IncomingMessage): boolean {
|
|
27
|
+
// No auth required if no API key configured (backward compatibility)
|
|
28
|
+
if (!this.config.apiKey) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check X-API-Key header (case-insensitive)
|
|
33
|
+
// Node.js http module automatically converts all header names to lowercase
|
|
34
|
+
const apiKey = req.headers["x-api-key"];
|
|
35
|
+
|
|
36
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return apiKey === this.config.apiKey;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
package/src/bin/mcp-proxy.ts
CHANGED
|
@@ -38,6 +38,10 @@ const argv = await yargs(hideBin(process.argv))
|
|
|
38
38
|
"populate--": true,
|
|
39
39
|
})
|
|
40
40
|
.options({
|
|
41
|
+
apiKey: {
|
|
42
|
+
describe: "API key for authenticating requests (uses X-API-Key header)",
|
|
43
|
+
type: "string",
|
|
44
|
+
},
|
|
41
45
|
debug: {
|
|
42
46
|
default: false,
|
|
43
47
|
describe: "Enable debug logging",
|
|
@@ -160,6 +164,7 @@ const proxy = async () => {
|
|
|
160
164
|
};
|
|
161
165
|
|
|
162
166
|
const server = await startHTTPServer({
|
|
167
|
+
apiKey: argv.apiKey,
|
|
163
168
|
createServer,
|
|
164
169
|
eventStore: new InMemoryEventStore(),
|
|
165
170
|
host: argv.host,
|
|
@@ -327,3 +327,377 @@ it("supports stateless HTTP streamable transport", async () => {
|
|
|
327
327
|
// Note: in stateless mode, onClose behavior may differ since there's no persistent session
|
|
328
328
|
await delay(100);
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
it("allows requests when no auth is configured", async () => {
|
|
332
|
+
const stdioTransport = new StdioClientTransport({
|
|
333
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
334
|
+
command: "tsx",
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const stdioClient = new Client(
|
|
338
|
+
{
|
|
339
|
+
name: "mcp-proxy",
|
|
340
|
+
version: "1.0.0",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
capabilities: {},
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
await stdioClient.connect(stdioTransport);
|
|
348
|
+
|
|
349
|
+
const serverVersion = stdioClient.getServerVersion() as {
|
|
350
|
+
name: string;
|
|
351
|
+
version: string;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as {
|
|
355
|
+
capabilities: Record<string, unknown>;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const port = await getRandomPort();
|
|
359
|
+
|
|
360
|
+
const httpServer = await startHTTPServer({
|
|
361
|
+
// No apiKey configured
|
|
362
|
+
createServer: async () => {
|
|
363
|
+
const mcpServer = new Server(serverVersion, {
|
|
364
|
+
capabilities: serverCapabilities,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await proxyServer({
|
|
368
|
+
client: stdioClient,
|
|
369
|
+
server: mcpServer,
|
|
370
|
+
serverCapabilities,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return mcpServer;
|
|
374
|
+
},
|
|
375
|
+
port,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const streamClient = new Client(
|
|
379
|
+
{
|
|
380
|
+
name: "stream-client",
|
|
381
|
+
version: "1.0.0",
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
capabilities: {},
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Connect without any authentication header
|
|
389
|
+
const transport = new StreamableHTTPClientTransport(
|
|
390
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
await streamClient.connect(transport);
|
|
394
|
+
|
|
395
|
+
// Should be able to make requests without auth
|
|
396
|
+
const result = await streamClient.listResources();
|
|
397
|
+
expect(result).toEqual({
|
|
398
|
+
resources: [
|
|
399
|
+
{
|
|
400
|
+
name: "Example Resource",
|
|
401
|
+
uri: "file:///example.txt",
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await streamClient.close();
|
|
407
|
+
await httpServer.close();
|
|
408
|
+
await stdioClient.close();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("rejects requests without API key when auth is enabled", async () => {
|
|
412
|
+
const stdioTransport = new StdioClientTransport({
|
|
413
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
414
|
+
command: "tsx",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const stdioClient = new Client(
|
|
418
|
+
{
|
|
419
|
+
name: "mcp-proxy",
|
|
420
|
+
version: "1.0.0",
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
capabilities: {},
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
await stdioClient.connect(stdioTransport);
|
|
428
|
+
|
|
429
|
+
const serverVersion = stdioClient.getServerVersion() as {
|
|
430
|
+
name: string;
|
|
431
|
+
version: string;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as {
|
|
435
|
+
capabilities: Record<string, unknown>;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const port = await getRandomPort();
|
|
439
|
+
|
|
440
|
+
const httpServer = await startHTTPServer({
|
|
441
|
+
apiKey: "test-api-key-123", // API key configured
|
|
442
|
+
createServer: async () => {
|
|
443
|
+
const mcpServer = new Server(serverVersion, {
|
|
444
|
+
capabilities: serverCapabilities,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
await proxyServer({
|
|
448
|
+
client: stdioClient,
|
|
449
|
+
server: mcpServer,
|
|
450
|
+
serverCapabilities,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return mcpServer;
|
|
454
|
+
},
|
|
455
|
+
port,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Try to connect without authentication header
|
|
459
|
+
const transport = new StreamableHTTPClientTransport(
|
|
460
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const streamClient = new Client(
|
|
464
|
+
{
|
|
465
|
+
name: "stream-client",
|
|
466
|
+
version: "1.0.0",
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
capabilities: {},
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Connection should fail due to missing auth
|
|
474
|
+
await expect(streamClient.connect(transport)).rejects.toThrow();
|
|
475
|
+
|
|
476
|
+
await httpServer.close();
|
|
477
|
+
await stdioClient.close();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("accepts requests with valid API key", async () => {
|
|
481
|
+
const stdioTransport = new StdioClientTransport({
|
|
482
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
483
|
+
command: "tsx",
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const stdioClient = new Client(
|
|
487
|
+
{
|
|
488
|
+
name: "mcp-proxy",
|
|
489
|
+
version: "1.0.0",
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
capabilities: {},
|
|
493
|
+
},
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
await stdioClient.connect(stdioTransport);
|
|
497
|
+
|
|
498
|
+
const serverVersion = stdioClient.getServerVersion() as {
|
|
499
|
+
name: string;
|
|
500
|
+
version: string;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as {
|
|
504
|
+
capabilities: Record<string, unknown>;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const port = await getRandomPort();
|
|
508
|
+
const apiKey = "test-api-key-123";
|
|
509
|
+
|
|
510
|
+
const httpServer = await startHTTPServer({
|
|
511
|
+
apiKey,
|
|
512
|
+
createServer: async () => {
|
|
513
|
+
const mcpServer = new Server(serverVersion, {
|
|
514
|
+
capabilities: serverCapabilities,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
await proxyServer({
|
|
518
|
+
client: stdioClient,
|
|
519
|
+
server: mcpServer,
|
|
520
|
+
serverCapabilities,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return mcpServer;
|
|
524
|
+
},
|
|
525
|
+
port,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Connect with proper authentication header
|
|
529
|
+
const transport = new StreamableHTTPClientTransport(
|
|
530
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
531
|
+
{
|
|
532
|
+
requestInit: {
|
|
533
|
+
headers: {
|
|
534
|
+
"X-API-Key": apiKey,
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const streamClient = new Client(
|
|
541
|
+
{
|
|
542
|
+
name: "stream-client",
|
|
543
|
+
version: "1.0.0",
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
capabilities: {},
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
await streamClient.connect(transport);
|
|
551
|
+
|
|
552
|
+
// Should be able to make requests with valid auth
|
|
553
|
+
const result = await streamClient.listResources();
|
|
554
|
+
expect(result).toEqual({
|
|
555
|
+
resources: [
|
|
556
|
+
{
|
|
557
|
+
name: "Example Resource",
|
|
558
|
+
uri: "file:///example.txt",
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await streamClient.close();
|
|
564
|
+
await httpServer.close();
|
|
565
|
+
await stdioClient.close();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("works with SSE transport and authentication", async () => {
|
|
569
|
+
const stdioTransport = new StdioClientTransport({
|
|
570
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
571
|
+
command: "tsx",
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const stdioClient = new Client(
|
|
575
|
+
{
|
|
576
|
+
name: "mcp-proxy",
|
|
577
|
+
version: "1.0.0",
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
capabilities: {},
|
|
581
|
+
},
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
await stdioClient.connect(stdioTransport);
|
|
585
|
+
|
|
586
|
+
const serverVersion = stdioClient.getServerVersion() as {
|
|
587
|
+
name: string;
|
|
588
|
+
version: string;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as {
|
|
592
|
+
capabilities: Record<string, unknown>;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const port = await getRandomPort();
|
|
596
|
+
const apiKey = "test-api-key-456";
|
|
597
|
+
|
|
598
|
+
const httpServer = await startHTTPServer({
|
|
599
|
+
apiKey,
|
|
600
|
+
createServer: async () => {
|
|
601
|
+
const mcpServer = new Server(serverVersion, {
|
|
602
|
+
capabilities: serverCapabilities,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
await proxyServer({
|
|
606
|
+
client: stdioClient,
|
|
607
|
+
server: mcpServer,
|
|
608
|
+
serverCapabilities,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return mcpServer;
|
|
612
|
+
},
|
|
613
|
+
port,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Connect with proper authentication header for SSE
|
|
617
|
+
const transport = new SSEClientTransport(
|
|
618
|
+
new URL(`http://localhost:${port}/sse`),
|
|
619
|
+
{
|
|
620
|
+
requestInit: {
|
|
621
|
+
headers: {
|
|
622
|
+
"X-API-Key": apiKey,
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const sseClient = new Client(
|
|
629
|
+
{
|
|
630
|
+
name: "sse-client",
|
|
631
|
+
version: "1.0.0",
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
capabilities: {},
|
|
635
|
+
},
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
await sseClient.connect(transport);
|
|
639
|
+
|
|
640
|
+
// Should be able to make requests with valid auth
|
|
641
|
+
const result = await sseClient.listResources();
|
|
642
|
+
expect(result).toEqual({
|
|
643
|
+
resources: [
|
|
644
|
+
{
|
|
645
|
+
name: "Example Resource",
|
|
646
|
+
uri: "file:///example.txt",
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await sseClient.close();
|
|
652
|
+
await httpServer.close();
|
|
653
|
+
await stdioClient.close();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("does not require auth for /ping endpoint", async () => {
|
|
657
|
+
const port = await getRandomPort();
|
|
658
|
+
const apiKey = "test-api-key-789";
|
|
659
|
+
|
|
660
|
+
const httpServer = await startHTTPServer({
|
|
661
|
+
apiKey,
|
|
662
|
+
createServer: async () => {
|
|
663
|
+
const mcpServer = new Server(
|
|
664
|
+
{ name: "test", version: "1.0.0" },
|
|
665
|
+
{ capabilities: {} },
|
|
666
|
+
);
|
|
667
|
+
return mcpServer;
|
|
668
|
+
},
|
|
669
|
+
port,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Test /ping without auth header
|
|
673
|
+
const response = await fetch(`http://localhost:${port}/ping`);
|
|
674
|
+
expect(response.status).toBe(200);
|
|
675
|
+
expect(await response.text()).toBe("pong");
|
|
676
|
+
|
|
677
|
+
await httpServer.close();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("does not require auth for OPTIONS requests", async () => {
|
|
681
|
+
const port = await getRandomPort();
|
|
682
|
+
const apiKey = "test-api-key-999";
|
|
683
|
+
|
|
684
|
+
const httpServer = await startHTTPServer({
|
|
685
|
+
apiKey,
|
|
686
|
+
createServer: async () => {
|
|
687
|
+
const mcpServer = new Server(
|
|
688
|
+
{ name: "test", version: "1.0.0" },
|
|
689
|
+
{ capabilities: {} },
|
|
690
|
+
);
|
|
691
|
+
return mcpServer;
|
|
692
|
+
},
|
|
693
|
+
port,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Test OPTIONS without auth header
|
|
697
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
698
|
+
method: "OPTIONS",
|
|
699
|
+
});
|
|
700
|
+
expect(response.status).toBe(204);
|
|
701
|
+
|
|
702
|
+
await httpServer.close();
|
|
703
|
+
});
|
package/src/startHTTPServer.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
8
8
|
import http from "http";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
|
|
11
|
+
import { AuthenticationMiddleware } from "./authentication.js";
|
|
11
12
|
import { InMemoryEventStore } from "./InMemoryEventStore.js";
|
|
12
13
|
|
|
13
14
|
export type SSEServer = {
|
|
@@ -455,6 +456,7 @@ const handleSSERequest = async <T extends ServerLike>({
|
|
|
455
456
|
};
|
|
456
457
|
|
|
457
458
|
export const startHTTPServer = async <T extends ServerLike>({
|
|
459
|
+
apiKey,
|
|
458
460
|
createServer,
|
|
459
461
|
enableJsonResponse,
|
|
460
462
|
eventStore,
|
|
@@ -467,6 +469,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
467
469
|
stateless,
|
|
468
470
|
streamEndpoint = "/mcp",
|
|
469
471
|
}: {
|
|
472
|
+
apiKey?: string;
|
|
470
473
|
createServer: (request: http.IncomingMessage) => Promise<T>;
|
|
471
474
|
enableJsonResponse?: boolean;
|
|
472
475
|
eventStore?: EventStore;
|
|
@@ -492,6 +495,8 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
492
495
|
}
|
|
493
496
|
> = {};
|
|
494
497
|
|
|
498
|
+
const authMiddleware = new AuthenticationMiddleware({ apiKey });
|
|
499
|
+
|
|
495
500
|
/**
|
|
496
501
|
* @author https://dev.classmethod.jp/articles/mcp-sse/
|
|
497
502
|
*/
|
|
@@ -521,6 +526,14 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
521
526
|
return;
|
|
522
527
|
}
|
|
523
528
|
|
|
529
|
+
// Check authentication for all other endpoints
|
|
530
|
+
if (!authMiddleware.validateRequest(req)) {
|
|
531
|
+
const authResponse = authMiddleware.getUnauthorizedResponse();
|
|
532
|
+
res.writeHead(401, authResponse.headers);
|
|
533
|
+
res.end(authResponse.body);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
524
537
|
if (
|
|
525
538
|
sseEndpoint &&
|
|
526
539
|
(await handleSSERequest({
|