
【CCA Foundations対策 / MCP編 #2】MCPでツールを定義する——サーバー実装とクライアント接続
Anthropic Academyの「Introduction to Model Context Protocol」コースをもとに解説しています。
前回(#1)でMCPのアーキテクチャと通信フローを把握した。今回は実装に入る——PythonでMCPサーバーを作り、ツールを定義し、Server Inspectorでデバッグし、クライアントから接続するまでを解説する。
この記事でわかること:
- FastMCPを使ったMCPサーバーの起動方法
@mcp.toolデコレータとPydantic Fieldによるツール定義- ツール記述(description)の質が選択信頼性に与える影響
- Server Inspectorを使ったデバッグ手順
- MCPクライアントの
list_tools/call_tool実装 - isErrorフラグとエラーカテゴリによる構造化エラー設計
FastMCPでサーバーを起動する
PythonのMCP SDKを使うと、サーバーの起動は1行で済む。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("TaskMCP", log_level="ERROR")
FastMCP にサーバー名を渡すだけで、MCP仕様に準拠したサーバーが起動する。ツール定義をゼロから書く必要はない。
今回はタスク管理システムを例に進める。タスクをインメモリの辞書で管理する。
tasks = {
"task-001": "認証モジュールのリファクタリング",
"task-002": "ユーザー登録フローのバグ修正",
"task-003": "パフォーマンステストの実施",
"task-004": "APIドキュメントの更新",
}
@mcp.toolでツールを定義する
ツールの定義にはデコレータを使う。JSONスキーマを手書きする必要はなく、Pythonの型ヒントとPydanticのFieldを書くだけでSDKが自動的にスキーマを生成する。
ツール①:タスクの内容を取得する
from pydantic import Field
@mcp.tool(
name="get_task",
description="タスクIDを指定してタスクの詳細を取得する。タスクの内容・担当者・期日を確認したい場合に使用する。プロジェクト設定やユーザー情報は返さない。"
)
def get_task(
task_id: str = Field(description="取得するタスクのID(例: task-001)")
):
if task_id not in tasks:
raise ValueError(f"タスク {task_id} が見つかりません")
return tasks[task_id]
ツール②:タスクの内容を更新する
@mcp.tool(
name="update_task",
description="タスクの内容テキストを更新する。既存のテキストを新しい内容に置き換える。タスクの削除・新規作成・ステータス変更には使用しない。"
)
def update_task(
task_id: str = Field(description="更新するタスクのID"),
new_content: str = Field(description="更新後のタスク内容")
):
if task_id not in tasks:
raise ValueError(f"タスク {task_id} が見つかりません")
tasks[task_id] = new_content
return f"タスク {task_id} を更新しました"
デコレータの name と description がClaudeに渡されるツール情報になる。Fieldの description が各引数の説明になる。
ツール記述の質が選択信頼性を決める
上記のdescriptionをよく見てほしい。単に「タスクを取得する」とだけ書くのではなく、**「何を返すか」と「何を返さないか」**を明示している。
なぜこれが重要か。
類似した2つのツールがある場合、Claudeはdescriptionを読んで「どちらを使うべきか」を判断する。descriptionが短すぎると境界が曖昧になり、誤選択が発生する。
悪い例:
get_task: "タスクを取得する"
update_task: "タスクを更新する"
良い例:
get_task: "タスクIDを指定してタスクの詳細を取得する。タスクの内容・担当者・期日を確認したい場合に使用する。プロジェクト設定やユーザー情報は返さない。"
update_task: "タスクの内容テキストを更新する。既存のテキストを新しい内容に置き換える。タスクの削除・新規作成・ステータス変更には使用しない。"
良い例では「このツールが対象とする操作」と「対象としない操作」の両方を書いている。これにより、Claudeが似た目的の2ツールを区別しやすくなる。
📋 試験ガイドより
公式試験ガイドのIn-Scope Topicsに「Tool interface design: Writing effective tool descriptions, splitting vs consolidating tools, tool naming to reduce ambiguity」が明記されている。ツール記述の質が選択信頼性の主要因であり、「何を返さないか」の明示と境界条件の記述が設計判断のポイントとして取り上げられている。
Server Inspectorでデバッグする
MCPサーバーを実装したら、アプリに組み込む前にServer Inspectorで動作確認できる。
mcp dev mcp_server.py
ブラウザでlocalhostのURLを開くと、Connectボタン→Tools一覧が表示される。ツールを選択してパラメータを入力し「Run Tool」をクリックすると、実際の実行結果を確認できる。
開発フローとしての使い方:
- ツールを定義する
- Server Inspectorで動作確認
- 問題があれば実装を修正
- アプリに組み込む
アプリ全体を動かさなくてもツール単体でテストできるため、問題の切り分けが早くなる。
クライアントを実装する
MCPサーバーが動いたら、アプリケーションコードから接続するクライアントを作る。実装が必要なのは主に2つの関数。
list_tools:使えるツールを取得する
async def list_tools(self) -> list[types.Tool]:
result = await self.session().list_tools()
return result.tools
アプリがClaudeにリクエストを送る前に、このリストを取得してツール定義として渡す。
call_tool:ツールを実行する
async def call_tool(
self, tool_name: str, tool_input: dict
) -> types.CallToolResult | None:
return await self.session().call_tool(tool_name, tool_input)
Claudeがツールをリクエストしてきたとき(stop_reason: tool_use)、このメソッドを呼んでMCPサーバーに実行を依頼する。tool_name と tool_input はClaudeのレスポンスから取得する。
エラーの構造化:isErrorとerrorCategory
ツールの実行が失敗したとき、単にエラーを投げるだけでなく構造化されたエラー情報を返すことで、Claudeが適切に対処できるようになる。
MCP SDKではツール結果に isError フラグを持たせられる。
# エラー時のtool_result例
{
"type": "tool_result",
"tool_use_id": "toolu_01...",
"content": "タスク task-999 が見つかりません",
"is_error": True
}
is_error: True を返すと、Claudeはエラーメッセージを読んで引数を修正して再試行したり、ユーザーに説明したりできる。エラーを握りつぶして空のレスポンスを返すより、Claudeが状況を把握しやすくなる。
さらに、エラーの種類をカテゴリとして返すと、呼び出し元が適切な対処を選べる。
| errorCategory | 意味 | 対処 |
|---|---|---|
transient |
一時的なエラー(ネットワーク障害・タイムアウト) | リトライ可能(isRetryable: true) |
validation |
入力値のエラー(存在しないID・不正なフォーマット) | 引数を修正してから再試行 |
permission |
権限エラー(アクセス拒否・認証失敗) | ユーザーに確認が必要 |
# 構造化エラーの例
{
"is_error": True,
"error_category": "validation",
"is_retryable": False,
"message": "タスク task-999 は存在しません。list_tasksで有効なIDを確認してください。"
}
📋 試験ガイドより
公式試験ガイドのIn-Scope Topicsに「Error handling and propagation: Structured error responses, transient vs business vs permission errors, local recovery before escalation」が明記されている。エラーカテゴリの分類とisRetryableフラグによる構造化エラー設計が、信頼性の高いMCPツール実装の判断ポイントとして記載されている。
よくある誤解まとめ
| 誤解 | 実際 |
|---|---|
| ツール名がわかりやすければdescriptionは短くていい | Claudeはdescriptionを主な判断材料にする。短いと類似ツールで誤選択が発生する |
| JSONスキーマを手書きする必要がある | PythonのMCP SDKは型ヒントとFieldからスキーマを自動生成する |
| ツール実行でエラーが起きたら例外を投げるだけでいい | is_error: Trueと構造化エラー情報を返すとClaudeが状況を把握して適切に対処できる |
| transientエラーは常にリトライしてよい | isRetryable: falseのケース(情報が存在しない場合など)もある。カテゴリに応じて判断する |
| MCPクライアントがツールを実行する | MCPクライアントはリクエストを中継するだけ。ツールの実行はMCPサーバーが担う |
設計の判断基準
| 場面 | やりがちな選択 | 正しい選択 | 判断の根拠 |
|---|---|---|---|
| 似た目的のツールを2つ定義したい | 短い1行のdescriptionを書く | 何を返すか・何を返さないかを含む3〜4文のdescriptionを書く | Claudeはdescriptionで選択する。境界が曖昧だと誤選択が頻発する |
| ツール実行でエラーが発生した | 例外を投げてアプリ側でデフォルト値を返す | is_error: Trueと具体的なエラーカテゴリを返す | Claudeがエラー内容を読んで引数修正・リトライ・ユーザーへの説明を判断できる |
| 一時的なネットワークエラーが発生した | 一律でリトライする | isRetryable: trueでtransientとして返す | カテゴリで対処を分けることで、リトライが意味ないケース(情報が存在しない)を区別できる |
まとめ
FastMCPと@mcp.toolデコレータでPythonの型ヒントからスキーマを自動生成できる- ツールのdescriptionは選択信頼性の主要因。「何を返すか・返さないか」を明示する
- Server Inspectorでアプリに組み込む前にツール単体をデバッグできる
- クライアントに必要な関数は
list_toolsとcall_toolの2つ - エラーは
is_error: True+errorCategory(transient/validation/permission)+isRetryableで構造化して返す
次回(#3)はResourcesとPrompts——データの公開方法とテンプレート管理を解説する。
← #1:MCPとは何か——ClaudeにツールをつなぐModel Context Protocol入門 | #3:MCPのリソースとプロンプト——データ公開とテンプレート管理 →