【BUG】Use Koa server create StreamableHTTPServerTransport, when call tool, server crash!
Opened this issue · 3 comments
zhuyuanmin commented
code:
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import Koa from 'koa';
import { koaBody } from 'koa-body';
import KoaRouter from 'koa-router';
import { randomUUID } from 'node:crypto';
import { mcpServer } from './server.js';
// 初始化应用
const app = new Koa();
app.use(koaBody());
const router = new KoaRouter();
// 存储会话与传输实例的映射
const transportMap = {};
// MCP核心处理端点
router.post('/mcp', async (ctx) => {
const sessionId = ctx.headers['mcp-session-id'];
let transport;
if (sessionId && transportMap[sessionId]) {
transport = transportMap[sessionId];
} else if (!sessionId && isInitializeRequest(ctx.request.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transportMap[sessionId] = transport;
},
});
ctx.req.on('close', () => {
console.log(`Connection closed for session: ${transport.sessionId}`);
delete transportMap[transport.sessionId];
});
await mcpServer.connect(transport);
} else {
ctx.status = 400;
ctx.body = { error: 'Invalid session ID' };
return;
}
await transport.handleRequest(ctx.req, ctx.res, ctx.request.body);
});
const handleSessionRequest = async (ctx) => {
const sessionId = ctx.headers['mcp-session-id'];
if (!sessionId || !transportMap[sessionId]) {
ctx.status = 400;
ctx.body = { error: 'Invalid or missing session ID' };
return;
}
await transportMap[sessionId].handleRequest(ctx.req, ctx.res);
};
router.get('/mcp', handleSessionRequest);
router.delete('/mcp', handleSessionRequest);
// 注册路由
app.use(router.routes()).use(router.allowedMethods());
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`MCP服务器启动在 http://localhost:${PORT}`);
console.log(`- MCP端点: http://localhost:${PORT}/mcp`);
});// server.js
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { writeFileSync } from "node:fs";
// Create an MCP server
export const mcpServer = new McpServer({
name: "MCP 测试服务器",
version: "1.0.0",
capabilities: {
resources: {},
tools: {},
},
});
// Add an addition tool
mcpServer.registerTool(
"add",
{
title: "加法工具",
description: "两个数字相加",
inputSchema: { a: z.number(), b: z.number() },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);XrEZE MINGW64 /d/workspace/IoT_UI/mcp-server
$ node mcp-http2
Debugger attached.
MCP服务器启动在 http://localhost:3000
- MCP端点: http://localhost:3000/mcp
Waiting for the debugger to disconnect...
node:events:485
throw er; // Unhandled 'error' event
^
Error [ERR_STREAM_WRITE_AFTER_END]: write after end
at write_ (node:_http_outgoing:951:11)
at ServerResponse.write (node:_http_outgoing:904:15)
at StreamableHTTPServerTransport.writeSSEEvent (file:///D:/workspace/IoT_UI/mcp-server/node_modules/@modelcontextprotocol/sdk/dist/e
sm/server/streamableHttp.js:238:20)
at StreamableHTTPServerTransport.send (file:///D:/workspace/IoT_UI/mcp-server/node_modules/@modelcontextprotocol/sdk/dist/esm/server
/streamableHttp.js:565:22)
at Promise.resolve.then.then.capturedTransport.send.jsonrpc (file:///D:/workspace/IoT_UI/mcp-server/node_modules/@modelcontextprotoc
ol/sdk/dist/esm/shared/protocol.js:162:108)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
Emitted 'error' event on ServerResponse instance at:
at emitErrorNt (node:_http_outgoing:923:9)
at process.processTicksAndRejections (node:internal/process/task_queues:91:21) {
code: 'ERR_STREAM_WRITE_AFTER_END'
}
Node.js v23.1.0
AechWhyL commented
try add ctx.respond = false to stop Koa's default response
I assume the transport.handleRequest has already handled the req and ended the response so maybe you should let it do all the res stuff
nchirania commented
server crash for me as well even I have used ctx.respond = false. still getting this Error [ERR_HTTP_HEADERS_SENT]: Cannot write headers after they are sent .
@AechWhyL were you able to do with ctx.respond = false?
`
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMcpServer } from "./server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export class McpController {
private server: McpServer;
constructor() {
this.server = createMcpServer();
}
async dispatchMcpRequest(ctx: Router.IRouterContext): Promise<void> {
console.log("MCP request received", {
method: ctx.method,
url: ctx.url,
body: ctx.request.body,
headers: ctx.headers,
});
try {
// Let the transport write directly to the Node response
ctx.respond = false;
const transport = new StreamableHTTPServerTransport({
enableJsonResponse: true,
sessionIdGenerator: undefined,
});
await this.server.connect(transport);
console.log("MCP transport created and connected");
await transport.handleRequest(ctx.req, ctx.res, ctx.request.body);
console.log("MCP request processed successfully via transport");
} catch (error) {
console.error("Error in MCP transport handling", { error });
if (!ctx.res.headersSent) {
ctx.status = 500;
ctx.body = {
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
data: error instanceof Error ? error.message : "Unknown error",
},
};
}
}
}
}
ochafik commented
Hi @zhuyuanmin, thanks for filing this!
Would you mind testing this against the latest changes merged into the main branch?
The error looks like it might have been fixed by #933.
Otherwise, might be worth asking https://github.com/koajs/koa/issues for help