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.
- package/README.md +148 -1
- package/dist/bin/mcp-proxy.js +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.js +1 -1
- package/dist/{stdio-CsjPjeWC.js → stdio-BEX6di72.js} +105 -29
- package/dist/stdio-BEX6di72.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/authentication.test.ts +145 -7
- package/src/authentication.ts +51 -5
- package/src/index.ts +1 -0
- package/src/startHTTPServer.test.ts +392 -0
- package/src/startHTTPServer.ts +207 -35
- package/dist/stdio-CsjPjeWC.js.map +0 -1
package/src/startHTTPServer.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
637
|
-
|
|
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);
|