0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Agentic AIを学びたい(Step7:ToolをMCP Serverで公開する)

0
Posted at

はじめに

前回の記事の続きです。今回は、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 として渡しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?