Tools
Maschinelle Übersetzung
Diese Seite wurde automatisch aus der englischen Dokumentation übersetzt, und die englische Seite ist die maßgebliche Fassung. Wenn sich etwas falsch liest, erklärt Übersetzungen, wie du es melden kannst.
Ein Tool ist eine Funktion, die das Modell aufrufen kann.
Du deklarierst eines, indem du @mcp.tool() auf eine ganz normale Python-Funktion setzt. Das ist die ganze API.
Dein erstes Tool
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})."
Sieh dir an, was du geschrieben hast. Keine Schemas, kein JSON, kein Protokoll, nur eine Funktion. Das SDK liest drei Dinge daraus:
- Der Name des Tools ist der Name der Funktion:
search_books. - Die Beschreibung, die das Modell sieht, ist der Docstring:
Search the catalog by title or author. - Die Argumente, die das Modell übergeben darf, ergeben sich aus den Type Hints:
query: strundlimit: int.
Das Eingabeschema
Aus diesen Type Hints erzeugt das SDK ein JSON Schema und sendet es während tools/list an den Client:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"}
},
"required": ["query", "limit"],
"title": "search_booksArguments"
}
Beide Argumente stehen in required, weil keines einen Standardwert hat. Das änderst du gleich. (Die title-Schlüssel sind Pydantic-Artefakte; die Properties, ihre Typen und required sind der Vertrag.)
Tip
Type Hints sind hier keine Dokumentation. Sie sind der Vertrag. Sendet ein Client "limit": "ten",
weist das SDK das zurück, bevor deine Funktion überhaupt läuft.
Was das Modell zurückbekommt
Ruf das Tool mit {"query": "dune", "limit": 5} auf, und das Ergebnis hat zwei Teile:
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 ist der Text, den das Modell liest. structured_content sind typisierte Daten für die Client-Anwendung. Es ist da, weil du den Rückgabetyp als -> str deklariert hast.
Kümmere dich noch nicht um structured_content. Gib aus deinen Tools echte Python-Objekte zurück, und es passiert das Richtige; die Seite Strukturierte Ausgabe dreht sich genau darum.
Ausprobieren
Starte den Server mit dem MCP Inspector:
uv run mcp dev server.py
Öffne die URL, die er ausgibt, geh zum Tab Tools und ruf search_books auf.
Der Inspector zeigt ein Formular mit einem erforderlichen Textfeld query und einem erforderlichen Zahlenfeld limit. Dieses Formular hat er aus deinen Type Hints gebaut. Das macht jeder andere MCP-Client genauso.
Optionale Argumente
Gib einem Parameter einen Standardwert, und er ist nicht mehr erforderlich. Das ist alles. Ganz normales 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})."
Das Schema zieht mit:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
limit ist aus required verschwunden und hat "default": 10 bekommen. Ein Client, der es weglässt, bekommt 10 – genau wie in Python.
Reichere Schemas mit Field
Type Hints bringen dich weit, aber manchmal willst du ein Argument beschreiben oder einschränken.
Verpacke den Typ in Annotated und füge ein Pydantic-Field hinzu:
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})."
Drei neue Dinge, alle an den Parametern:
Field(description=...): eine Beschreibung pro Argument, die das Modell zusätzlich zum Docstring liest.Field(ge=1, le=50): numerische Grenzen. Sie landen im Schema als"minimum": 1, "maximum": 50.Literal["fiction", "non-fiction", "poetry"]: ein Enum. Das Modell kann nur einen dieser Werte wählen.
Check
Constraints sind keine Dekoration. Ruf das Tool mit limit=999 auf, und das SDK antwortet mit einem
Tool-Fehler, bevor deine Funktion läuft:
Input should be less than or equal to 50
Dieser Fehler geht als Tool-Ergebnis zurück an das Modell, das Modell liest ihn und versucht es mit
einem gültigen Wert erneut. Du hast einmal le=50 geschrieben und bekommst selbstkorrigierende Agenten umsonst dazu.
Info
Wenn du FastAPI oder Pydantic schon benutzt hast, kennst du das alles bereits. Es ist dasselbe Field,
dasselbe Annotated, dieselbe Validierung. Es gibt hier nichts MCP-Spezifisches zu lernen.
Ein Modell als Parameter
Nimmt ein Tool mehr als ein paar Argumente entgegen, fasse sie in einem Pydantic-Modell zusammen:
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})."
Das Book-Schema wird in das Eingabeschema des Tools eingebettet (als $defs-Referenz), das Modell füllt es als JSON-Objekt aus, und deine Funktion erhält eine echte Book-Instanz, bereits validiert, mit den Attributen .title, .author und .year.
Du kannst frei kombinieren: einfache Parameter neben Modell-Parametern, verschachtelte Modelle, Listen von Modellen. Es ist Pydantic bis ganz nach unten.
async def
Macht ein Tool I/O (ruft eine API auf, liest eine Datei, fragt eine Datenbank ab), deklariere es als async def und verwende await darin. Das SDK wartet darauf.
Ein Tool mit einfachem def funktioniert auch: Das SDK führt es in einem Thread aus, damit es den Server nie blockiert.
Mehr gibt es nicht zu konfigurieren.
Namen, Titel und Annotationen
Alles, was das SDK ableitet, kannst du im Dekorator überschreiben:
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}."
titleist ein menschenlesbarer Name für UIs. Clients zeigen „Search the catalog“ stattsearch_books.annotationssind Hinweise zum Verhalten für den Client:read_only_hint=True: Dieses Tool ändert nichts.open_world_hint=False: Es arbeitet auf einer geschlossenen Menge von Dingen (diesem Katalog), nicht im offenen Web.- Die beiden anderen,
destructive_hintundidempotent_hint, beschreiben ein Tool, das schreibt: Darf es etwas löschen, und ist zweimal aufrufen dasselbe wie einmal aufrufen? Die Spezifikation definiert beide nur für Tools, die nicht read-only sind, deshalb würden sie beisearch_booksnichts aussagen.
Ein gut erzogener Client nutzt sie, um Dinge zu entscheiden wie „Muss ich die Person fragen, bevor ich das ausführe?“. Es sind Hinweise, keine Sicherheit. Verlass dich nie darauf, dass ein Client sie beachtet.
Tip
@mcp.tool() akzeptiert auch name= und description=, falls du sie nicht aus dem Funktionsnamen
und dem Docstring ableiten lassen willst. Meistens willst du das aber.
Zusammenfassung
@mcp.tool()auf einer Funktion macht sie zum Tool. Name aus der Funktion, Beschreibung aus dem Docstring.- Type Hints sind das Eingabeschema. Standardwerte machen Argumente optional.
Annotated[..., Field(...)]fügt Beschreibungen und Constraints hinzu;Literalfügt Enums hinzu.- Über einen Pydantic-Modell-Parameter nimmst du einen strukturierten „Body“ entgegen.
- Ungültige Argumente werden für dich abgewiesen, mit einem Fehler, den das Modell lesen und aus dem es sich erholen kann.
async deffür I/O, einfachesdeffür alles andere.
Strukturierte Ausgabe beschreibt, was mit dem Wert passiert, den du mit return zurückgibst.