🔥

@hono/mcp で Streamable HTTP の MCP Server を動かしてみる

に公開

はじめに

本記事では、@hono/mcp で、Streamable HTTP の MCP Server を動かす方法についてメモを残します。

具体的には、これまでに観た映画情報を返す MCP Server を作成します。
なお、映画の情報は、Cloudflare D1 に格納し、MCP Server は、Cloudflare Workers にデプロイします。

ローカルで MCP Server を動かす

以降は、サンプルコードを用いて環境構築を実施します。

ダミーデータの作成

まず、以下のコマンドを実行し、ローカルの D1 に映画情報を用意します。

git clone https://github.com/0machi/mcp-sample
cd mcp-sample
bun install
bun run d1:local:migrate
bun run d1:local:seed
bun run d1:local:studio

その後、https://local.drizzle.studio/ にアクセスすると、以下のテーブルに格納された映画情報を確認することができます。

https://github.com/0machi/mcp-sample/blob/e249356d516bd73cd487f66a6e6e1b099d8e357c/drizzle/0000_dusty_marrow.sql#L1-L5

MCP Server の作成

次に、@modelcontextprotocol/sdk を利用して、MCP Server が提供する Tool を登録します。

今回は、getMcpServer という関数に D1 から映画情報を取得する Tool を3つ作成しました。

  • 映画情報を全件取得する Tool
  • 映画の製作年を指定して映画情報を取得する Tool
  • 鑑賞した映画の本数を製作年ごとに集計する Tool
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { asc, between, count, desc } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import type { Context } from "hono";
import z from "zod";
import { movieTable } from "../drizzle/schema";
import type { Env } from ".";

export const getMcpServer = async (c: Context<Env>) => {
  const server = new McpServer({
    name: "my-mcp-server",
    version: "1.0.0",
  });

  const db = drizzle(c.env.MOVIE_DB);

  server.registerTool(
    "get-all-movies",
    {
      title: "Get All Movies",
      description: "Get all list of movies",
    },
    async () => {
      const result = await db
        .select()
        .from(movieTable)
        .orderBy(desc(movieTable.productionYear), asc(movieTable.movieId));
      return { content: [{ type: "text", text: JSON.stringify(result) }] };
    },
  );

  server.registerTool(
    "get-movies-by-production-year",
    {
      title: "Get Movies By Production Year",
      description: "Get a list of movies produced between the specified years",
      inputSchema: {
        fromYear: z.number(),
        toYear: z.number(),
      },
    },
    async (params) => {
      const result = await db
        .select()
        .from(movieTable)
        .where(
          between(movieTable.productionYear, params.fromYear, params.toYear),
        )
        .orderBy(desc(movieTable.productionYear), asc(movieTable.movieId));
      return { content: [{ type: "text", text: JSON.stringify(result) }] };
    },
  );

  server.registerTool(
    "get-watched-movies-count-by-production-year",
    {
      title: "Get Watched Movies Count By Production Year",
      description: "Get the count of watched movies grouped by production year",
    },
    async () => {
      const result = await db
        .select({
          productionYear: movieTable.productionYear,
          watchedCount: count(movieTable.movieId),
        })
        .from(movieTable)
        .groupBy(movieTable.productionYear)
        .orderBy(desc(movieTable.productionYear));
      return { content: [{ type: "text", text: JSON.stringify(result) }] };
    },
  );

  return server;
};

@hono/mcp で Streamable HTTP の MCP Server を動かす

作成した getMcpServer という関数を import し、@hono/mcp で Streamable HTTP の MCP Server を /mcp で受け付けるようにします。
ここでは、MCP Server には、Bearer Auth Middlewareで Bearer 認証を適用しています。

import { StreamableHTTPTransport } from "@hono/mcp";
import { Hono } from "hono";
import { env } from "hono/adapter";
import { bearerAuth } from "hono/bearer-auth";
import { getMcpServer } from "./mcp";

export type Env = {
  Bindings: {
    MOVIE_DB: D1Database;
  };
};

const app = new Hono<Env>();
const transport = new StreamableHTTPTransport();
let isConnected = false;

app.use(
  "/mcp",
  bearerAuth({
    verifyToken: async (token, c) => {
      const { TOKEN } = env<{ TOKEN: string }>(c);
      return token === TOKEN;
    },
  }),
);

app.all("/mcp", async (c) => {
  const mcpServer = await getMcpServer(c);
  const connectedToServer = mcpServer.connect(transport).then(() => {
    isConnected = true;
  });
  if (!isConnected) {
    await connectedToServer;
  }
  return transport.handleRequest(c);
});

export default app;

MCP Inspector による動作確認

最後に、@modelcontextprotocol/inspector を使用して、作成した MCP Server の動作を確認します。

cp .example.vars .dev.vars
bun run dev
bunx @modelcontextprotocol/inspector

MCP Inspector の起動後、以下の設定を実施して、MCP Server に接続します。

以下のように Tool が実行できることを確認します。

Cloudflare Workers で MCP Server を動かす

  • 必要に応じて作成した MCP Server を Cloudflare Workers にデプロイし、D1 にデータを格納します。

Claude Desktop から接続

claude_desktop_config.json に以下のように設定を追加して、MCP Server の動作を確認します。

{
  "mcpServers": {
    "movie-api": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "<URL>",
        "--header",
        "Authorization: Bearer <TOKEN>"
      ]
    }
  }
}

Workers AI LLM Playground から接続

まず、CORS Middleware を、https://playground.ai.cloudflare.com/ に対して適用します。

app.use("/mcp", cors({ origin: "https://playground.ai.cloudflare.com" }));

その後、https://playground.ai.cloudflare.com/ にアクセスし、MCP Server の動作を確認します。

おわりに

本記事では、@hono/mcp で、Streamable HTTP の MCP Server を動かす方法についてメモを残しました。MCP Server を動かしてみたい方の参考になれば幸いです。

最近リピめしというサービスを公開したので、良ければ利用してみてください。

https://repemeshi.com/

shinaps テックブログ

Discussion