Skip to content

Commit e5f6524

Browse files
committed
Fix support for streamable-http connections.
* In server/index.js - add get/post handlers for /mcp - amend console log on SSE connect, with deprecation message - add /stdio GET handler and refactored /sse GET handler to not also do stdio. Each transport has its own handler now - add appropriate headers to streamable-http request * In /client/src/lib/hooks/useConnection.ts - in connect function - create server url properly based on new transport type.
1 parent 6ab7ac3 commit e5f6524

File tree

2 files changed

+135
-15
lines changed

2 files changed

+135
-15
lines changed

client/src/lib/hooks/useConnection.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,15 +278,26 @@ export function useConnection({
278278
setConnectionStatus("error-connecting-to-proxy");
279279
return;
280280
}
281-
const mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
282-
mcpProxyServerUrl.searchParams.append("transportType", transportType);
283-
if (transportType === "stdio") {
284-
mcpProxyServerUrl.searchParams.append("command", command);
285-
mcpProxyServerUrl.searchParams.append("args", args);
286-
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
287-
} else {
288-
mcpProxyServerUrl.searchParams.append("url", sseUrl);
281+
let mcpProxyServerUrl;
282+
switch (transportType) {
283+
case "stdio":
284+
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
285+
mcpProxyServerUrl.searchParams.append("command", command);
286+
mcpProxyServerUrl.searchParams.append("args", args);
287+
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
288+
break;
289+
case "sse":
290+
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
291+
mcpProxyServerUrl.searchParams.append("url", sseUrl);
292+
break;
293+
294+
case "streamable-http":
295+
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
296+
mcpProxyServerUrl.searchParams.append("url", sseUrl);
297+
break;
289298
}
299+
(mcpProxyServerUrl as URL).searchParams.append("transportType", transportType);
300+
290301

291302
try {
292303
// Inject auth manually instead of using SSEClientTransport, because we're
@@ -304,7 +315,7 @@ export function useConnection({
304315
headers[authHeaderName] = `Bearer ${token}`;
305316
}
306317

307-
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
318+
const clientTransport = new SSEClientTransport(mcpProxyServerUrl as URL, {
308319
eventSourceInit: {
309320
fetch: (url, init) => fetch(url, { ...init, headers }),
310321
},

server/src/index.ts

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
9797
console.log("Connected to SSE transport");
9898
return transport;
9999
} else if (transportType === "streamable-http") {
100-
const headers: HeadersInit = {};
100+
const headers: HeadersInit = {
101+
Accept: "text/event-stream, application/json"
102+
};
101103

102104
for (const key of STREAMABLE_HTTP_HEADERS_PASSTHROUGH) {
103105
if (req.headers[key] === undefined) {
@@ -127,9 +129,10 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
127129

128130
let backingServerTransport: Transport | undefined;
129131

130-
app.get("/sse", async (req, res) => {
132+
133+
app.get("/mcp", async (req, res) => {
131134
try {
132-
console.log("New SSE connection");
135+
console.log("New streamable-http connection");
133136

134137
try {
135138
await backingServerTransport?.close();
@@ -149,9 +152,7 @@ app.get("/sse", async (req, res) => {
149152

150153
console.log("Connected MCP client to backing server transport");
151154

152-
const webAppTransport = new SSEServerTransport("/message", res);
153-
console.log("Created web app transport");
154-
155+
const webAppTransport = new SSEServerTransport("/mcp", res);
155156
webAppTransports.push(webAppTransport);
156157
console.log("Created web app transport");
157158

@@ -181,6 +182,114 @@ app.get("/sse", async (req, res) => {
181182
}
182183
});
183184

185+
app.post("/mcp", async (req, res) => {
186+
try {
187+
const sessionId = req.query.sessionId;
188+
console.log(`Received message for sessionId ${sessionId}`);
189+
190+
const transport = webAppTransports.find((t) => t.sessionId === sessionId);
191+
if (!transport) {
192+
res.status(404).end("Session not found");
193+
return;
194+
}
195+
await transport.handlePostMessage(req, res);
196+
} catch (error) {
197+
console.error("Error in /mcp route:", error);
198+
res.status(500).json(error);
199+
}
200+
});
201+
202+
app.get("/stdio", async (req, res) => {
203+
try {
204+
console.log("New connection");
205+
206+
try {
207+
await backingServerTransport?.close();
208+
backingServerTransport = await createTransport(req);
209+
} catch (error) {
210+
if (error instanceof SseError && error.code === 401) {
211+
console.error(
212+
"Received 401 Unauthorized from MCP server:",
213+
error.message,
214+
);
215+
res.status(401).json(error);
216+
return;
217+
}
218+
219+
throw error;
220+
}
221+
222+
console.log("Connected MCP client to backing server transport");
223+
224+
const webAppTransport = new SSEServerTransport("/message", res);
225+
webAppTransports.push(webAppTransport);
226+
227+
console.log("Created web app transport");
228+
229+
await webAppTransport.start();
230+
(backingServerTransport as StdioClientTransport).stderr!.on("data", (chunk) => {
231+
webAppTransport.send({
232+
jsonrpc: "2.0",
233+
method: "notifications/stderr",
234+
params: {
235+
content: chunk.toString(),
236+
},
237+
});
238+
});
239+
240+
mcpProxy({
241+
transportToClient: webAppTransport,
242+
transportToServer: backingServerTransport,
243+
});
244+
245+
console.log("Set up MCP proxy");
246+
} catch (error) {
247+
console.error("Error in /stdio route:", error);
248+
res.status(500).json(error);
249+
}
250+
});
251+
252+
app.get("/sse", async (req, res) => {
253+
try {
254+
console.log("New SSE connection. NOTE: The sse transport is deprecated and has been replaced by streamable-http");
255+
256+
try {
257+
await backingServerTransport?.close();
258+
backingServerTransport = await createTransport(req);
259+
} catch (error) {
260+
if (error instanceof SseError && error.code === 401) {
261+
console.error(
262+
"Received 401 Unauthorized from MCP server:",
263+
error.message,
264+
);
265+
res.status(401).json(error);
266+
return;
267+
}
268+
269+
throw error;
270+
}
271+
272+
console.log("Connected MCP client to backing server transport");
273+
274+
const webAppTransport = new SSEServerTransport("/message", res);
275+
webAppTransports.push(webAppTransport);
276+
277+
console.log("Created web app transport");
278+
279+
await webAppTransport.start();
280+
281+
mcpProxy({
282+
transportToClient: webAppTransport,
283+
transportToServer: backingServerTransport,
284+
});
285+
286+
console.log("Set up MCP proxy");
287+
} catch (error) {
288+
console.error("Error in /sse route:", error);
289+
res.status(500).json(error);
290+
}
291+
});
292+
184293
app.post("/message", async (req, res) => {
185294
try {
186295
const sessionId = req.query.sessionId;

0 commit comments

Comments
 (0)