mcp-proxy 5.11.1 → 5.12.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.
- package/dist/bin/mcp-proxy.js +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/dist/{stdio-DLSsHME0.js → stdio-CfAxSAGj.js} +51 -5
- package/dist/stdio-CfAxSAGj.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/authentication.test.ts +195 -0
- package/src/authentication.ts +45 -0
- package/src/startHTTPServer.ts +42 -3
- package/dist/stdio-DLSsHME0.js.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -327,4 +327,199 @@ describe("AuthenticationMiddleware", () => {
|
|
|
327
327
|
expect(header.indexOf('error=')).toBeLessThan(header.indexOf('error_description='));
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
describe("getScopeChallengeResponse", () => {
|
|
332
|
+
it("should return 403 status code", () => {
|
|
333
|
+
const middleware = new AuthenticationMiddleware({
|
|
334
|
+
oauth: {
|
|
335
|
+
protectedResource: {
|
|
336
|
+
resource: "https://example.com",
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
const response = middleware.getScopeChallengeResponse(["read", "write"]);
|
|
341
|
+
|
|
342
|
+
expect(response.statusCode).toBe(403);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should include required scopes in WWW-Authenticate header", () => {
|
|
346
|
+
const middleware = new AuthenticationMiddleware({
|
|
347
|
+
oauth: {
|
|
348
|
+
protectedResource: {
|
|
349
|
+
resource: "https://example.com",
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
const response = middleware.getScopeChallengeResponse(["read", "write"]);
|
|
354
|
+
|
|
355
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error="insufficient_scope"');
|
|
356
|
+
expect(response.headers["WWW-Authenticate"]).toContain('scope="read write"');
|
|
357
|
+
expect(response.headers["WWW-Authenticate"]).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should include error_description in WWW-Authenticate header", () => {
|
|
361
|
+
const middleware = new AuthenticationMiddleware({
|
|
362
|
+
oauth: {
|
|
363
|
+
protectedResource: {
|
|
364
|
+
resource: "https://example.com",
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
const response = middleware.getScopeChallengeResponse(
|
|
369
|
+
["admin"],
|
|
370
|
+
"Admin access required",
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error_description="Admin access required"');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("should escape quotes in error_description", () => {
|
|
377
|
+
const middleware = new AuthenticationMiddleware({
|
|
378
|
+
oauth: {
|
|
379
|
+
protectedResource: {
|
|
380
|
+
resource: "https://example.com",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
const response = middleware.getScopeChallengeResponse(
|
|
385
|
+
["admin"],
|
|
386
|
+
'Requires "admin" scope',
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error_description="Requires \\"admin\\" scope"');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should include request ID in response body", () => {
|
|
393
|
+
const middleware = new AuthenticationMiddleware({
|
|
394
|
+
oauth: {
|
|
395
|
+
protectedResource: {
|
|
396
|
+
resource: "https://example.com",
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
const response = middleware.getScopeChallengeResponse(["read"], undefined, 123);
|
|
401
|
+
|
|
402
|
+
const body = JSON.parse(response.body);
|
|
403
|
+
expect(body.id).toBe(123);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("should include required scopes in response body data", () => {
|
|
407
|
+
const middleware = new AuthenticationMiddleware({
|
|
408
|
+
oauth: {
|
|
409
|
+
protectedResource: {
|
|
410
|
+
resource: "https://example.com",
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
const response = middleware.getScopeChallengeResponse(["read", "write"]);
|
|
415
|
+
|
|
416
|
+
const body = JSON.parse(response.body);
|
|
417
|
+
expect(body.error.code).toBe(-32001);
|
|
418
|
+
expect(body.error.message).toBe("Insufficient scope");
|
|
419
|
+
expect(body.error.data.error).toBe("insufficient_scope");
|
|
420
|
+
expect(body.error.data.required_scopes).toEqual(["read", "write"]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should use custom error_description in response body message", () => {
|
|
424
|
+
const middleware = new AuthenticationMiddleware({
|
|
425
|
+
oauth: {
|
|
426
|
+
protectedResource: {
|
|
427
|
+
resource: "https://example.com",
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
const response = middleware.getScopeChallengeResponse(
|
|
432
|
+
["admin"],
|
|
433
|
+
"Admin privileges required",
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const body = JSON.parse(response.body);
|
|
437
|
+
expect(body.error.message).toBe("Admin privileges required");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("should handle single scope", () => {
|
|
441
|
+
const middleware = new AuthenticationMiddleware({
|
|
442
|
+
oauth: {
|
|
443
|
+
protectedResource: {
|
|
444
|
+
resource: "https://example.com",
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
const response = middleware.getScopeChallengeResponse(["admin"]);
|
|
449
|
+
|
|
450
|
+
expect(response.headers["WWW-Authenticate"]).toContain('scope="admin"');
|
|
451
|
+
const body = JSON.parse(response.body);
|
|
452
|
+
expect(body.error.data.required_scopes).toEqual(["admin"]);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("should handle multiple scopes", () => {
|
|
456
|
+
const middleware = new AuthenticationMiddleware({
|
|
457
|
+
oauth: {
|
|
458
|
+
protectedResource: {
|
|
459
|
+
resource: "https://example.com",
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
const response = middleware.getScopeChallengeResponse(["read", "write", "admin"]);
|
|
464
|
+
|
|
465
|
+
expect(response.headers["WWW-Authenticate"]).toContain('scope="read write admin"');
|
|
466
|
+
const body = JSON.parse(response.body);
|
|
467
|
+
expect(body.error.data.required_scopes).toEqual(["read", "write", "admin"]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("should not include WWW-Authenticate header without OAuth config", () => {
|
|
471
|
+
const middleware = new AuthenticationMiddleware({});
|
|
472
|
+
const response = middleware.getScopeChallengeResponse(["admin"]);
|
|
473
|
+
|
|
474
|
+
expect(response.headers["WWW-Authenticate"]).toBeUndefined();
|
|
475
|
+
expect(response.headers["Content-Type"]).toBe("application/json");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("should return proper JSON-RPC 2.0 format", () => {
|
|
479
|
+
const middleware = new AuthenticationMiddleware({
|
|
480
|
+
oauth: {
|
|
481
|
+
protectedResource: {
|
|
482
|
+
resource: "https://example.com",
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
const response = middleware.getScopeChallengeResponse(["read"], "Description", "req-123");
|
|
487
|
+
|
|
488
|
+
const body = JSON.parse(response.body);
|
|
489
|
+
expect(body.jsonrpc).toBe("2.0");
|
|
490
|
+
expect(body.id).toBe("req-123");
|
|
491
|
+
expect(body.error).toBeDefined();
|
|
492
|
+
expect(body.error.code).toBe(-32001);
|
|
493
|
+
expect(body.error.message).toBe("Description");
|
|
494
|
+
expect(body.error.data).toBeDefined();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should include Content-Type header", () => {
|
|
498
|
+
const middleware = new AuthenticationMiddleware({
|
|
499
|
+
oauth: {
|
|
500
|
+
protectedResource: {
|
|
501
|
+
resource: "https://example.com",
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
const response = middleware.getScopeChallengeResponse(["read"]);
|
|
506
|
+
|
|
507
|
+
expect(response.headers["Content-Type"]).toBe("application/json");
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("should handle empty scopes array", () => {
|
|
511
|
+
const middleware = new AuthenticationMiddleware({
|
|
512
|
+
oauth: {
|
|
513
|
+
protectedResource: {
|
|
514
|
+
resource: "https://example.com",
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
const response = middleware.getScopeChallengeResponse([]);
|
|
519
|
+
|
|
520
|
+
expect(response.headers["WWW-Authenticate"]).toContain('scope=""');
|
|
521
|
+
const body = JSON.parse(response.body);
|
|
522
|
+
expect(body.error.data.required_scopes).toEqual([]);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
330
525
|
});
|
package/src/authentication.ts
CHANGED
|
@@ -17,6 +17,51 @@ export interface AuthConfig {
|
|
|
17
17
|
export class AuthenticationMiddleware {
|
|
18
18
|
constructor(private config: AuthConfig = {}) {}
|
|
19
19
|
|
|
20
|
+
getScopeChallengeResponse(
|
|
21
|
+
requiredScopes: string[],
|
|
22
|
+
errorDescription?: string,
|
|
23
|
+
requestId?: unknown,
|
|
24
|
+
): { body: string; headers: Record<string, string>; statusCode: number } {
|
|
25
|
+
const headers: Record<string, string> = {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Build WWW-Authenticate header with all required parameters
|
|
30
|
+
if (this.config.oauth?.protectedResource?.resource) {
|
|
31
|
+
const parts = [
|
|
32
|
+
"Bearer",
|
|
33
|
+
'error="insufficient_scope"',
|
|
34
|
+
`scope="${requiredScopes.join(" ")}"`,
|
|
35
|
+
`resource_metadata="${this.config.oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (errorDescription) {
|
|
39
|
+
// Escape quotes in description
|
|
40
|
+
const escaped = errorDescription.replace(/"/g, '\\"');
|
|
41
|
+
parts.push(`error_description="${escaped}"`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
headers["WWW-Authenticate"] = parts.join(", ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
error: {
|
|
50
|
+
code: -32001, // Custom error code for insufficient scope
|
|
51
|
+
data: {
|
|
52
|
+
error: "insufficient_scope",
|
|
53
|
+
required_scopes: requiredScopes,
|
|
54
|
+
},
|
|
55
|
+
message: errorDescription || "Insufficient scope",
|
|
56
|
+
},
|
|
57
|
+
id: requestId ?? null,
|
|
58
|
+
jsonrpc: "2.0",
|
|
59
|
+
}),
|
|
60
|
+
headers,
|
|
61
|
+
statusCode: 403,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
20
65
|
getUnauthorizedResponse(options?: {
|
|
21
66
|
error?: string;
|
|
22
67
|
error_description?: string;
|
package/src/startHTTPServer.ts
CHANGED
|
@@ -118,6 +118,28 @@ const getWWWAuthenticateHeader = (
|
|
|
118
118
|
return `Bearer ${params.join(", ")}`;
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
+
// Helper function to detect scope challenge errors
|
|
122
|
+
const isScopeChallengeError = (error: unknown): error is {
|
|
123
|
+
data: {
|
|
124
|
+
error: string;
|
|
125
|
+
errorDescription?: string;
|
|
126
|
+
requiredScopes: string[];
|
|
127
|
+
};
|
|
128
|
+
name: string;
|
|
129
|
+
} => {
|
|
130
|
+
return (
|
|
131
|
+
typeof error === "object" &&
|
|
132
|
+
error !== null &&
|
|
133
|
+
"name" in error &&
|
|
134
|
+
error.name === "InsufficientScopeError" &&
|
|
135
|
+
"data" in error &&
|
|
136
|
+
typeof error.data === "object" &&
|
|
137
|
+
error.data !== null &&
|
|
138
|
+
"error" in error.data &&
|
|
139
|
+
error.data.error === "insufficient_scope"
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
121
143
|
// Helper function to handle Response errors and send appropriate HTTP response
|
|
122
144
|
const handleResponseError = async (
|
|
123
145
|
error: unknown,
|
|
@@ -267,6 +289,7 @@ const applyCorsHeaders = (
|
|
|
267
289
|
const handleStreamRequest = async <T extends ServerLike>({
|
|
268
290
|
activeTransports,
|
|
269
291
|
authenticate,
|
|
292
|
+
authMiddleware,
|
|
270
293
|
createServer,
|
|
271
294
|
enableJsonResponse,
|
|
272
295
|
endpoint,
|
|
@@ -283,6 +306,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
283
306
|
{ server: T; transport: StreamableHTTPServerTransport }
|
|
284
307
|
>;
|
|
285
308
|
authenticate?: (request: http.IncomingMessage) => Promise<unknown>;
|
|
309
|
+
authMiddleware: AuthenticationMiddleware;
|
|
286
310
|
createServer: (request: http.IncomingMessage) => Promise<T>;
|
|
287
311
|
enableJsonResponse?: boolean;
|
|
288
312
|
endpoint: string;
|
|
@@ -298,6 +322,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
298
322
|
req.method === "POST" &&
|
|
299
323
|
new URL(req.url!, "http://localhost").pathname === endpoint
|
|
300
324
|
) {
|
|
325
|
+
let body: unknown;
|
|
301
326
|
try {
|
|
302
327
|
const sessionId = Array.isArray(req.headers["mcp-session-id"])
|
|
303
328
|
? req.headers["mcp-session-id"][0]
|
|
@@ -307,7 +332,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
307
332
|
|
|
308
333
|
let server: T;
|
|
309
334
|
|
|
310
|
-
|
|
335
|
+
body = await getBody(req);
|
|
311
336
|
|
|
312
337
|
// Per-request authentication in stateless mode
|
|
313
338
|
if (stateless && authenticate) {
|
|
@@ -566,6 +591,19 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
566
591
|
|
|
567
592
|
return true;
|
|
568
593
|
} catch (error) {
|
|
594
|
+
// Check for scope challenge errors
|
|
595
|
+
if (isScopeChallengeError(error)) {
|
|
596
|
+
const response = authMiddleware.getScopeChallengeResponse(
|
|
597
|
+
error.data.requiredScopes,
|
|
598
|
+
error.data.errorDescription,
|
|
599
|
+
(body as { id?: unknown })?.id,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
res.writeHead(response.statusCode, response.headers);
|
|
603
|
+
res.end(response.body);
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
569
607
|
console.error("[mcp-proxy] error handling request", error);
|
|
570
608
|
|
|
571
609
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -718,8 +756,8 @@ const handleSSERequest = async <T extends ServerLike>({
|
|
|
718
756
|
|
|
719
757
|
await transport.send({
|
|
720
758
|
jsonrpc: "2.0",
|
|
721
|
-
method: "
|
|
722
|
-
params: {
|
|
759
|
+
method: "notifications/message",
|
|
760
|
+
params: { data: "SSE Connection established", level: "info" },
|
|
723
761
|
});
|
|
724
762
|
|
|
725
763
|
if (onConnect) {
|
|
@@ -858,6 +896,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
858
896
|
(await handleStreamRequest({
|
|
859
897
|
activeTransports: activeStreamTransports,
|
|
860
898
|
authenticate,
|
|
899
|
+
authMiddleware,
|
|
861
900
|
createServer,
|
|
862
901
|
enableJsonResponse,
|
|
863
902
|
endpoint: streamEndpoint,
|