ツールは、モデルが呼び出せる関数です。server.tool(name, config) で登録します:
server.tool("search_issues", {
title: "Search issues",
description: "Search open GitHub issues in a repository. Use it before filing a new issue.",
input: z.object({
repo: z.string().describe("owner/name"),
query: z.string(),
limit: z.number().int().max(50).default(10),
}),
annotations: { readOnlyHint: true, openWorldHint: true },
run: async ({ repo, query, limit }, { signal }) => {
const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(`${query} repo:${repo}`)}&per_page=${limit}`, { signal });
const { items } = await res.json();
return items.map((i: { title: string; html_url: string }) => `${i.title} — ${i.html_url}`).join("\n");
},
});設定#
| オプション | 型 | 説明 |
|---|---|---|
description | string | ツールが何をするか、いつ使うか。モデルはこの文章を読みます。 |
title | string | クライアントに表示される名前。 |
input | Standard Schema | 引数のスキーマ。引数のないツールでは省略します。 |
output | Standard Schema | 構造化された結果のスキーマ。構造化出力を参照。 |
annotations | ToolAnnotations | クライアント向けのヒント:readOnlyHint、destructiveHint、idempotentHint、openWorldHint。 |
run | (input, ctx) => value | ハンドラー。非同期でもかまいません。 |
ツール名は 1〜128 文字で、英字、数字、_、.、- だけを使えます。不正な名前や重複した名前を登録するとすぐにエラーになるので、ミスはモデルがツールを呼んだときではなく、サーバーの起動時に見つかります。
入力の検証#
picomcp は run を呼ぶ前に、スキーマで引数を検証します。デフォルト値と変換も適用されるので、上の例では run の中の limit は必ず数値です。
検証に失敗した場合、ツールは呼ばれません。クライアントには問題のあるフィールドを示すツールエラーが返るので、モデルは呼び出しを直して再試行できます:
Invalid arguments for tool "search_issues":
limit: Too big: expected number to be <=50戻り値#
自然な形で返してください。picomcp が MCP の結果に変換します:
run の戻り値 | クライアントが受け取るもの |
|---|---|
string | テキストブロック 1 つ |
| オブジェクト、配列、数値 | その JSON をテキストとして。オブジェクトは structuredContent としても送られます。 |
| コンテンツブロック、またはその配列 | それらのブロックをそのまま |
{ content: [...] } | 結果をそのまま。細かく制御したいとき向け |
undefined | 空の結果 |
コンテンツブロックはヘルパーで作ります:
import { audio, image, link, text } from "picomcp";
run: async () => [
text("Here is the chart:"),
image(await renderPng(), "image/png"), // Uint8Array or base64 string
link("file:///reports/q3.csv", "q3.csv", { mimeType: "text/csv" }),
];構造化出力#
結果をモデルだけでなくクライアントやコードでも使う場合は、output スキーマを宣言します:
server.tool("get_price", {
input: z.object({ sku: z.string() }),
output: z.object({ sku: z.string(), cents: z.number().int(), currency: z.literal("EUR") }),
run: async ({ sku }) => ({ sku, cents: await priceOf(sku), currency: "EUR" }),
});すると picomcp は:
tools/listでスキーマをoutputSchemaとして公開し、runの戻り値を型レベルでチェックし、- 実行時にも値を検証して
structuredContentとして送ります。古いクライアント向けに JSON のテキストも添えます。
値がスキーマに合わない場合、クライアントは壊れたデータではなくツールエラーを受け取ります。
エラー#
run の中でエラーを投げてください。そのメッセージはツールエラー(isError: true)になり、モデルはそれを読んで対応できます:
run: async ({ id }) => {
const user = await db.users.find(id);
if (!user) throw new Error(`No user with id ${id}. Call list_users to see valid ids.`);
return user;
};エラーメッセージはモデルに向けて書きましょう。何がうまくいかなかったのか、次に何をすべきかを伝えます。
ツールエラーは通常の会話の一部ですが、プロトコルエラーは違います。McpError は、ユーザーに権限がないなど、リクエスト自体が誤っている場合にだけ使ってください:
import { McpError } from "picomcp";
throw new McpError(-32001, "Not authorised for this workspace");キャンセル#
run の第 2 引数には AbortSignal が入っています。クライアントが notifications/cancelled を送ると発火します。たいていはユーザーが停止ボタンを押したときです。fetch など、シグナルを受け付ける API に渡してください:
run: async ({ url }, { signal }) => {
const res = await fetch(url, { signal });
return res.text();
};リクエストがキャンセルされると、プロトコルの規定どおり、picomcp はそのリクエストに応答を返しません。
メタデータ#
クライアントがリクエストに _meta オブジェクトを付けて送った場合、ctx.meta に入ります。クライアントはこれをトレースや進捗トークンに使います。
良いツールを書くために#
- 1 つのツールに 1 つの仕事。
invoice(action)よりもcreate_invoiceとsend_invoiceのほうが良い設計です。 - 引数を説明する。 各フィールドの
.describe()は、モデルが見る JSON Schema に反映されます。 - 返す量を減らす。 モデルはトークンごとにコストがかかります。API レスポンス全体ではなく、必要なフィールドだけを返しましょう。
- 正直にアノテーションする。
readOnlyHint: trueがあると、クライアントは確認を省略できます。書き込みを行うツールには絶対に付けないでください。