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.
- package/README.md +71 -49
- package/dist/bin/mcp-proxy.js +31 -35
- package/dist/bin/mcp-proxy.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +516 -295
- package/dist/index.js.map +1 -1
- package/dist/{stdio-DQCs94rj.js → stdio-BAyQuMKu.js} +11135 -8848
- package/dist/stdio-BAyQuMKu.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +19 -19
- package/src/InMemoryEventStore.test.ts +58 -50
- package/src/InMemoryEventStore.ts +2 -3
- package/src/authentication.test.ts +275 -18
- package/src/authentication.ts +57 -5
- package/src/bin/mcp-proxy.ts +2 -1
- package/src/proxyServer.test.ts +40 -22
- package/src/startHTTPServer.test.ts +41 -23
- package/src/startHTTPServer.ts +132 -52
- package/src/startStdioServer.test.ts +8 -5
- package/dist/stdio-DQCs94rj.js.map +0 -1
package/src/startHTTPServer.ts
CHANGED
|
@@ -81,7 +81,9 @@ const getWWWAuthenticateHeader = (
|
|
|
81
81
|
|
|
82
82
|
// Add resource_metadata if configured
|
|
83
83
|
if (oauth.protectedResource?.resource) {
|
|
84
|
-
params.push(
|
|
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 =
|
|
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 =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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:
|
|
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)
|
|
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(
|
|
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(
|
|
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 =
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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 &&
|
|
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 =
|
|
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 =
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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(
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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 =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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 =
|
|
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,
|