mcp-proxy 5.11.2 → 5.12.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.
@@ -81,7 +81,9 @@ const getWWWAuthenticateHeader = (
81
81
 
82
82
  // Add resource_metadata if configured
83
83
  if (oauth.protectedResource?.resource) {
84
- params.push(`resource_metadata="${oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`);
84
+ params.push(
85
+ `resource_metadata="${oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`,
86
+ );
85
87
  }
86
88
 
87
89
  // Add error from options or config (options takes precedence)
@@ -91,7 +93,8 @@ const getWWWAuthenticateHeader = (
91
93
  }
92
94
 
93
95
  // Add error_description from options or config (options takes precedence)
94
- const error_description = options?.error_description || oauth.error_description;
96
+ const error_description =
97
+ options?.error_description || oauth.error_description;
95
98
  if (error_description) {
96
99
  // Escape quotes in error description
97
100
  const escaped = error_description.replace(/"/g, '\\"');
@@ -118,6 +121,30 @@ const getWWWAuthenticateHeader = (
118
121
  return `Bearer ${params.join(", ")}`;
119
122
  };
120
123
 
124
+ // Helper function to detect scope challenge errors
125
+ const isScopeChallengeError = (
126
+ error: unknown,
127
+ ): error is {
128
+ data: {
129
+ error: string;
130
+ errorDescription?: string;
131
+ requiredScopes: string[];
132
+ };
133
+ name: string;
134
+ } => {
135
+ return (
136
+ typeof error === "object" &&
137
+ error !== null &&
138
+ "name" in error &&
139
+ error.name === "InsufficientScopeError" &&
140
+ "data" in error &&
141
+ typeof error.data === "object" &&
142
+ error.data !== null &&
143
+ "error" in error.data &&
144
+ error.data.error === "insufficient_scope"
145
+ );
146
+ };
147
+
121
148
  // Helper function to handle Response errors and send appropriate HTTP response
122
149
  const handleResponseError = async (
123
150
  error: unknown,
@@ -125,11 +152,12 @@ const handleResponseError = async (
125
152
  ): Promise<boolean> => {
126
153
  // Check if it's a Response-like object (duck typing)
127
154
  // 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;
155
+ const isResponseLike =
156
+ error &&
157
+ typeof error === "object" &&
158
+ "status" in error &&
159
+ "headers" in error &&
160
+ "statusText" in error;
133
161
 
134
162
  if (isResponseLike || error instanceof Response) {
135
163
  const responseError = error as Response;
@@ -188,7 +216,8 @@ const applyCorsHeaders = (
188
216
 
189
217
  // Default CORS configuration for backward compatibility
190
218
  const defaultCorsOptions: CorsOptions = {
191
- allowedHeaders: "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id",
219
+ allowedHeaders:
220
+ "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id",
192
221
  credentials: true,
193
222
  exposedHeaders: ["Mcp-Session-Id"],
194
223
  methods: ["GET", "POST", "OPTIONS"],
@@ -224,7 +253,9 @@ const applyCorsHeaders = (
224
253
  ? origin.origin
225
254
  : "false";
226
255
  } else if (typeof finalCorsOptions.origin === "function") {
227
- allowedOrigin = finalCorsOptions.origin(origin.origin) ? origin.origin : "false";
256
+ allowedOrigin = finalCorsOptions.origin(origin.origin)
257
+ ? origin.origin
258
+ : "false";
228
259
  }
229
260
  }
230
261
 
@@ -234,30 +265,43 @@ const applyCorsHeaders = (
234
265
 
235
266
  // Handle credentials
236
267
  if (finalCorsOptions.credentials !== undefined) {
237
- res.setHeader("Access-Control-Allow-Credentials", finalCorsOptions.credentials.toString());
268
+ res.setHeader(
269
+ "Access-Control-Allow-Credentials",
270
+ finalCorsOptions.credentials.toString(),
271
+ );
238
272
  }
239
273
 
240
274
  // Handle methods
241
275
  if (finalCorsOptions.methods) {
242
- res.setHeader("Access-Control-Allow-Methods", finalCorsOptions.methods.join(", "));
276
+ res.setHeader(
277
+ "Access-Control-Allow-Methods",
278
+ finalCorsOptions.methods.join(", "),
279
+ );
243
280
  }
244
281
 
245
282
  // Handle allowed headers
246
283
  if (finalCorsOptions.allowedHeaders) {
247
- const allowedHeaders = typeof finalCorsOptions.allowedHeaders === "string"
248
- ? finalCorsOptions.allowedHeaders
249
- : finalCorsOptions.allowedHeaders.join(", ");
284
+ const allowedHeaders =
285
+ typeof finalCorsOptions.allowedHeaders === "string"
286
+ ? finalCorsOptions.allowedHeaders
287
+ : finalCorsOptions.allowedHeaders.join(", ");
250
288
  res.setHeader("Access-Control-Allow-Headers", allowedHeaders);
251
289
  }
252
290
 
253
291
  // Handle exposed headers
254
292
  if (finalCorsOptions.exposedHeaders) {
255
- res.setHeader("Access-Control-Expose-Headers", finalCorsOptions.exposedHeaders.join(", "));
293
+ res.setHeader(
294
+ "Access-Control-Expose-Headers",
295
+ finalCorsOptions.exposedHeaders.join(", "),
296
+ );
256
297
  }
257
298
 
258
299
  // Handle max age
259
300
  if (finalCorsOptions.maxAge !== undefined) {
260
- res.setHeader("Access-Control-Max-Age", finalCorsOptions.maxAge.toString());
301
+ res.setHeader(
302
+ "Access-Control-Max-Age",
303
+ finalCorsOptions.maxAge.toString(),
304
+ );
261
305
  }
262
306
  } catch (error) {
263
307
  console.error("[mcp-proxy] error parsing origin", error);
@@ -267,6 +311,7 @@ const applyCorsHeaders = (
267
311
  const handleStreamRequest = async <T extends ServerLike>({
268
312
  activeTransports,
269
313
  authenticate,
314
+ authMiddleware,
270
315
  createServer,
271
316
  enableJsonResponse,
272
317
  endpoint,
@@ -283,6 +328,7 @@ const handleStreamRequest = async <T extends ServerLike>({
283
328
  { server: T; transport: StreamableHTTPServerTransport }
284
329
  >;
285
330
  authenticate?: (request: http.IncomingMessage) => Promise<unknown>;
331
+ authMiddleware: AuthenticationMiddleware;
286
332
  createServer: (request: http.IncomingMessage) => Promise<T>;
287
333
  enableJsonResponse?: boolean;
288
334
  endpoint: string;
@@ -298,6 +344,7 @@ const handleStreamRequest = async <T extends ServerLike>({
298
344
  req.method === "POST" &&
299
345
  new URL(req.url!, "http://localhost").pathname === endpoint
300
346
  ) {
347
+ let body: unknown;
301
348
  try {
302
349
  const sessionId = Array.isArray(req.headers["mcp-session-id"])
303
350
  ? req.headers["mcp-session-id"][0]
@@ -307,7 +354,7 @@ const handleStreamRequest = async <T extends ServerLike>({
307
354
 
308
355
  let server: T;
309
356
 
310
- const body = await getBody(req);
357
+ body = await getBody(req);
311
358
 
312
359
  // Per-request authentication in stateless mode
313
360
  if (stateless && authenticate) {
@@ -315,10 +362,18 @@ const handleStreamRequest = async <T extends ServerLike>({
315
362
  const authResult = await authenticate(req);
316
363
 
317
364
  // Check for both falsy AND { authenticated: false } pattern
318
- if (!authResult || (typeof authResult === 'object' && 'authenticated' in authResult && !authResult.authenticated)) {
365
+ if (
366
+ !authResult ||
367
+ (typeof authResult === "object" &&
368
+ "authenticated" in authResult &&
369
+ !authResult.authenticated)
370
+ ) {
319
371
  // Extract error message if available
320
372
  const errorMessage =
321
- authResult && typeof authResult === 'object' && 'error' in authResult && typeof authResult.error === 'string'
373
+ authResult &&
374
+ typeof authResult === "object" &&
375
+ "error" in authResult &&
376
+ typeof authResult.error === "string"
322
377
  ? authResult.error
323
378
  : "Unauthorized: Authentication failed";
324
379
 
@@ -337,11 +392,11 @@ const handleStreamRequest = async <T extends ServerLike>({
337
392
  JSON.stringify({
338
393
  error: {
339
394
  code: -32000,
340
- message: errorMessage
395
+ message: errorMessage,
341
396
  },
342
397
  id: (body as { id?: unknown })?.id ?? null,
343
- jsonrpc: "2.0"
344
- })
398
+ jsonrpc: "2.0",
399
+ }),
345
400
  );
346
401
  return true;
347
402
  }
@@ -352,7 +407,10 @@ const handleStreamRequest = async <T extends ServerLike>({
352
407
  }
353
408
 
354
409
  // Extract error details from thrown errors
355
- const errorMessage = error instanceof Error ? error.message : "Unauthorized: Authentication error";
410
+ const errorMessage =
411
+ error instanceof Error
412
+ ? error.message
413
+ : "Unauthorized: Authentication error";
356
414
  console.error("Authentication error:", error);
357
415
  res.setHeader("Content-Type", "application/json");
358
416
 
@@ -369,11 +427,11 @@ const handleStreamRequest = async <T extends ServerLike>({
369
427
  JSON.stringify({
370
428
  error: {
371
429
  code: -32000,
372
- message: errorMessage
430
+ message: errorMessage,
373
431
  },
374
432
  id: (body as { id?: unknown })?.id ?? null,
375
- jsonrpc: "2.0"
376
- })
433
+ jsonrpc: "2.0",
434
+ }),
377
435
  );
378
436
  return true;
379
437
  }
@@ -439,11 +497,13 @@ const handleStreamRequest = async <T extends ServerLike>({
439
497
  }
440
498
 
441
499
  // Detect authentication errors and return HTTP 401
442
- const errorMessage = error instanceof Error ? error.message : String(error);
443
- const isAuthError = errorMessage.includes('Authentication') ||
444
- errorMessage.includes('Invalid JWT') ||
445
- errorMessage.includes('Token') ||
446
- errorMessage.includes('Unauthorized');
500
+ const errorMessage =
501
+ error instanceof Error ? error.message : String(error);
502
+ const isAuthError =
503
+ errorMessage.includes("Authentication") ||
504
+ errorMessage.includes("Invalid JWT") ||
505
+ errorMessage.includes("Token") ||
506
+ errorMessage.includes("Unauthorized");
447
507
 
448
508
  if (isAuthError) {
449
509
  res.setHeader("Content-Type", "application/json");
@@ -457,14 +517,16 @@ const handleStreamRequest = async <T extends ServerLike>({
457
517
  res.setHeader("WWW-Authenticate", wwwAuthHeader);
458
518
  }
459
519
 
460
- res.writeHead(401).end(JSON.stringify({
461
- error: {
462
- code: -32000,
463
- message: errorMessage
464
- },
465
- id: (body as { id?: unknown })?.id ?? null,
466
- jsonrpc: "2.0"
467
- }));
520
+ res.writeHead(401).end(
521
+ JSON.stringify({
522
+ error: {
523
+ code: -32000,
524
+ message: errorMessage,
525
+ },
526
+ id: (body as { id?: unknown })?.id ?? null,
527
+ jsonrpc: "2.0",
528
+ }),
529
+ );
468
530
  return true;
469
531
  }
470
532
 
@@ -502,11 +564,13 @@ const handleStreamRequest = async <T extends ServerLike>({
502
564
  }
503
565
 
504
566
  // Detect authentication errors and return HTTP 401
505
- const errorMessage = error instanceof Error ? error.message : String(error);
506
- const isAuthError = errorMessage.includes('Authentication') ||
507
- errorMessage.includes('Invalid JWT') ||
508
- errorMessage.includes('Token') ||
509
- errorMessage.includes('Unauthorized');
567
+ const errorMessage =
568
+ error instanceof Error ? error.message : String(error);
569
+ const isAuthError =
570
+ errorMessage.includes("Authentication") ||
571
+ errorMessage.includes("Invalid JWT") ||
572
+ errorMessage.includes("Token") ||
573
+ errorMessage.includes("Unauthorized");
510
574
 
511
575
  if (isAuthError) {
512
576
  res.setHeader("Content-Type", "application/json");
@@ -520,14 +584,16 @@ const handleStreamRequest = async <T extends ServerLike>({
520
584
  res.setHeader("WWW-Authenticate", wwwAuthHeader);
521
585
  }
522
586
 
523
- res.writeHead(401).end(JSON.stringify({
524
- error: {
525
- code: -32000,
526
- message: errorMessage
527
- },
528
- id: (body as { id?: unknown })?.id ?? null,
529
- jsonrpc: "2.0"
530
- }));
587
+ res.writeHead(401).end(
588
+ JSON.stringify({
589
+ error: {
590
+ code: -32000,
591
+ message: errorMessage,
592
+ },
593
+ id: (body as { id?: unknown })?.id ?? null,
594
+ jsonrpc: "2.0",
595
+ }),
596
+ );
531
597
  return true;
532
598
  }
533
599
 
@@ -566,6 +632,19 @@ const handleStreamRequest = async <T extends ServerLike>({
566
632
 
567
633
  return true;
568
634
  } catch (error) {
635
+ // Check for scope challenge errors
636
+ if (isScopeChallengeError(error)) {
637
+ const response = authMiddleware.getScopeChallengeResponse(
638
+ error.data.requiredScopes,
639
+ error.data.errorDescription,
640
+ (body as { id?: unknown })?.id,
641
+ );
642
+
643
+ res.writeHead(response.statusCode, response.headers);
644
+ res.end(response.body);
645
+ return true;
646
+ }
647
+
569
648
  console.error("[mcp-proxy] error handling request", error);
570
649
 
571
650
  res.setHeader("Content-Type", "application/json");
@@ -858,6 +937,7 @@ export const startHTTPServer = async <T extends ServerLike>({
858
937
  (await handleStreamRequest({
859
938
  activeTransports: activeStreamTransports,
860
939
  authenticate,
940
+ authMiddleware,
861
941
  createServer,
862
942
  enableJsonResponse,
863
943
  endpoint: streamEndpoint,
@@ -19,9 +19,8 @@ describe("startStdioServer.test.ts", () => {
19
19
  let proc: ChildProcess;
20
20
 
21
21
  beforeEach(async () => {
22
- const serverPath = require.resolve(
23
- "@modelcontextprotocol/sdk/examples/server/sseAndStreamableHttpCompatibleServer.js",
24
- );
22
+ const serverPath =
23
+ require.resolve("@modelcontextprotocol/sdk/examples/server/sseAndStreamableHttpCompatibleServer.js");
25
24
  proc = fork(serverPath, [], {
26
25
  stdio: "pipe",
27
26
  });
@@ -83,9 +82,11 @@ describe("startStdioServer.test.ts", () => {
83
82
  {
84
83
  description:
85
84
  "Starts sending periodic notifications for testing resumability",
85
+ execution: {
86
+ taskSupport: "forbidden",
87
+ },
86
88
  inputSchema: {
87
89
  $schema: "http://json-schema.org/draft-07/schema#",
88
- additionalProperties: false,
89
90
  properties: {
90
91
  count: {
91
92
  default: 50,
@@ -177,9 +178,11 @@ describe("startStdioServer.test.ts", () => {
177
178
  {
178
179
  description:
179
180
  "Starts sending periodic notifications for testing resumability",
181
+ execution: {
182
+ taskSupport: "forbidden",
183
+ },
180
184
  inputSchema: {
181
185
  $schema: "http://json-schema.org/draft-07/schema#",
182
- additionalProperties: false,
183
186
  properties: {
184
187
  count: {
185
188
  default: 50,