mcp-proxy 6.5.3 → 6.5.5
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 +23 -2
- package/dist/bin/mcp-proxy.mjs +17 -2
- package/dist/bin/mcp-proxy.mjs.map +1 -1
- package/dist/index.d.mts +41 -2
- package/dist/index.mjs +1 -1
- package/dist/{stdio-DNR9B0BZ.mjs → stdio-Be0Fx2iV.mjs} +99 -5
- package/dist/stdio-Be0Fx2iV.mjs.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/InMemoryEventStore.test.ts +96 -0
- package/src/InMemoryEventStore.ts +51 -0
- package/src/bin/mcp-proxy.ts +21 -2
- package/src/index.ts +2 -1
- package/src/startHTTPServer.test.ts +205 -0
- package/src/startHTTPServer.ts +145 -10
- package/dist/stdio-DNR9B0BZ.mjs.map +0 -1
package/src/startHTTPServer.ts
CHANGED
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
EventStore,
|
|
5
5
|
StreamableHTTPServerTransport,
|
|
6
6
|
} from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isInitializeRequest,
|
|
9
|
+
JSONRPCMessageSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
11
|
import fs from "fs";
|
|
9
12
|
import http from "http";
|
|
10
13
|
import https from "https";
|
|
@@ -15,6 +18,25 @@ import { InMemoryEventStore } from "./InMemoryEventStore.js";
|
|
|
15
18
|
|
|
16
19
|
const DEFAULT_KEEP_ALIVE_TIMEOUT = 300_000;
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* `false` disables the resumability event store entirely (no replay-on-
|
|
23
|
+
* reconnect, no retained state). Omitted/`undefined` creates a fresh,
|
|
24
|
+
* bounded `InMemoryEventStore` per session - see `eventStoreMaxEvents`.
|
|
25
|
+
* Pass an `EventStore` instance to use a shared or custom-backed store.
|
|
26
|
+
*/
|
|
27
|
+
export type EventStoreOption = EventStore | false;
|
|
28
|
+
|
|
29
|
+
const resolveEventStore = (
|
|
30
|
+
eventStore: EventStoreOption | undefined,
|
|
31
|
+
maxEvents: number | undefined,
|
|
32
|
+
): EventStore | undefined => {
|
|
33
|
+
if (eventStore === false) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return eventStore ?? new InMemoryEventStore({ maxEvents });
|
|
38
|
+
};
|
|
39
|
+
|
|
18
40
|
export interface CorsOptions {
|
|
19
41
|
allowedHeaders?: string | string[]; // Allow string[] or '*' for wildcard
|
|
20
42
|
credentials?: boolean;
|
|
@@ -62,6 +84,35 @@ const createJsonRpcErrorResponse = (code: number, message: string) => {
|
|
|
62
84
|
});
|
|
63
85
|
};
|
|
64
86
|
|
|
87
|
+
type SessionUnauthorizedResponseOptions = {
|
|
88
|
+
readonly body?: unknown;
|
|
89
|
+
readonly oauth?: AuthConfig["oauth"];
|
|
90
|
+
readonly res: http.ServerResponse;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const getRequestId = (body: unknown): unknown => {
|
|
94
|
+
if (
|
|
95
|
+
typeof body !== "object" ||
|
|
96
|
+
body === null ||
|
|
97
|
+
Array.isArray(body) ||
|
|
98
|
+
!("id" in body)
|
|
99
|
+
) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return body.id;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const isJsonRpcMessage = (message: unknown): boolean => {
|
|
107
|
+
return JSONRPCMessageSchema.safeParse(message).success;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isJsonRpcBody = (body: unknown): boolean => {
|
|
111
|
+
return Array.isArray(body)
|
|
112
|
+
? body.every(isJsonRpcMessage)
|
|
113
|
+
: isJsonRpcMessage(body);
|
|
114
|
+
};
|
|
115
|
+
|
|
65
116
|
// Helper function to get WWW-Authenticate header value
|
|
66
117
|
const getWWWAuthenticateHeader = (
|
|
67
118
|
oauth?: AuthConfig["oauth"],
|
|
@@ -125,6 +176,35 @@ const getWWWAuthenticateHeader = (
|
|
|
125
176
|
return `Bearer ${params.join(", ")}`;
|
|
126
177
|
};
|
|
127
178
|
|
|
179
|
+
const sendSessionUnauthorizedResponse = ({
|
|
180
|
+
body,
|
|
181
|
+
oauth,
|
|
182
|
+
res,
|
|
183
|
+
}: SessionUnauthorizedResponseOptions): void => {
|
|
184
|
+
const message = "Unauthorized: No valid session ID provided";
|
|
185
|
+
|
|
186
|
+
res.setHeader("Content-Type", "application/json");
|
|
187
|
+
|
|
188
|
+
const wwwAuthHeader = getWWWAuthenticateHeader(oauth, {
|
|
189
|
+
error: "invalid_token",
|
|
190
|
+
error_description: message,
|
|
191
|
+
});
|
|
192
|
+
if (wwwAuthHeader) {
|
|
193
|
+
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
res.writeHead(401).end(
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
error: {
|
|
199
|
+
code: -32000,
|
|
200
|
+
message,
|
|
201
|
+
},
|
|
202
|
+
id: getRequestId(body),
|
|
203
|
+
jsonrpc: "2.0",
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
128
208
|
// Helper function to detect scope challenge errors
|
|
129
209
|
const isScopeChallengeError = (
|
|
130
210
|
error: unknown,
|
|
@@ -320,6 +400,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
320
400
|
enableJsonResponse,
|
|
321
401
|
endpoint,
|
|
322
402
|
eventStore,
|
|
403
|
+
eventStoreMaxEvents,
|
|
323
404
|
oauth,
|
|
324
405
|
onClose,
|
|
325
406
|
onConnect,
|
|
@@ -336,7 +417,8 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
336
417
|
createServer: (request: http.IncomingMessage) => Promise<T>;
|
|
337
418
|
enableJsonResponse?: boolean;
|
|
338
419
|
endpoint: string;
|
|
339
|
-
eventStore?:
|
|
420
|
+
eventStore?: EventStoreOption;
|
|
421
|
+
eventStoreMaxEvents?: number;
|
|
340
422
|
oauth?: AuthConfig["oauth"];
|
|
341
423
|
onClose?: (server: T) => Promise<void>;
|
|
342
424
|
onConnect?: (server: T) => Promise<void>;
|
|
@@ -353,9 +435,9 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
353
435
|
// In stateless mode, ignore session ID header entirely (like Python MCP SDK)
|
|
354
436
|
const sessionId = stateless
|
|
355
437
|
? undefined
|
|
356
|
-
:
|
|
357
|
-
|
|
358
|
-
|
|
438
|
+
: Array.isArray(req.headers["mcp-session-id"])
|
|
439
|
+
? req.headers["mcp-session-id"][0]
|
|
440
|
+
: req.headers["mcp-session-id"];
|
|
359
441
|
|
|
360
442
|
let transport: StreamableHTTPServerTransport;
|
|
361
443
|
|
|
@@ -449,6 +531,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
449
531
|
if (sessionId) {
|
|
450
532
|
const activeTransport = activeTransports[sessionId];
|
|
451
533
|
if (!activeTransport) {
|
|
534
|
+
if (authenticate && isJsonRpcBody(body)) {
|
|
535
|
+
sendSessionUnauthorizedResponse({ body, oauth, res });
|
|
536
|
+
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
452
540
|
res.setHeader("Content-Type", "application/json");
|
|
453
541
|
res
|
|
454
542
|
.writeHead(404)
|
|
@@ -476,7 +564,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
476
564
|
// Create a new transport for the session
|
|
477
565
|
transport = new StreamableHTTPServerTransport({
|
|
478
566
|
enableJsonResponse,
|
|
479
|
-
eventStore: eventStore
|
|
567
|
+
eventStore: resolveEventStore(eventStore, eventStoreMaxEvents),
|
|
480
568
|
onsessioninitialized: (_sessionId) => {
|
|
481
569
|
// add only when the id Session id is generated (skip in stateless mode)
|
|
482
570
|
if (!stateless && _sessionId) {
|
|
@@ -570,7 +658,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
570
658
|
// In stateless mode, handle non-initialize requests by creating a new transport
|
|
571
659
|
transport = new StreamableHTTPServerTransport({
|
|
572
660
|
enableJsonResponse,
|
|
573
|
-
eventStore: eventStore
|
|
661
|
+
eventStore: resolveEventStore(eventStore, eventStoreMaxEvents),
|
|
574
662
|
onsessioninitialized: () => {
|
|
575
663
|
// No session tracking in stateless mode
|
|
576
664
|
},
|
|
@@ -634,6 +722,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
634
722
|
|
|
635
723
|
return true;
|
|
636
724
|
} else {
|
|
725
|
+
if (authenticate && isJsonRpcBody(body)) {
|
|
726
|
+
sendSessionUnauthorizedResponse({ body, oauth, res });
|
|
727
|
+
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
637
731
|
// Error if the server is not created but the request is not an initialize request
|
|
638
732
|
res.setHeader("Content-Type", "application/json");
|
|
639
733
|
|
|
@@ -690,7 +784,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
690
784
|
}
|
|
691
785
|
| undefined = sessionId ? activeTransports[sessionId] : undefined;
|
|
692
786
|
|
|
693
|
-
if (!sessionId) {
|
|
787
|
+
if (!sessionId) {
|
|
694
788
|
// Return METHOD_NOT_ALLOWED so stateless clients' transport stops reconnecting
|
|
695
789
|
if (stateless) {
|
|
696
790
|
res.writeHead(405, { Allow: "POST" }).end("Method Not Allowed");
|
|
@@ -698,12 +792,24 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
698
792
|
return true;
|
|
699
793
|
}
|
|
700
794
|
|
|
795
|
+
if (authenticate) {
|
|
796
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
797
|
+
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
|
|
701
801
|
res.writeHead(400).end("No sessionId");
|
|
702
802
|
|
|
703
803
|
return true;
|
|
704
804
|
}
|
|
705
805
|
|
|
706
806
|
if (!activeTransport) {
|
|
807
|
+
if (authenticate) {
|
|
808
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
809
|
+
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
|
|
707
813
|
res.writeHead(400).end("No active transport");
|
|
708
814
|
|
|
709
815
|
return true;
|
|
@@ -735,6 +841,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
735
841
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
736
842
|
|
|
737
843
|
if (!sessionId) {
|
|
844
|
+
if (authenticate) {
|
|
845
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
846
|
+
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
|
|
738
850
|
res.writeHead(400).end("Invalid or missing sessionId");
|
|
739
851
|
|
|
740
852
|
return true;
|
|
@@ -745,6 +857,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
745
857
|
const activeTransport = activeTransports[sessionId];
|
|
746
858
|
|
|
747
859
|
if (!activeTransport) {
|
|
860
|
+
if (authenticate) {
|
|
861
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
862
|
+
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
|
|
748
866
|
res.writeHead(400).end("No active transport");
|
|
749
867
|
return true;
|
|
750
868
|
}
|
|
@@ -882,6 +1000,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
882
1000
|
createServer,
|
|
883
1001
|
enableJsonResponse,
|
|
884
1002
|
eventStore,
|
|
1003
|
+
eventStoreMaxEvents,
|
|
885
1004
|
host = "::",
|
|
886
1005
|
keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT,
|
|
887
1006
|
oauth,
|
|
@@ -901,7 +1020,22 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
901
1020
|
cors?: boolean | CorsOptions;
|
|
902
1021
|
createServer: (request: http.IncomingMessage) => Promise<T>;
|
|
903
1022
|
enableJsonResponse?: boolean;
|
|
904
|
-
|
|
1023
|
+
/**
|
|
1024
|
+
* Event store for the streamable HTTP transport's resumability support.
|
|
1025
|
+
* Pass `false` to disable resumability entirely (recommended for
|
|
1026
|
+
* request/response-only deployments that don't need replay-on-reconnect).
|
|
1027
|
+
* Omit to get a fresh, bounded `InMemoryEventStore` per session - see
|
|
1028
|
+
* `eventStoreMaxEvents`. Pass an `EventStore` instance to bring your own
|
|
1029
|
+
* (e.g. a persistent, cross-process store); it will be shared across all
|
|
1030
|
+
* sessions handled by this server.
|
|
1031
|
+
*/
|
|
1032
|
+
eventStore?: EventStoreOption;
|
|
1033
|
+
/**
|
|
1034
|
+
* Caps how many events the auto-created per-session `InMemoryEventStore`
|
|
1035
|
+
* retains (oldest evicted first) before it's overridden by an explicit
|
|
1036
|
+
* `eventStore`. Bounds memory for long-lived sessions. Default: 1000.
|
|
1037
|
+
*/
|
|
1038
|
+
eventStoreMaxEvents?: number;
|
|
905
1039
|
host?: string;
|
|
906
1040
|
keepAliveTimeout?: number;
|
|
907
1041
|
oauth?: AuthConfig["oauth"];
|
|
@@ -1010,6 +1144,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
1010
1144
|
enableJsonResponse,
|
|
1011
1145
|
endpoint: streamEndpoint,
|
|
1012
1146
|
eventStore,
|
|
1147
|
+
eventStoreMaxEvents,
|
|
1013
1148
|
oauth,
|
|
1014
1149
|
onClose,
|
|
1015
1150
|
onConnect,
|
|
@@ -1024,7 +1159,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
1024
1159
|
res.writeHead(404).end();
|
|
1025
1160
|
};
|
|
1026
1161
|
|
|
1027
|
-
let httpServer;
|
|
1162
|
+
let httpServer: http.Server | https.Server;
|
|
1028
1163
|
if (sslCa || sslCert || sslKey) {
|
|
1029
1164
|
const options: https.ServerOptions = {};
|
|
1030
1165
|
if (sslCa) {
|