mcp-proxy
Version:
A TypeScript SSE proxy for MCP servers that use stdio transport.
180 lines (140 loc) • 4.12 kB
text/typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import http from "http";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
export type SSEServer = {
close: () => Promise<void>;
};
type ServerLike = {
connect: Server["connect"];
close: Server["close"];
};
export const startSSEServer = async <T extends ServerLike>({
port,
createServer,
endpoint,
onConnect,
onClose,
onUnhandledRequest,
}: {
port: number;
endpoint: string;
createServer: (request: http.IncomingMessage) => Promise<T>;
onConnect?: (server: T) => void;
onClose?: (server: T) => void;
onUnhandledRequest?: (
req: http.IncomingMessage,
res: http.ServerResponse,
) => Promise<void>;
}): Promise<SSEServer> => {
const activeTransports: Record<string, SSEServerTransport> = {};
/**
* @author https://dev.classmethod.jp/articles/mcp-sse/
*/
const httpServer = http.createServer(async (req, res) => {
if (req.headers.origin) {
try {
const origin = new URL(req.headers.origin);
res.setHeader("Access-Control-Allow-Origin", origin.origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "*");
} catch (error) {
console.error("Error parsing origin:", error);
}
}
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
if (req.method === "GET" && req.url === `/ping`) {
res.writeHead(200).end("pong");
return;
}
if (req.method === "GET" && req.url === endpoint) {
const transport = new SSEServerTransport("/messages", res);
let server: T;
try {
server = await createServer(req);
} catch (error) {
if (error instanceof Response) {
res.writeHead(error.status).end(error.statusText);
return;
}
res.writeHead(500).end("Error creating server");
return;
}
activeTransports[transport.sessionId] = transport;
let closed = false;
res.on("close", async () => {
closed = true;
try {
await server.close();
} catch (error) {
console.error("Error closing server:", error);
}
delete activeTransports[transport.sessionId];
onClose?.(server);
});
try {
await server.connect(transport);
await transport.send({
jsonrpc: "2.0",
method: "sse/connection",
params: { message: "SSE Connection established" },
});
onConnect?.(server);
} catch (error) {
if (!closed) {
console.error("Error connecting to server:", error);
res.writeHead(500).end("Error connecting to server");
}
}
return;
}
if (req.method === "POST" && req.url?.startsWith("/messages")) {
const sessionId = new URL(
req.url,
"https://example.com",
).searchParams.get("sessionId");
if (!sessionId) {
res.writeHead(400).end("No sessionId");
return;
}
const activeTransport: SSEServerTransport | undefined =
activeTransports[sessionId];
if (!activeTransport) {
res.writeHead(400).end("No active transport");
return;
}
await activeTransport.handlePostMessage(req, res);
return;
}
if (onUnhandledRequest) {
await onUnhandledRequest(req, res);
} else {
res.writeHead(404).end();
}
});
await new Promise((resolve) => {
httpServer.listen(port, "::", () => {
resolve(undefined);
});
});
return {
close: async () => {
for (const transport of Object.values(activeTransports)) {
await transport.close();
}
return new Promise((resolve, reject) => {
httpServer.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
},
};
};