modelcontextprotocol/typescript-sdk

【BUG】Use Koa server create StreamableHTTPServerTransport, when call tool, server crash!

Opened this issue · 3 comments

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

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

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",
          },
        };
      }
    }
  }
}

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