mcp-proxy 5.9.0 → 5.11.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.
@@ -11,6 +11,15 @@ import { randomUUID } from "node:crypto";
11
11
  import { AuthConfig, AuthenticationMiddleware } from "./authentication.js";
12
12
  import { InMemoryEventStore } from "./InMemoryEventStore.js";
13
13
 
14
+ export interface CorsOptions {
15
+ allowedHeaders?: string | string[]; // Allow string[] or '*' for wildcard
16
+ credentials?: boolean;
17
+ exposedHeaders?: string[];
18
+ maxAge?: number;
19
+ methods?: string[];
20
+ origin?: ((origin: string) => boolean) | string | string[];
21
+ }
22
+
14
23
  export type SSEServer = {
15
24
  close: () => Promise<void>;
16
25
  };
@@ -52,22 +61,82 @@ const createJsonRpcErrorResponse = (code: number, message: string) => {
52
61
  // Helper function to get WWW-Authenticate header value
53
62
  const getWWWAuthenticateHeader = (
54
63
  oauth?: AuthConfig["oauth"],
64
+ options?: {
65
+ error?: string;
66
+ error_description?: string;
67
+ error_uri?: string;
68
+ scope?: string;
69
+ },
55
70
  ): string | undefined => {
56
- if (!oauth?.protectedResource?.resource) {
71
+ if (!oauth) {
72
+ return undefined;
73
+ }
74
+
75
+ const params: string[] = [];
76
+
77
+ // Add realm if configured
78
+ if (oauth.realm) {
79
+ params.push(`realm="${oauth.realm}"`);
80
+ }
81
+
82
+ // Add resource_metadata if configured
83
+ if (oauth.protectedResource?.resource) {
84
+ params.push(`resource_metadata="${oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`);
85
+ }
86
+
87
+ // Add error from options or config (options takes precedence)
88
+ const error = options?.error || oauth.error;
89
+ if (error) {
90
+ params.push(`error="${error}"`);
91
+ }
92
+
93
+ // Add error_description from options or config (options takes precedence)
94
+ const error_description = options?.error_description || oauth.error_description;
95
+ if (error_description) {
96
+ // Escape quotes in error description
97
+ const escaped = error_description.replace(/"/g, '\\"');
98
+ params.push(`error_description="${escaped}"`);
99
+ }
100
+
101
+ // Add error_uri from options or config (options takes precedence)
102
+ const error_uri = options?.error_uri || oauth.error_uri;
103
+ if (error_uri) {
104
+ params.push(`error_uri="${error_uri}"`);
105
+ }
106
+
107
+ // Add scope from options or config (options takes precedence)
108
+ const scope = options?.scope || oauth.scope;
109
+ if (scope) {
110
+ params.push(`scope="${scope}"`);
111
+ }
112
+
113
+ // Return undefined if no parameters were added
114
+ if (params.length === 0) {
57
115
  return undefined;
58
116
  }
59
117
 
60
- return `Bearer resource_metadata="${oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`;
118
+ return `Bearer ${params.join(", ")}`;
61
119
  };
62
120
 
63
121
  // Helper function to handle Response errors and send appropriate HTTP response
64
- const handleResponseError = (
122
+ const handleResponseError = async (
65
123
  error: unknown,
66
124
  res: http.ServerResponse,
67
- ): boolean => {
68
- if (error instanceof Response) {
125
+ ): Promise<boolean> => {
126
+ // Check if it's a Response-like object (duck typing)
127
+ // The instanceof check may fail due to different Response implementations across module boundaries
128
+ const isResponseLike = error &&
129
+ typeof error === 'object' &&
130
+ 'status' in error &&
131
+ 'headers' in error &&
132
+ 'statusText' in error;
133
+
134
+ if (isResponseLike || error instanceof Response) {
135
+ const responseError = error as Response;
136
+
137
+ // Convert Headers to http.OutgoingHttpHeaders format
69
138
  const fixedHeaders: http.OutgoingHttpHeaders = {};
70
- error.headers.forEach((value, key) => {
139
+ responseError.headers.forEach((value, key) => {
71
140
  if (fixedHeaders[key]) {
72
141
  if (Array.isArray(fixedHeaders[key])) {
73
142
  (fixedHeaders[key] as string[]).push(value);
@@ -78,11 +147,16 @@ const handleResponseError = (
78
147
  fixedHeaders[key] = value;
79
148
  }
80
149
  });
81
- res
82
- .writeHead(error.status, error.statusText, fixedHeaders)
83
- .end(error.statusText);
150
+
151
+ // Read the body from the Response object
152
+ const body = await responseError.text();
153
+
154
+ res.writeHead(responseError.status, responseError.statusText, fixedHeaders);
155
+ res.end(body);
156
+
84
157
  return true;
85
158
  }
159
+
86
160
  return false;
87
161
  };
88
162
 
@@ -102,6 +176,94 @@ const cleanupServer = async <T extends ServerLike>(
102
176
  }
103
177
  };
104
178
 
179
+ // Helper function to apply CORS headers
180
+ const applyCorsHeaders = (
181
+ req: http.IncomingMessage,
182
+ res: http.ServerResponse,
183
+ corsOptions?: boolean | CorsOptions,
184
+ ) => {
185
+ if (!req.headers.origin) {
186
+ return;
187
+ }
188
+
189
+ // Default CORS configuration for backward compatibility
190
+ const defaultCorsOptions: CorsOptions = {
191
+ allowedHeaders: "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id",
192
+ credentials: true,
193
+ exposedHeaders: ["Mcp-Session-Id"],
194
+ methods: ["GET", "POST", "OPTIONS"],
195
+ origin: "*",
196
+ };
197
+
198
+ let finalCorsOptions: CorsOptions;
199
+
200
+ if (corsOptions === false) {
201
+ // CORS disabled
202
+ return;
203
+ } else if (corsOptions === true || corsOptions === undefined) {
204
+ // Use default CORS settings
205
+ finalCorsOptions = defaultCorsOptions;
206
+ } else {
207
+ // Merge user options with defaults
208
+ finalCorsOptions = {
209
+ ...defaultCorsOptions,
210
+ ...corsOptions,
211
+ };
212
+ }
213
+
214
+ try {
215
+ const origin = new URL(req.headers.origin);
216
+
217
+ // Handle origin
218
+ let allowedOrigin = "*";
219
+ if (finalCorsOptions.origin) {
220
+ if (typeof finalCorsOptions.origin === "string") {
221
+ allowedOrigin = finalCorsOptions.origin;
222
+ } else if (Array.isArray(finalCorsOptions.origin)) {
223
+ allowedOrigin = finalCorsOptions.origin.includes(origin.origin)
224
+ ? origin.origin
225
+ : "false";
226
+ } else if (typeof finalCorsOptions.origin === "function") {
227
+ allowedOrigin = finalCorsOptions.origin(origin.origin) ? origin.origin : "false";
228
+ }
229
+ }
230
+
231
+ if (allowedOrigin !== "false") {
232
+ res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
233
+ }
234
+
235
+ // Handle credentials
236
+ if (finalCorsOptions.credentials !== undefined) {
237
+ res.setHeader("Access-Control-Allow-Credentials", finalCorsOptions.credentials.toString());
238
+ }
239
+
240
+ // Handle methods
241
+ if (finalCorsOptions.methods) {
242
+ res.setHeader("Access-Control-Allow-Methods", finalCorsOptions.methods.join(", "));
243
+ }
244
+
245
+ // Handle allowed headers
246
+ if (finalCorsOptions.allowedHeaders) {
247
+ const allowedHeaders = typeof finalCorsOptions.allowedHeaders === "string"
248
+ ? finalCorsOptions.allowedHeaders
249
+ : finalCorsOptions.allowedHeaders.join(", ");
250
+ res.setHeader("Access-Control-Allow-Headers", allowedHeaders);
251
+ }
252
+
253
+ // Handle exposed headers
254
+ if (finalCorsOptions.exposedHeaders) {
255
+ res.setHeader("Access-Control-Expose-Headers", finalCorsOptions.exposedHeaders.join(", "));
256
+ }
257
+
258
+ // Handle max age
259
+ if (finalCorsOptions.maxAge !== undefined) {
260
+ res.setHeader("Access-Control-Max-Age", finalCorsOptions.maxAge.toString());
261
+ }
262
+ } catch (error) {
263
+ console.error("[mcp-proxy] error parsing origin", error);
264
+ }
265
+ };
266
+
105
267
  const handleStreamRequest = async <T extends ServerLike>({
106
268
  activeTransports,
107
269
  authenticate,
@@ -163,7 +325,10 @@ const handleStreamRequest = async <T extends ServerLike>({
163
325
  res.setHeader("Content-Type", "application/json");
164
326
 
165
327
  // Add WWW-Authenticate header if OAuth config is available
166
- const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
328
+ const wwwAuthHeader = getWWWAuthenticateHeader(oauth, {
329
+ error: "invalid_token",
330
+ error_description: errorMessage,
331
+ });
167
332
  if (wwwAuthHeader) {
168
333
  res.setHeader("WWW-Authenticate", wwwAuthHeader);
169
334
  }
@@ -181,13 +346,21 @@ const handleStreamRequest = async <T extends ServerLike>({
181
346
  return true;
182
347
  }
183
348
  } catch (error) {
349
+ // Check if error is a Response object with headers already set
350
+ if (await handleResponseError(error, res)) {
351
+ return true;
352
+ }
353
+
184
354
  // Extract error details from thrown errors
185
355
  const errorMessage = error instanceof Error ? error.message : "Unauthorized: Authentication error";
186
356
  console.error("Authentication error:", error);
187
357
  res.setHeader("Content-Type", "application/json");
188
358
 
189
359
  // Add WWW-Authenticate header if OAuth config is available
190
- const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
360
+ const wwwAuthHeader = getWWWAuthenticateHeader(oauth, {
361
+ error: "invalid_token",
362
+ error_description: errorMessage,
363
+ });
191
364
  if (wwwAuthHeader) {
192
365
  res.setHeader("WWW-Authenticate", wwwAuthHeader);
193
366
  }
@@ -260,6 +433,11 @@ const handleStreamRequest = async <T extends ServerLike>({
260
433
  try {
261
434
  server = await createServer(req);
262
435
  } catch (error) {
436
+ // Check if error is a Response object with headers already set
437
+ if (await handleResponseError(error, res)) {
438
+ return true;
439
+ }
440
+
263
441
  // Detect authentication errors and return HTTP 401
264
442
  const errorMessage = error instanceof Error ? error.message : String(error);
265
443
  const isAuthError = errorMessage.includes('Authentication') ||
@@ -271,7 +449,10 @@ const handleStreamRequest = async <T extends ServerLike>({
271
449
  res.setHeader("Content-Type", "application/json");
272
450
 
273
451
  // Add WWW-Authenticate header if OAuth config is available
274
- const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
452
+ const wwwAuthHeader = getWWWAuthenticateHeader(oauth, {
453
+ error: "invalid_token",
454
+ error_description: errorMessage,
455
+ });
275
456
  if (wwwAuthHeader) {
276
457
  res.setHeader("WWW-Authenticate", wwwAuthHeader);
277
458
  }
@@ -287,10 +468,6 @@ const handleStreamRequest = async <T extends ServerLike>({
287
468
  return true;
288
469
  }
289
470
 
290
- if (handleResponseError(error, res)) {
291
- return true;
292
- }
293
-
294
471
  res.writeHead(500).end("Error creating server");
295
472
 
296
473
  return true;
@@ -319,6 +496,11 @@ const handleStreamRequest = async <T extends ServerLike>({
319
496
  try {
320
497
  server = await createServer(req);
321
498
  } catch (error) {
499
+ // Check if error is a Response object with headers already set
500
+ if (await handleResponseError(error, res)) {
501
+ return true;
502
+ }
503
+
322
504
  // Detect authentication errors and return HTTP 401
323
505
  const errorMessage = error instanceof Error ? error.message : String(error);
324
506
  const isAuthError = errorMessage.includes('Authentication') ||
@@ -330,7 +512,10 @@ const handleStreamRequest = async <T extends ServerLike>({
330
512
  res.setHeader("Content-Type", "application/json");
331
513
 
332
514
  // Add WWW-Authenticate header if OAuth config is available
333
- const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
515
+ const wwwAuthHeader = getWWWAuthenticateHeader(oauth, {
516
+ error: "invalid_token",
517
+ error_description: errorMessage,
518
+ });
334
519
  if (wwwAuthHeader) {
335
520
  res.setHeader("WWW-Authenticate", wwwAuthHeader);
336
521
  }
@@ -346,10 +531,6 @@ const handleStreamRequest = async <T extends ServerLike>({
346
531
  return true;
347
532
  }
348
533
 
349
- if (handleResponseError(error, res)) {
350
- return true;
351
- }
352
-
353
534
  res.writeHead(500).end("Error creating server");
354
535
 
355
536
  return true;
@@ -504,7 +685,7 @@ const handleSSERequest = async <T extends ServerLike>({
504
685
  try {
505
686
  server = await createServer(req);
506
687
  } catch (error) {
507
- if (handleResponseError(error, res)) {
688
+ if (await handleResponseError(error, res)) {
508
689
  return true;
509
690
  }
510
691
 
@@ -586,6 +767,7 @@ const handleSSERequest = async <T extends ServerLike>({
586
767
  export const startHTTPServer = async <T extends ServerLike>({
587
768
  apiKey,
588
769
  authenticate,
770
+ cors,
589
771
  createServer,
590
772
  enableJsonResponse,
591
773
  eventStore,
@@ -601,6 +783,7 @@ export const startHTTPServer = async <T extends ServerLike>({
601
783
  }: {
602
784
  apiKey?: string;
603
785
  authenticate?: (request: http.IncomingMessage) => Promise<unknown>;
786
+ cors?: boolean | CorsOptions;
604
787
  createServer: (request: http.IncomingMessage) => Promise<T>;
605
788
  enableJsonResponse?: boolean;
606
789
  eventStore?: EventStore;
@@ -633,19 +816,8 @@ export const startHTTPServer = async <T extends ServerLike>({
633
816
  * @author https://dev.classmethod.jp/articles/mcp-sse/
634
817
  */
635
818
  const httpServer = http.createServer(async (req, res) => {
636
- if (req.headers.origin) {
637
- try {
638
- const origin = new URL(req.headers.origin);
639
-
640
- res.setHeader("Access-Control-Allow-Origin", origin.origin);
641
- res.setHeader("Access-Control-Allow-Credentials", "true");
642
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
643
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id");
644
- res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
645
- } catch (error) {
646
- console.error("[mcp-proxy] error parsing origin", error);
647
- }
648
- }
819
+ // Apply CORS headers
820
+ applyCorsHeaders(req, res, cors);
649
821
 
650
822
  if (req.method === "OPTIONS") {
651
823
  res.writeHead(204);