Skip to content

Commit d2a715c

Browse files
authored
Merge pull request #813 from cliffhall/fix-new-headers-issue
Polyfill Headers in proxy with node-fetch
2 parents 81889e6 + f1e93e0 commit d2a715c

File tree

3 files changed

+161
-4
lines changed

3 files changed

+161
-4
lines changed

package-lock.json

Lines changed: 92 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@modelcontextprotocol/inspector-server": "^0.16.7",
5353
"@modelcontextprotocol/sdk": "^1.18.0",
5454
"concurrently": "^9.2.0",
55+
"node-fetch": "^3.3.2",
5556
"open": "^10.2.0",
5657
"shell-quote": "^1.8.3",
5758
"spawn-rx": "^5.1.2",

server/src/index.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
import cors from "cors";
44
import { parseArgs } from "node:util";
55
import { parse as shellParseArgs } from "shell-quote";
6+
import nodeFetch, { Headers as NodeHeaders } from "node-fetch";
7+
8+
// Type-compatible wrappers for node-fetch to work with browser-style types
9+
const fetch = nodeFetch;
10+
const Headers = NodeHeaders;
611

712
import {
813
SSEClientTransport,
@@ -231,13 +236,37 @@ const authMiddleware = (
231236
next();
232237
};
233238

239+
/**
240+
* Converts a Node.js ReadableStream to a web-compatible ReadableStream
241+
* This is necessary for the EventSource polyfill which expects web streams
242+
*/
243+
const createWebReadableStream = (nodeStream: any): ReadableStream => {
244+
return new ReadableStream({
245+
start(controller) {
246+
nodeStream.on("data", (chunk: any) => {
247+
controller.enqueue(chunk);
248+
});
249+
nodeStream.on("end", () => {
250+
controller.close();
251+
});
252+
nodeStream.on("error", (err: any) => {
253+
controller.error(err);
254+
});
255+
},
256+
});
257+
};
258+
234259
/**
235260
* Creates a `fetch` function that merges dynamic session headers with the
236261
* headers from the actual request, ensuring that request-specific headers like
237-
* `Content-Type` are preserved.
262+
* `Content-Type` are preserved. For SSE requests, it also converts Node.js
263+
* streams to web-compatible streams.
238264
*/
239265
const createCustomFetch = (headerHolder: { headers: HeadersInit }) => {
240-
return (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
266+
return async (
267+
input: RequestInfo | URL,
268+
init?: RequestInit,
269+
): Promise<Response> => {
241270
// Determine the headers from the original request/init.
242271
// The SDK may pass a Request object or a URL and an init object.
243272
const originalHeaders =
@@ -252,8 +281,43 @@ const createCustomFetch = (headerHolder: { headers: HeadersInit }) => {
252281
finalHeaders.set(key, value);
253282
});
254283

255-
// This works for both `fetch(url, init)` and `fetch(request)` style calls.
256-
return fetch(input, { ...init, headers: finalHeaders });
284+
// Convert Headers to a plain object for node-fetch compatibility
285+
const headersObject: Record<string, string> = {};
286+
finalHeaders.forEach((value, key) => {
287+
headersObject[key] = value;
288+
});
289+
290+
// Get the response from node-fetch (cast input and init to handle type differences)
291+
const response = await fetch(
292+
input as any,
293+
{ ...init, headers: headersObject } as any,
294+
);
295+
296+
// Check if this is an SSE request by looking at the Accept header
297+
const acceptHeader = finalHeaders.get("Accept");
298+
const isSSE = acceptHeader?.includes("text/event-stream");
299+
300+
if (isSSE && response.body) {
301+
// For SSE requests, we need to convert the Node.js stream to a web ReadableStream
302+
// because the EventSource polyfill expects web-compatible streams
303+
const webStream = createWebReadableStream(response.body);
304+
305+
// Create a new response with the web-compatible stream
306+
// Convert node-fetch headers to plain object for web Response compatibility
307+
const responseHeaders: Record<string, string> = {};
308+
response.headers.forEach((value: string, key: string) => {
309+
responseHeaders[key] = value;
310+
});
311+
312+
return new Response(webStream, {
313+
status: response.status,
314+
statusText: response.statusText,
315+
headers: responseHeaders,
316+
}) as Response;
317+
}
318+
319+
// For non-SSE requests, return the response as-is (cast to handle type differences)
320+
return response as unknown as Response;
257321
};
258322
};
259323

0 commit comments

Comments
 (0)