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.
@@ -4,7 +4,10 @@ import {
4
4
  EventStore,
5
5
  StreamableHTTPServerTransport,
6
6
  } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
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?: 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
- : (Array.isArray(req.headers["mcp-session-id"])
357
- ? req.headers["mcp-session-id"][0]
358
- : req.headers["mcp-session-id"]);
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 || new InMemoryEventStore(),
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 || new InMemoryEventStore(),
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
- eventStore?: EventStore;
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) {