mcp-proxy 6.4.5 → 6.5.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 +37 -1
- package/dist/bin/mcp-proxy.mjs +15 -1
- package/dist/bin/mcp-proxy.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{stdio-_93Y9W6l.mjs → stdio-BGZrO8Rz.mjs} +4 -2
- package/dist/{stdio-_93Y9W6l.mjs.map → stdio-BGZrO8Rz.mjs.map} +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/bin/mcp-proxy.ts +23 -0
- package/src/startHTTPServer.test.ts +55 -0
- package/src/startHTTPServer.ts +12 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/bin/mcp-proxy.ts
CHANGED
|
@@ -55,6 +55,12 @@ const argv = await yargs(hideBin(process.argv))
|
|
|
55
55
|
"The timeout (in milliseconds) for initial connection to the MCP server (default: 60 seconds)",
|
|
56
56
|
type: "number",
|
|
57
57
|
},
|
|
58
|
+
corsAddAllowedHeader: {
|
|
59
|
+
array: true,
|
|
60
|
+
describe:
|
|
61
|
+
"Add a header name to Access-Control-Allow-Headers (defaults preserved). Repeat to add multiple, e.g. `--corsAddAllowedHeader X-API-Key`.",
|
|
62
|
+
type: "string",
|
|
63
|
+
},
|
|
58
64
|
debug: {
|
|
59
65
|
default: false,
|
|
60
66
|
describe: "Enable debug logging",
|
|
@@ -137,6 +143,22 @@ const argv = await yargs(hideBin(process.argv))
|
|
|
137
143
|
.help()
|
|
138
144
|
.parseAsync();
|
|
139
145
|
|
|
146
|
+
// Default Access-Control-Allow-Headers list — must stay in sync with
|
|
147
|
+
// `defaultCorsOptions.allowedHeaders` in src/startHTTPServer.ts.
|
|
148
|
+
const DEFAULT_ALLOWED_HEADERS = [
|
|
149
|
+
"Content-Type",
|
|
150
|
+
"Authorization",
|
|
151
|
+
"Accept",
|
|
152
|
+
"Mcp-Session-Id",
|
|
153
|
+
"Mcp-Protocol-Version",
|
|
154
|
+
"Last-Event-Id",
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const corsOption =
|
|
158
|
+
argv.corsAddAllowedHeader && argv.corsAddAllowedHeader.length > 0
|
|
159
|
+
? { allowedHeaders: [...DEFAULT_ALLOWED_HEADERS, ...argv.corsAddAllowedHeader] }
|
|
160
|
+
: undefined;
|
|
161
|
+
|
|
140
162
|
// If -- separator was used, everything after -- is the command and its args
|
|
141
163
|
const dashDashArgs = argv["--"] as string[] | undefined;
|
|
142
164
|
|
|
@@ -218,6 +240,7 @@ const proxy = async () => {
|
|
|
218
240
|
|
|
219
241
|
const server = await startHTTPServer({
|
|
220
242
|
apiKey: argv.apiKey,
|
|
243
|
+
cors: corsOption,
|
|
221
244
|
createServer,
|
|
222
245
|
eventStore: new InMemoryEventStore(),
|
|
223
246
|
host: argv.host,
|
|
@@ -746,6 +746,61 @@ it("allows onUnhandledRequest to serve routes without auth", async () => {
|
|
|
746
746
|
await httpServer.close();
|
|
747
747
|
});
|
|
748
748
|
|
|
749
|
+
it("routes MCP stream endpoint to handleStreamRequest even when onUnhandledRequest closes response for unknown paths", async () => {
|
|
750
|
+
// Regression test for the interaction between PR #59 and consumers
|
|
751
|
+
// (e.g. fastmcp) whose onUnhandledRequest handler writes 404 for any path
|
|
752
|
+
// it doesn't recognise. Before the fix, the POST /mcp request was served
|
|
753
|
+
// by onUnhandledRequest (→ 404) and never reached handleStreamRequest.
|
|
754
|
+
const port = await getRandomPort();
|
|
755
|
+
|
|
756
|
+
const httpServer = await startHTTPServer({
|
|
757
|
+
createServer: async () => {
|
|
758
|
+
return new Server(
|
|
759
|
+
{ name: "test", version: "1.0.0" },
|
|
760
|
+
{ capabilities: {} },
|
|
761
|
+
);
|
|
762
|
+
},
|
|
763
|
+
// Simulates fastmcp's handleUnhandledRequest: consumes unknown paths
|
|
764
|
+
// with a 404 because it assumes it runs *after* the MCP protocol handlers.
|
|
765
|
+
onUnhandledRequest: async (req, res) => {
|
|
766
|
+
if (req.url === "/health") {
|
|
767
|
+
res.writeHead(200).end("ok");
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
res.writeHead(404).end();
|
|
771
|
+
},
|
|
772
|
+
port,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// Sanity: custom route still works (preserves PR #59 behaviour).
|
|
776
|
+
const healthResponse = await fetch(`http://localhost:${port}/health`);
|
|
777
|
+
expect(healthResponse.status).toBe(200);
|
|
778
|
+
|
|
779
|
+
// The MCP initialize call must reach handleStreamRequest, NOT the 404
|
|
780
|
+
// fallback inside onUnhandledRequest.
|
|
781
|
+
const mcpResponse = await fetch(`http://localhost:${port}/mcp`, {
|
|
782
|
+
body: JSON.stringify({
|
|
783
|
+
id: 1,
|
|
784
|
+
jsonrpc: "2.0",
|
|
785
|
+
method: "initialize",
|
|
786
|
+
params: {
|
|
787
|
+
capabilities: {},
|
|
788
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
789
|
+
protocolVersion: "2025-03-26",
|
|
790
|
+
},
|
|
791
|
+
}),
|
|
792
|
+
headers: {
|
|
793
|
+
Accept: "application/json, text/event-stream",
|
|
794
|
+
"Content-Type": "application/json",
|
|
795
|
+
},
|
|
796
|
+
method: "POST",
|
|
797
|
+
});
|
|
798
|
+
expect(mcpResponse.status).toBe(200);
|
|
799
|
+
expect(mcpResponse.headers.get("mcp-session-id")).toBeTruthy();
|
|
800
|
+
|
|
801
|
+
await httpServer.close();
|
|
802
|
+
});
|
|
803
|
+
|
|
749
804
|
// Stateless OAuth 2.0 JWT Bearer Token Authentication Tests (PR #37)
|
|
750
805
|
|
|
751
806
|
it("accepts requests with valid Bearer token in stateless mode", async () => {
|
package/src/startHTTPServer.ts
CHANGED
|
@@ -938,9 +938,20 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
938
938
|
return;
|
|
939
939
|
}
|
|
940
940
|
|
|
941
|
+
// Determine whether the request targets an MCP protocol endpoint (SSE
|
|
942
|
+
// or HTTP Stream). For those endpoints, onUnhandledRequest MUST NOT run
|
|
943
|
+
// first — some consumers (e.g. fastmcp) use it as a catch-all 404 handler
|
|
944
|
+
// and would otherwise short-circuit the MCP protocol handlers.
|
|
945
|
+
// Use a fixed base because `host` may be "::" (IPv6 any), which is not a
|
|
946
|
+
// valid URL authority. We only need pathname here.
|
|
947
|
+
const requestUrl = new URL(req.url || "", "http://localhost");
|
|
948
|
+
const isMcpEndpoint =
|
|
949
|
+
(sseEndpoint && requestUrl.pathname === sseEndpoint) ||
|
|
950
|
+
(streamEndpoint && requestUrl.pathname === streamEndpoint);
|
|
951
|
+
|
|
941
952
|
// Let non-MCP routes (e.g. /health, /ready, OAuth metadata) be handled
|
|
942
953
|
// before auth — API key auth protects MCP protocol endpoints, not custom routes.
|
|
943
|
-
if (onUnhandledRequest) {
|
|
954
|
+
if (onUnhandledRequest && !isMcpEndpoint) {
|
|
944
955
|
await onUnhandledRequest(req, res);
|
|
945
956
|
if (res.writableEnded) {
|
|
946
957
|
return;
|