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()
を実装しました。
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
}
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 サーバーのクライアントを持つエージェントを実装します。
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(),
});
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