ツール
ツールとは、モデルが呼び出せる関数のことです。
普通の Python 関数に @mcp.tool() を付ければ宣言できます。API はこれだけです。
最初のツール
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str, limit: int) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (showing up to {limit})."
書いたコードを見てください。スキーマも JSON もプロトコルもなく、あるのは関数だけです。SDK はこの関数から 3 つのことを読み取ります。
- ツールの名前は関数名、つまり
search_booksです。 - モデルが目にする説明は docstring、つまり
Search the catalog by title or author.です。 - モデルが渡すことを許される引数は型ヒント、つまり
query: strとlimit: intから決まります。
入力スキーマ
SDK はこれらの型ヒントから JSON Schema を生成し、tools/list のときにクライアントへ送ります。
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"}
},
"required": ["query", "limit"],
"title": "search_booksArguments"
}
どちらの引数にもデフォルト値がないため、両方とも required に入っています。これはすぐ後で直します。(title キーは Pydantic が生成した付随物です。契約にあたるのは、プロパティとその型、そして required です。)
Tip
ここでの型ヒントはドキュメントではありません。契約そのものです。クライアントが "limit": "ten" を送ってきても、関数が実行される前に SDK が拒否します。
モデルが受け取るもの
{"query": "dune", "limit": 5} でツールを呼び出すと、結果は 2 つの部分からなります。
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
content はモデルが読むテキストです。structured_content はクライアントアプリケーション向けの型付きデータです。これが含まれているのは、戻り値の型を -> str と宣言したからです。
structured_content については、まだ気にしなくてかまいません。ツールから本物の Python オブジェクトを返せば、適切に処理されます。詳しくは 構造化出力 のページを参照してください。
試してみる
MCP Inspector でサーバーを実行してください。
uv run mcp dev server.py
表示される URL を開き、Tools タブに移動して search_books を呼び出してください。
Inspector は、必須の query テキストフィールドと必須の limit 数値フィールドを持つフォームを描画します。このフォームは型ヒントから組み立てられたものです。ほかの MCP クライアントもすべて同じようにします。
省略可能な引数
パラメーターにデフォルト値を与えると、必須ではなくなります。これだけです。ただの Python です。
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str, limit: int = 10) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (showing up to {limit})."
スキーマは次のようになります。
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
limit は required から外れ、"default": 10 が付きました。省略したクライアントには 10 が渡ります。Python とまったく同じです。
Field を使った詳細なスキーマ
型ヒントだけでもかなりのことができますが、引数に「説明」を付けたり、制約を課したりしたい場合もあります。
型を Annotated で包み、Pydantic の Field を加えます。
from typing import Annotated, Literal
from pydantic import Field
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(
query: Annotated[str, Field(description="Title or author to search for.")],
limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of results.")] = 10,
genre: Literal["fiction", "non-fiction", "poetry"] | None = None,
) -> str:
"""Search the catalog by title or author."""
where = f" in {genre}" if genre else ""
return f"Found 3 books matching {query!r}{where} (showing up to {limit})."
新しい点が 3 つあり、どれもパラメーターに付いています。
Field(description=...):引数ごとの説明です。モデルは docstring と合わせてこれを読みます。Field(ge=1, le=50):数値の範囲です。スキーマには"minimum": 1, "maximum": 50として入ります。Literal["fiction", "non-fiction", "poetry"]:列挙型です。モデルはこの中の 1 つしか選べません。
Check
制約は飾りではありません。limit=999 でツールを呼び出すと、関数が実行される前に SDK がツールエラーを返します。
Input should be less than or equal to 50
このエラーはツールの結果としてモデルに返され、モデルはそれを読んで有効な値でやり直します。le=50 と一度書いただけで、自己修正するエージェントがただで手に入ったことになります。
Info
FastAPI や Pydantic を使ったことがあれば、これはすべて既知の内容です。同じ Field、同じ Annotated、同じバリデーションです。MCP 固有の学ぶべきことはここにはありません。
パラメーターとしてのモデル
ツールが取る引数が 2、3 個を超えるときは、Pydantic モデルにまとめます。
from pydantic import BaseModel, Field
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
class Book(BaseModel):
title: str
author: str
year: int = Field(ge=1450, description="Year of first publication.")
@mcp.tool()
def add_book(book: Book) -> str:
"""Add a book to the catalog."""
return f"Added {book.title!r} by {book.author} ({book.year})."
Book のスキーマはツールの入力スキーマの中に($defs の参照として)ネストされます。モデルはこれを JSON オブジェクトとして埋め、関数はバリデーション済みの本物の Book インスタンスを受け取ります。.title、.author、.year の各属性が使えます。
自由に組み合わせられます。普通のパラメーターとモデルのパラメーターを並べても、モデルをネストしても、モデルのリストにしてもかまいません。どこまで行っても Pydantic です。
async def
ツールが I/O を行う場合(API を呼ぶ、ファイルを読む、データベースに問い合わせるなど)は、async def で宣言し、その中で await してください。SDK 側がそれを await します。
普通の def のツールも使えます。SDK がスレッド内で実行するので、サーバーをブロックすることはありません。
ほかに設定することはありません。
名前、タイトル、アノテーション
SDK が推論するものはすべて、デコレーターで上書きできます。
from mcp.server import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer("Bookshop")
@mcp.tool(
title="Search the catalog",
annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
)
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
titleは UI 向けの、人が読むための名前です。クライアントはsearch_booksの代わりに "Search the catalog" を表示します。annotationsはクライアントに対する振る舞いのヒントです。read_only_hint=True:このツールは何も変更しません。open_world_hint=False:開かれた Web ではなく、閉じた対象の集合(このカタログ)に対して働きます。- 残りの 2 つ、
destructive_hintとidempotent_hintは「書き込む」ツールを説明するものです。何かを削除する可能性があるか、そして 2 回呼び出すのは 1 回呼び出すのと同じか、を表します。仕様はどちらも読み取り専用でないツールに対してだけ定義しているので、search_booksに付けても何も伝わりません。
行儀のよいクライアントは、これらを使って「これを実行する前にユーザーに確認する必要があるか」といったことを判断します。これらはヒントであって、セキュリティではありません。クライアントがこれらを守ることを決して当てにしないでください。
Tip
関数名と docstring から導きたくない場合は、@mcp.tool() に name= と description= を渡すこともできます。たいていは導くほうで十分です。
まとめ
- 関数に
@mcp.tool()を付けるとツールになります。名前は関数から、説明は docstring から取られます。 - 型ヒントがそのまま入力スキーマです。デフォルト値を付けると引数は省略可能になります。
Annotated[..., Field(...)]で説明と制約を、Literalで列挙型を加えられます。- 構造化された「ボディ」を受け取るには、Pydantic モデルのパラメーターを使います。
- 不正な引数は自動的に拒否され、モデルが読んで立て直せるエラーが返ります。
- I/O には
async defを、それ以外には普通のdefを使います。
return した値がその後どうなるかは、構造化出力 で扱います。