はじめに
前回の記事の続きです。今回は、3つの Tool を MCP Server として公開し、Runtime からは MCP Client 経由で呼びます。
前回は State で取得済みを管理し、予定・天気・移動が揃ってから回答していました。
Step6の記事URLは、公開後に追記します。
これまでの関数はそのままです。変わったのは呼び方です。
runtime.ts
↓ listTools / callTool
mcp/client.ts
↓ stdio
mcp/server.ts life-assistant
↓ runTool
getSchedule / getWeather / getRoute
LLM に渡しているのは、今までどおり Function Calling の name / description / parameters です。その parameters は、こちらで書いていた定義ではなく、MCP の listTools が返した inputSchema です。実行は callTool でサーバーに渡します。
公開した Tool は1つのサーバーにまとめています。
life-assistant
├─ get_schedule
├─ get_weather
└─ get_route
今回の実行でも、予定、天気、移動の順に呼び、傘と 18:20 出発で終わっています。
成果物
--- MCP ---
server: life-assistant
tools: get_schedule, get_weather, get_route
--- User Prompt ---
今日どう動けばいい?
--- 1回目のLLM ---
MCP callTool get_schedule({"date":"今日"})
19:00 梅田で食事
--- 2回目のLLM ---
MCP callTool get_weather({"location":"梅田","time":"19:00"})
18:00から雨
--- 3回目のLLM ---
MCP callTool get_route({"origin":"自宅","destination":"梅田"})
30分
--- 回答 ---
終了条件: 予定・天気・移動が揃い、本文が返った
予定は19:00に梅田で食事です。18:00から雨なので傘が必要です。出発は18:20です。
シーケンス図
Function Calling
LLMへ渡す説明
name / description / parameters
MCP
Serverが公開する説明
name / description / inputSchema
client.ts が inputSchema を parameters に入れて、LLMへ渡す
LLMの tool_call は、callTool で Server に渡す
ディレクトリ構成
増えたのは src/mcp/ です。
agentic_ai_study/
├─ src/
│ ├─ index.ts
│ ├─ agent/
│ │ ├─ prompts.ts
│ │ ├─ state.ts
│ │ └─ runtime.ts listTools と callTool を使う
│ ├─ llm/
│ │ ├─ openrouter.ts
│ │ └─ types.ts
│ ├─ mcp/
│ │ ├─ server.ts life-assistant。Tool を公開する
│ │ └─ client.ts stdio でサーバーを起動し、一覧と実行を行う
│ └─ tools/
│ ├─ schedule.ts
│ ├─ weather.ts
│ ├─ route.ts
│ └─ execute.ts 名前から関数を実行する
├─ .env
└─ package.json
npm start
└─ tsx src/index.ts
└─ runtime.ts
└─ mcp/client.ts
└─ node --import tsx src/mcp/server.ts
コード
src/mcp/server.ts
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod/v4";
import { runTool } from "../tools/execute.js";
function createServer(): McpServer {
const server = new McpServer({ name: "life-assistant", version: "1.0.0" });
server.registerTool(
"get_schedule",
{
description: "指定した日の予定を取得する",
inputSchema: z.object({
date: z.string().describe("予定を確認する日。例: 今日"),
}),
},
async ({ date }) => ({
content: [{ type: "text", text: JSON.stringify(runTool("get_schedule", { date })) }],
}),
);
server.registerTool(
"get_weather",
{
description: "指定した場所と時刻の天気予報を取得する",
inputSchema: z.object({
location: z.string().describe("場所。例: 梅田"),
time: z.string().describe("確認したい時刻。例: 19:00"),
}),
},
async ({ location, time }) => ({
content: [
{ type: "text", text: JSON.stringify(runTool("get_weather", { location, time })) },
],
}),
);
server.registerTool(
"get_route",
{
description: "出発地から目的地までの移動時間を取得する",
inputSchema: z.object({
origin: z.string().describe("出発地。例: 自宅"),
destination: z.string().describe("目的地。例: 梅田"),
}),
},
async ({ origin, destination }) => ({
content: [
{
type: "text",
text: JSON.stringify(runTool("get_route", { origin, destination })),
},
],
}),
);
return server;
}
serveStdio(createServer);
src/mcp/client.ts
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import type { ToolDefinition } from "../llm/types.js";
type ToolContent = {
type: string;
text?: string;
};
export type LifeAssistant = {
serverName: string;
tools: ToolDefinition[];
callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
close: () => Promise<void>;
};
function textOf(content: ToolContent[]): string {
const block = content.find((item) => item.type === "text" && typeof item.text === "string");
if (!block?.text) {
throw new Error("MCP の結果に text がありません");
}
return block.text;
}
export async function connectLifeAssistant(): Promise<LifeAssistant> {
const client = new Client({ name: "agent-study", version: "1.0.0" });
const transport = new StdioClientTransport({
command: process.execPath,
args: ["--import", "tsx", "src/mcp/server.ts"],
});
await client.connect(transport);
const listed = await client.listTools();
const tools: ToolDefinition[] = listed.tools.map((tool) => ({
type: "function",
function: {
name: tool.name,
description: tool.description ?? "",
parameters: tool.inputSchema as ToolDefinition["function"]["parameters"],
},
}));
return {
serverName: client.getServerVersion()?.name ?? "life-assistant",
tools,
callTool: async (name, args) => {
const result = await client.callTool({ name, arguments: args });
const text = textOf(result.content as ToolContent[]);
if (result.isError) {
throw new Error(text);
}
return JSON.parse(text) as unknown;
},
close: () => client.close(),
};
}
src/agent/runtime.ts では、手元の executeTool の代わりにこの接続を使います。
const mcp = await connectLifeAssistant();
try {
await runLoop(mcp);
} finally {
await mcp.close();
}
const result = skipped
? skippedResult(state, call.function.name)
: await mcp.callTool(call.function.name, args as Record<string, unknown>);
listTools で受け取った mcp.tools を、OpenRouter の tools として渡しています。