🍟

Hono でリモート MCP サーバーを構築して Mastra エージェントから呼び出す on Docker

に公開

レバテック開発部の瀬尾です✌
Zenn にダークモードが来て喜んでいます。

今回は掲題のものを実装してみたら結構簡単だったので、その備忘録を残します。

やったこと

  • DB にあるデータを取得して返す MCP サーバーを実装する
  • その MCP サーバーを Hono を使って Streamable HTTP Transport で利用可能にする
  • Mastra で作ったエージェントで、その リモート MCP サーバーを使ってデータ取得する

技術スタック

  • MCP Server
    • Bun, TypeScript
    • Hono v4.8
  • Agent
    • Bun, TypeScript
    • Mastra v0.10
  • DB
    • MySQL

以上を Docker compose でコンテナ上で動かしました。

compose.yml
version: '3'

services:
  db:
    image: mysql:8
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test
      MYSQL_USER: test
      MYSQL_PASSWORD: test
      MYSQL_TCP_PORT: 5506
    expose:
      - 5506
    ports:
      - 5506:5506
    volumes:
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql # 適当な初期データ

  mcp-server:
    image: oven/bun:latest
    restart: always
    environment:
      DB_HOST: 'db'
      DB_PORT: 5506
      DB_USER: test
      DB_PASSWORD: test
      DB_NAME: test
      DOCKER_ENV: 'true'
    command: sh -c "bun install && bun run start"
    working_dir: /app
    volumes:
      - ./mcp-server:/app
      - /app/node_modules  # node_modulesを除外
    expose:
      - 5001
    ports:
      - 5001:5001
    depends_on:
      - db
    networks:
      - default

  agent:
    image: oven/bun:latest
    restart: always
    command: sh -c "bun install && bun run dev"
    working_dir: /app
    environment:
      ANTHROPIC_API_KEY: 'your key'
      MCP_SERVER_URL: 'http://mcp-server:5001/mcp'
    volumes:
      - ./agent:/app
      - /app/node_modules
    ports:
      - 4111:4111
    depends_on:
      - mcp-server
    networks:
      - default

networks:
  default:
    driver: bridge
ディレクトリ構成
.
├── agent
│   ├── src/mastra
│   │   ├── index.ts
│   │   └── agent.ts
│   ├── package.json
│   └── ...
│
├── mcp-server
│   ├── index.ts
│   ├── mcp.ts
│   ├── types.ts
│   ├── package.json
│   └── ...
│
├── db
│   └── init.sql
│
└── compose.yml

MCP サーバーの実装

MCP 部分

Figma MCP 実装 などを参考に、Tool を登録した McpServer インスタンスを返す createMcpServer() を実装しました。

createMcpServer
export async function createMcpServer() {
  const server = new McpServer(
    ServerInfo,
    { capabilities: { logging: {} },
  })
  const pool = mysql.createPool(DbInfo)

  // ここで Tool を登録
  registerTools(server, pool)
  server.server.onerror = console.error.bind(console);

  // McpServer のインスタンスを返す
  return server
}
registerTools
function registerTools(server: McpServer, pool: mysql.Pool) {
  const IdSchema = z.number().describe('データID(例:1)')

  server.tool(
    "get_data_by_id",
    "IDからデータ詳細を取得する",
    { id: IdSchema },
    async ({ id }) => {
      try {
        const [rowById] = await pool.query<[Data & RowDataPacket]>('SELECT * FROM test_data WHERE id = ?', [id]);
        return {
          content: [{ type: "text", text: JSON.stringify(rowById[0]) }],
        };
      } catch {
        return {
          isError: true,
          content: [{ type: "text", text: "error dayo" }]
        };
      }
    },
  )

registerTools() では、与えられた ID を使って DB にクエリする tool を登録します。いったん pool は引数で受け取るようにしました。mysql2/promise を使ってクエリ部分を実装しています。

Hono で動かす

上記 MCP サーバー Hono 上で公開します。

Hono v4.8.0 から @hono/mcp middleware が登場しました。
これを使うことで簡単に Streamable HTTP Transport の MCP サーバーを実装することが出来ます。

こんな感じ
export const mcpServer = await createMcpServer();
const transport = new StreamableHTTPTransport({
  sessionIdGenerator: undefined, // ステートレス
})

const app = new Hono();

app.all('/mcp', async (c) => {
  return transport.handleRequest(c)
})
実装の全体

azukiazusa さんの記事 を参考にしています(記事では express をつかっている)

import { StreamableHTTPTransport } from "@hono/mcp";
import { serve } from "bun";
import { Hono } from "hono";
import { createMcpServer } from "./mcp";

// MCPサーバーのインスタンスを作る
// テストで使うために export してた
export const mcpServer = await createMcpServer();
const transport = new StreamableHTTPTransport({
  sessionIdGenerator: undefined, // ステートレス
})

const app = new Hono();

// MCP 要素
app.all('/mcp', async (c) => {
  return transport.handleRequest(c)
})

const setupServer = async () => {
  await mcpServer.connect(transport)
}

setupServer()
  .then(() => {
    // 動作確認のため Dockerかローカルかで分岐させた
    const isDocker = process.env.DOCKER_ENV === 'true';
    const hostname = isDocker ? "0.0.0.0" : "localhost";
    serve({
      fetch: app.fetch,
      port: 5001,
      hostname: hostname,
    });
    console.log(`Server is running on http://${hostname}:5001/mcp`);
  })
  .catch((error) => {
    console.error("Error connecting MCP server:", error);
    process.exit(1);
  });

bun run index.ts で実行されるようになりました!

エージェントの実装

Mastra で自作 MCP サーバーを呼ぶ

Mastra を使うと、簡単なものならあっというまにエージェントができあがります。
npm create mastra@latest というコマンドも用意されていて、これを動かすだけでも天気の情報を返す MCP サーバーと連携する実装が自動で作られるので、一度遊んでみるとよいです。

今回は、上で作った MCP サーバーのクライアントを持つエージェントを実装します。

Agent
import { anthropic } from "@ai-sdk/anthropic";
import { Agent } from "@mastra/core/agent";
import { MCPClient } from "@mastra/mcp";

// 上で実装したものをURLで指定する
const mcp = new MCPClient({
  servers: {
    "get-data-mcp-server": {
      url: new URL(process.env.MCP_SERVER_URL ?? "http://localhost:5001/mcp"),
    }
  }
});

export const getDataAgent = new Agent({
  name: "Get Data Agent",
  instructions: `あなたはデータ取得専門のエージェントです。云々…`,
  model: anthropic('claude-4-sonnet-20250514'), // 環境変数 ANTHROPIC_API_KEY を設定
  tools: await mcp.getTools(),
});
Mastra本体
export const mastra = new Mastra({
  agents: { 
    getDataAgent, // 上で実装したエージェント
  },
  // 以下、その他の設定
  storage: new LibSQLStore({
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
});

bun run dev すると、Playground で下記の ChatGPT 的な画面を見ることが出来ます。

実際には業務でつかう「案件」のデータを対象にしたエージェントを実装したので、名前がちょっと違っています。

「この ID のデータを取得して」とお願いすると、DB にあるデータを返してくれます。今回はテストデータとして適当な案件データを作って入れていたので、それを返してくれました!

おわりに

Mastra や Hono を使うことで簡単に実装できました。
ぜひみなさんも何か作ってみてください〜👋

レバテック開発部

Discussion