🔥

HonoでMCPサーバーを動かす

に公開

はじめに

こちらのブログを参考にStreamable HTTP transport MCPサーバーをexpressで動かすことができた。

これをHonoでやってみた。

変更点

やってみる。といっても、ちょっと実装変えるだけ。

  1. ルーティングの実装
// Express
app.post("/mcp", async (req, res) => { ... });

// Hono
app.post("/mcp", async (c) => { ... });

Honoでは、コンテキストcを通じてリクエストとレスポンスにアクセスします。これはExpressのreqとresパラメータと異なる方式です。

  1. リクエストとレスポンスの変換
import { toFetchResponse, toReqRes } from "fetch-to-node";

// リクエスト処理部分
const { req, res } = toReqRes(c.req.raw);

await transport.handleRequest(req, res, body);

HonoはHonoRequestクラスを使用してますが、MCPサーバーのSDKにパラメータとして渡すために、fetch-to-nodeライブラリを使用して変換を行っています。

  1. return時の変換
// Hono
// 明示的にレスポンスを返す必要がある
return toFetchResponse(res);

MCPサーバーSDKの処理後は、toFetchResponse(res)がこの変換を行い、Honoがクライアントに適切にレスポンスを送信できるようにします。

全体

hono-mcp-server.js
+ import { Hono } from "hono";
+ import { handle } from 'hono/aws-lambda';
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+ import { toFetchResponse, toReqRes } from "fetch-to-node";

const getServer = () => {
  // MCP サーバーを作成
  const server = new McpServer(
    {
      name: "my-mcp-server",
      version: "0.0.1",
    }
  );

  // Add an addition tool
  server.tool(
    ...
  );

  return server;
};

+ const app = new Hono();

// MCPエンドポイント (POST)
+ app.post("/mcp", async (c) => {
  // Fetch APIのリクエスト/レスポンスをNode.jsのリクエスト/レスポンスに変換 
+ const { req, res } = toReqRes(c.req.raw);

  const server = getServer();

  try {
    const body = await c.req.json();
    
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
    });
    
    transport.onerror = console.error.bind(console);

    // MCPサーバーをトランスポートに接続
    await server.connect(transport);

    // MCPリクエストを処理
    await transport.handleRequest(req, res, body);

    // リクエストが終了したらリソースをクリーンアップ
    res.on("close", () => {
      console.log("Request closed");
      transport.close();
      server.close();
    });

    console.log(res);

    // Node.jsのレスポンスをFetch APIのレスポンスに変換して返す
+    return toFetchResponse(res);
  } catch (e) {
    console.error("MCP request error:", e);
    return c.json(
      {
        jsonrpc: "2.0",
        error: {
          code: -32603,
          message: "Internal server error",
        },
        id: null,
      },
      { status: 500 }
    );
  }
});

// GET リクエスト(MCP)
+app.get("/mcp", (c) => {
  console.log("Received GET MCP request");
  return c.json(
    {
      jsonrpc: "2.0",
      error: {
        code: -32000,
        message: "Method not allowed.",
      },
      id: null,
    },
    { status: 405 }
  );
});

// DELETE リクエスト(MCP)
+app.delete("/mcp", (c) => {
  console.log("Received DELETE MCP request");
  return c.json(
    {
      jsonrpc: "2.0",
      error: {
        code: -32000,
        message: "Method not allowed.",
      },
      id: null,
    },
    { status: 405 }
  );
});

export const handler = handle(app);

終わりに

次はこれをLambdaで動かしてみたい。

Discussion