😸
1ソースでMCPもCLIも動く:Hono中心エージェント開発
TL;DR
- AI エージェントに外部ツールをつなぐ経路は MCP(HTTP/stdio) と CLI の 2 系統。Hono を芯に据えると、アプリ本体のロジックを 1 箇所に集約し、同じ実装を MCP(Remote/Local)と CLI の両方で使い回せる。
- 役割分担は「Hono にロジック」「薄いアダプタで運ぶ」。
- 今回の記事にサンプルのリポジトリはこちら
- すぐ試す →
npx -y gpt-search@latest --stdioまたはnpx gpt-search@latest search "検索したい内容"(OPENAI_API_KEYが必要)。
はじめに
LLM/AI エージェントから外部ツールを呼ぶには、大きく 2 つの経路があります。
- MCP(Model Context Protocol): 通信は Streamable HTTP(SSE) と stdio の 2 種類。Remote/Local どちらの配置でも運用できる。
-
CLI 連携:
ghのようなコマンドを直接叩く。
Hono を使うと、これら 2 系統を 1 つのソースに集約し、MCP(Remote/Local)と CLI を同じ実装で提供できます。この記事では、こちら を参考に作成した GPT-5 ベースの検索ツールを題材に、アプリケーションロジックを Hono に集約し、Remote / Local / CLI の 3 形態で動かす設計を紹介します。
構成(ディレクトリと責務)
-
src/search.ts:検索の中核。OpenAI Responses API を呼び出し、結果テキストを返す。 -
src/app.ts:MCP サーバー本体。ツール名gpt-web-searchとしてrunSearch()を公開。 -
src/index.ts:Hono アプリ。/searchと/mcpを提供。 -
src/cli.ts:エントリーポイント。CLI / HTTP サーバ / stdio MCP を切り替える。
アーキテクチャ
- コアは
runSearch()に集約(トランスポート非依存)。 - MCP は
buildServer()でgpt-web-searchを公開。 - Hono は
/searchと/mcpを提供(StreamableHTTPTransportで接続)。 -
1 エントリ(
src/cli.ts) で CLI / HTTP / stdio を切り替え。
コア: src/search.ts
OpenAI の responses.create を呼び出し、Web 検索ツールを有効化します。戻り値はモデルのテキスト出力です。
// src/search.ts(抜粋)
import OpenAI from "openai";
import type { Env } from "./env";
export async function runSearch(input: string, env: Env): Promise<string> {
const client = new OpenAI({ apiKey: env.OPENAI_API_KEY! });
const response = await client.responses.create({
model: env.OPENAI_MODEL || "gpt-5",
input,
tools: [
{
type: "web_search_preview",
user_location: { type: "approximate", timezone: env.TZ || "Asia/Tokyo" },
search_context_size: env.SEARCH_CONTEXT_SIZE || "medium",
},
],
tool_choice: "auto",
parallel_tool_calls: true,
reasoning: { effort: env.REASONING_EFFORT || "low" },
store: false,
});
return response.output_text ?? "No response text available.";
}
MCP サーバー: src/app.ts
検索ロジックを gpt-web-search という 1 ツールに束ね、MCP として公開します。
// src/app.ts(抜粋)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { Env } from "./env";
import { runSearch } from "./search";
export const buildServer = (env: Env) => {
const server = new McpServer({ name: "gpt-web-search", version: "0.0.1" });
server.tool(
"gpt-web-search",
"Advanced web search agent.",
{ input: z.string().describe("Ask questions in natural language.") },
async ({ input }) => {
const text = await runSearch(input, env);
return { content: [{ type: "text", text }] };
}
);
return server;
};
HTTP エンドポイントと MCP: src/index.ts
Hono で /search と /mcp を提供します。/mcp は @hono/mcp の StreamableHTTPTransport で接続します。
// src/index.ts
import { Hono } from "hono";
import { StreamableHTTPTransport } from "@hono/mcp";
import { buildServer } from "./app";
import { runSearch } from "./search";
export interface Env {
OPENAI_API_KEY: string
SEARCH_CONTEXT_SIZE?: 'low' | 'medium' | 'high'
REASONING_EFFORT?: 'low' | 'medium' | 'high'
TZ?: string
}
const app = new Hono<{ Bindings: Env }>()
app.post("/search/:input", async (c) => {
const input = c.req.param("input");
if (!input) return c.text("Missing 'input'", 400);
const text = await runSearch(input, c.env as Env);
return c.text(text);
});
app.all("/mcp", async (c) => {
const server = buildServer(c.env);
const transport = new StreamableHTTPTransport();
await server.connect(transport);
return transport.handleRequest(c);
});
export default app;
実行エントリ: src/cli.ts
1 つのエントリで CLI / HTTP / stdio の 3 モードを切り替えます。hono-cli-adapter により、CLI コマンド(例: search)も同じ Hono アプリから提供できます。
// src/cli.ts
#!/usr/bin/env node
import { cli as honoCli } from 'hono-cli-adapter'
import { serve } from "@hono/node-server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import app from "./index";
import { buildServer } from "./app";
import type { Env } from "./env";
const args = process.argv.slice(2);
const useStdio = args.includes("--stdio");
const isServe = args[0] === "serve" || args[0] === "server" || args.includes("--server") || args.includes("--http");
const port = Number(process.env.PORT || "9876");
if (useStdio) {
(async () => {
const server = buildServer(process.env as unknown as Env);
const transport = new StdioServerTransport();
await server.connect(transport);
})().catch((e) => {
console.error("stdio server failed:", e?.message || e);
process.exit(1);
});
} else if (isServe) {
console.log(`HTTP mode: http://localhost:${port}/mcp`);
serve({ port, fetch: app.fetch });
} else {
await honoCli(app);
}
まとめ
Hono にアプリケーションロジックを集約し、HTTP / stdio / CLI の運び方は薄いアダプタに切り分けます。
これにより、同一コードをローカル、CLI 利用、Cloudflare Workers 配備まで一貫して使い回せます。
ぜひ試してみてください。
Discussion