MCP ツールは、MCP サーバー がクライアントから呼べるよう公開する関数です。LLM はツールを呼び出してデータの検索、計算、API の呼び出しができます。MCP サーバーがツールを実行し、結果を返します。
ステートレスな createMcpHandler サーバーには @modelcontextprotocol/server を使います。McpAgent は非推奨で、機能は凍結されています。既存の McpAgent ルートは、移行中に限り @modelcontextprotocol/sdk だけを使い続けてください。
WebMCP の例
server.registerTool() で、ステートレスな McpServer インスタンスにツールを登録します。各ツールには名前、説明、Zod ↗ や Valibot ↗ などのスキーマライブラリで定義した入力スキーマ、ハンドラー関数があります。
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "Math", version: "1.0.0" });
server.registerTool(
"add",
{
description: "Add two numbers together",
inputSchema: { a: z.number(), b: z.number() },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
);
return server;
}import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "Math", version: "1.0.0" });
server.registerTool(
"add",
{
description: "Add two numbers together",
inputSchema: { a: z.number(), b: z.number() },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
);
return server;
}ツールハンドラーは検証済みの入力を受け取り、content 配列を持つオブジェクトを返す必要があります。各コンテンツ項目には type(通常は "text")と対応するデータがあります。
ツールの結果は、コンテンツパーツの配列として返ります。いちばん多い型は text ですが、画像や埋め込みリソースも返せます。
server.registerTool(
"lookup",
{
description: "Look up a user by ID",
inputSchema: { userId: z.string() },
},
async ({ userId }) => {
const user = await db.getUser(userId);
if (!user) {
return {
isError: true,
content: [{ type: "text", text: `User ${userId} not found` }],
};
}
return {
content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
};
},
);server.registerTool(
"lookup",
{
description: "Look up a user by ID",
inputSchema: { userId: z.string() },
},
async ({ userId }) => {
const user = await db.getUser(userId);
if (!user) {
return {
isError: true,
content: [{ type: "text", text: `User ${userId} not found` }],
};
}
return {
content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
};
},
);ツール呼び出しが失敗したことを示すには、isError: true を設定します。LLM はエラーメッセージを受け取り、次の対応を判断できます。
description パラメーターは重要です。LLM はこれを読んで、ツールを呼ぶかどうか、いつ呼ぶかを決めます。説明は次のように書いてください。
- 具体的に何をするかを書く: 「Weather tool」より「都市の現在の天気を取得する」
- 入力を明確にする: 「都市名を文字列で指定する」と書くと、LLM が呼び出しを正しく整形しやすくなります
- 制限を正直に書く: 「米国の都市のみ対応」と書けば、未対応の入力で呼ばれにくくなります
ツール入力は Zod スキーマとして定義され、ハンドラー実行前に自動検証されます。各パラメーターの文脈を LLM に伝えるには、Zod の .describe() メソッドを使います。
server.registerTool(
"search",
{
description: "Search for documents by query",
inputSchema: {
query: z.string().describe("The search query"),
limit: z
.number()
.min(1)
.max(100)
.default(10)
.describe("Maximum number of results to return"),
category: z
.enum(["docs", "blog", "api"])
.optional()
.describe("Filter by content category"),
},
},
async ({ query, limit, category }) => {
const results = await searchIndex(query, { limit, category });
return {
content: [{ type: "text", text: JSON.stringify(results) }],
};
},
);server.registerTool(
"search",
{
description: "Search for documents by query",
inputSchema: {
query: z.string().describe("The search query"),
limit: z
.number()
.min(1)
.max(100)
.default(10)
.describe("Maximum number of results to return"),
category: z
.enum(["docs", "blog", "api"])
.optional()
.describe("Filter by content category"),
},
},
async ({ query, limit, category }) => {
const results = await searchIndex(query, { limit, category });
return {
content: [{ type: "text", text: JSON.stringify(results) }],
};
},
);ステートレスな MCP サーバーでは、ファクトリ関数内でツールを定義し、サーバーを createMcpHandler に渡します。
import { createMcpHandler } from "agents/mcp/server";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "My Tools", version: "1.0.0" });
server.registerTool(
"ping",
{ description: "Check if the server is alive", inputSchema: {} },
async () => ({
content: [{ type: "text", text: "pong" }],
}),
);
return server;
}
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
};import { createMcpHandler } from "agents/mcp/server";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "My Tools", version: "1.0.0" });
server.registerTool(
"ping",
{ description: "Check if the server is alive", inputSchema: {} },
async () => ({
content: [{ type: "text", text: "pong" }],
}),
);
return server;
}
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
} satisfies ExportedHandler;このセクションは、移行中の既存レガシールートにだけ当てはまります。ツールは McpAgent の init() メソッドで定義します。ツールは this 経由でエージェントインスタンスにアクセスできるので、状態の読み書きができます。
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "Stateful Tools", version: "1.0.0" });
async init() {
this.server.tool(
"incrementCounter",
"Increment and return a counter",
{},
async () => {
const count = (this.state?.count ?? 0) + 1;
this.setState({ count });
return {
content: [{ type: "text", text: `Counter: ${count}` }],
};
},
);
}
}import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "Stateful Tools", version: "1.0.0" });
async init() {
this.server.tool(
"incrementCounter",
"Increment and return a counter",
{},
async () => {
const count = (this.state?.count ?? 0) + 1;
this.setState({ count });
return {
content: [{ type: "text", text: `Counter: ${count}` }],
};
},
);
}
}