Der Client
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 Client ist der Weg, auf dem ein Python-Programm mit einem MCP-Server spricht.
Er ist ein einziges Objekt mit einem einzigen Lebenszyklus: erzeugen, async with betreten, Methoden aufrufen. Jedes Verb des Protokolls (die Tools auflisten, eines aufrufen, eine Ressource lesen, einen Prompt rendern) ist eine async-Methode darauf, die ein typisiertes Ergebnis zurückgibt.
Der erste Client
from mcp import Client
from mcp.server import MCPServer
mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.")
@mcp.tool()
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
async def main() -> None:
async with Client(mcp) as client:
print(client.server_info)
print(client.server_capabilities)
print(client.protocol_version)
print(client.instructions)
Der Server oben ist nur da, damit du etwas hast, womit du dich verbinden kannst. Der Client sind die fünf hervorgehobenen Zeilen.
Client(mcp)bekommt das Server-Objekt selbst. Das ist der In-Memory-Transport: kein Subprozess, kein Port, kein HTTP. So verbindet sich jedes Beispiel auf dieser Seite und jeder Test, den du schreibst.async withist der Lebenszyklus. Beim Betreten wird verbunden und ausgehandelt, beim Verlassen getrennt. Es gibt keinconnect()/close()-Paar, und einClientlässt sich nach dem Ende des Blocks nicht wiederverwenden.- Innerhalb des Blocks liegen die Fakten zur Verbindung bereits als einfache Properties vor.
Was sich an Client übergeben lässt
Client nimmt ein positionelles Argument und leitet den Transport aus dessen Typ ab:
- Eine Instanz von
MCPServer(oder des Low-Level-Server): Verbindung im selben Prozess. - Ein URL-String (
Client("http://localhost:8000/mcp")): Streamable HTTP, der Weg für die Produktion. - Ein Transport: alles, was sich mit
async with ... as (read, write)verwenden lässt, etwastdio_client(...)um einen Subprozess herum.
Alles Übrige auf dieser Seite ist in allen drei Fällen identisch. Header, Subprozesse, Timeouts und das Transport-Protokoll haben ihre eigene Seite: Client-Transporte.
Was ein verbundener Client mitbringt
Vier schreibgeschützte Properties, die gefüllt sind, sobald du den Block betrittst:
client.server_info: die Identität des Servers oderNonebei einem Server der 2026er-Generation, der keine meldet (python-sdk-Server tun das standardmäßig).server_info.nameist hier"Bookshop",server_info.versionist das, was der Server meldet.client.server_capabilities: was der Server kann (tools,resources,prompts,completions, ...). Eine Capability, die der Server nicht hat, istNone.client.protocol_version: die Protokollversion, auf die sich beide Seiten geeinigt haben. Hier ist sie"2026-07-28".client.instructions: derinstructions=-String des Servers oderNone, wenn er keinen gesetzt hat.
Eine Protokollversion hast du nie ausgewählt. Standardmäßig sondiert der Client den Server und fällt bei älteren auf den klassischen Handshake zurück, sodass ein einziger Client mit Servern jeder Generation funktioniert. Wenn du das steuern musst: Alles Weitere steht in Protokollversionen.
Tip
client.session ist die darunterliegende ClientSession, der Low-Level-Notausgang.
Für nichts auf dieser Seite wirst du sie brauchen.
Tools auflisten
from mcp import Client
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool(title="Search the catalog")
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})."
async def main() -> None:
async with Client(mcp) as client:
result = await client.list_tools()
for tool in result.tools:
print(tool.name)
print(tool.title)
print(tool.description)
print(tool.input_schema)
list_tools() gibt ein ListToolsResult zurück; die Tools stehen in .tools. Jedes davon ist die vollständige Definition, die ein Host einem Modell übergeben würde:
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
und tool.input_schema ist das JSON-Schema, das der Server aus den Type Hints der Funktion abgeleitet hat:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
Dieses Schema ist alles, was eine UI braucht, um ein Argumentformular zu rendern, und alles, was ein Modell braucht, um gültige Argumente zu erzeugen.
Tip
title ist optional, also muss sich eine UI, die einem Menschen Tools anzeigt, entscheiden: den title, wenn es einen gibt,
sonst den name. from mcp.shared.metadata_utils import get_display_name macht genau das –
für Tools, Ressourcen, Ressourcen-Templates und Prompts.
Ein Tool aufrufen
call_tool(name, arguments) führt das Tool aus und gibt dir ein CallToolResult zurück.
from pydantic import BaseModel
from mcp import Client
from mcp.server import MCPServer
from mcp.types import TextContent
mcp = MCPServer("Bookshop")
class Book(BaseModel):
title: str
author: str
year: int
@mcp.tool()
def lookup_book(title: str) -> Book:
"""Look up a book by its exact title."""
if title != "Dune":
raise ValueError(f"No book titled {title!r} in the catalog.")
return Book(title="Dune", author="Frank Herbert", year=1965)
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool("lookup_book", {"title": "Dune"})
for block in result.content:
if isinstance(block, TextContent):
print(block.text)
print(result.structured_content)
print(result.is_error)
lookup_book auf dem Server gibt ein Pydantic-Book zurück. Das sieht der Client:
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
result.is_error # False
Ein Rückgabewert, drei Dinge zu lesen. Jedes hat einen anderen Abnehmer.
content: was das Modell liest
content ist eine list von Content-Blöcken, und ein Content-Block ist eine Union: TextContent, ImageContent, AudioContent, ResourceLink oder EmbeddedResource. Ein Tool kann mehrere zurückgeben, auch verschiedener Art.
Deshalb grenzt main mit isinstance(block, TextContent) ein, bevor es block.text anfasst. Beachte, dass es kein .text außerhalb des isinstance gibt: Der Typprüfer lässt das nicht zu, denn ImageContent hat .data, nicht .text. Die Union ist ehrlich darüber, was ein Tool dir schicken darf; dein Code sollte es auch sein.
structured_content: was deine Anwendung liest
structured_content ist der Rückgabewert des Tools als JSON, passend zum deklarierten output_schema des Tools. Kein String-Parsing, kein Raten.
Wenn beide vorhanden sind, sagen sie absichtlich zweimal dasselbe: content ist für ein Modell, structured_content ist für Code. Woher die strukturierte Hälfte kommt und wie du sie steuerst, steht auf der Seite Strukturierte Ausgabe.
is_error: ob das Tool fehlgeschlagen ist
Ein Tool, das eine Exception auslöst, löst in deinem Client keine aus. Es kommt als gewöhnliches Ergebnis mit is_error=True zurück.
Check
Frag lookup_book nach "Solaris" (einem Titel, der nicht im Katalog steht), und die Funktion löst
ValueError aus. Der Aufruf kehrt trotzdem normal zurück:
result.is_error # True
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
result.structured_content # None
Die Meldung der Exception ist in content gelandet, wo das Modell sie lesen und es erneut versuchen kann. Das
ist Absicht: Ein Tool-Fehler ist Teil des Gesprächs, kein Absturz. Sieh dir immer is_error an,
bevor du structured_content vertraust.
Warning
is_error=True deckt mehr ab als dein eigenes raise. Frag nach einem Tool, das der Server gar nicht hat
(call_tool("does_not_exist", {})), und nichts wird ausgelöst. Du bekommst dieselbe Form zurück,
is_error=True mit Unknown tool: does_not_exist in content. Eine Client-Methode löst
MCPError nur aus, wenn der Server mit einem JSON-RPC-Fehler statt eines Ergebnisses antwortet, und
Fehler behandeln erklärt, wann ein Server welches davon erzeugt.
Ressourcen
Die Ressourcen-Verben kommen paarweise: zwei Wege zum Auflisten, einer zum Lesen.
from mcp import Client
from mcp.server import MCPServer
from mcp.types import TextResourceContents
mcp = MCPServer("Bookshop")
@mcp.resource("catalog://genres")
def genres() -> list[str]:
"""The genres the catalog is organised by."""
return ["fiction", "non-fiction", "poetry"]
@mcp.resource("catalog://genres/{genre}")
def books_in_genre(genre: str) -> str:
"""Every title we stock in one genre."""
return f"3 books filed under {genre}."
async def main() -> None:
async with Client(mcp) as client:
listed = await client.list_resources()
print([resource.uri for resource in listed.resources])
templates = await client.list_resource_templates()
print([template.uri_template for template in templates.resource_templates])
result = await client.read_resource("catalog://genres/poetry")
for contents in result.contents:
if isinstance(contents, TextResourceContents):
print(contents.text)
list_resources()gibt die konkreten Ressourcen zurück, die mit festem URI. Hier:['catalog://genres'].list_resource_templates()gibt die parametrisierten zurück. Hier:['catalog://genres/{genre}']. Es sind zwei verschiedene Listen, weil ein Template erst lesbar ist, wenn du es ausfüllst.read_resource(uri)nimmt einen URI als einfachenstrund funktioniert mit beiden: Übergib"catalog://genres/poetry", und der Server ordnet ihn dem Template zu.
read_resource gibt contents zurück, eine Liste aus TextResourceContents oder BlobResourceContents. Dieselbe Idee wie beim Tool-Content: mit isinstance eingrenzen, dann .text (oder .blob) lesen.
Ein Client kann sich auch mitteilen lassen, wann sich eine Ressource ändert. Auf Verbindungen der 2025er-Generation geschieht das über subscribe_resource(uri) / unsubscribe_resource(uri) – ein Methodenpaar, das MCPServer nicht implementiert, sodass der Request auf der 2026-07-28-Leitung (wo es diese Verben nicht mehr gibt) mit -32601, Method not found, beantwortet wird. Der Ersatz in 2026 ist ein subscriptions/listen-Stream, den MCPServer sehr wohl bedient – server_capabilities.resources.subscribe ist dort True –, und wie du ihn mit client.listen(...) konsumierst, steht auf der Seite Abonnements in diesem Abschnitt.
Prompts
from mcp import Client
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.prompt(title="Recommend a book")
def recommend(genre: str) -> str:
"""Ask for a recommendation in a genre."""
return f"Recommend one {genre} book from the catalog and say why."
async def main() -> None:
async with Client(mcp) as client:
listed = await client.list_prompts()
print(listed.prompts)
result = await client.get_prompt("recommend", {"genre": "poetry"})
for message in result.messages:
print(message.role, message.content)
list_prompts() sagt dir, was der Server anbietet und was jeder Prompt braucht:
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
get_prompt(name, arguments) rendert ihn. Das Argument-Dict ist str -> str: Prompt-Argumente sind immer Strings. Das Ergebnis ist messages, eine Liste von PromptMessage, jeweils mit einer role und einem content-Block:
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
Ein Host reicht diese Nachrichten direkt an das Modell weiter. Das ist das ganze Feature.
Vervollständigungen
Ein Server mit einem Handler für Vervollständigungen kann Argumente von Prompts und Ressourcen-Templates automatisch vervollständigen, während die Person tippt.
from mcp import Client
from mcp.server import MCPServer
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
mcp = MCPServer("Bookshop")
GENRES = ["fiction", "non-fiction", "poetry"]
@mcp.prompt()
def recommend(genre: str) -> str:
"""Ask for a recommendation in a genre."""
return f"Recommend one {genre} book from the catalog and say why."
@mcp.completion()
async def complete_genre(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
return Completion(values=[genre for genre in GENRES if genre.startswith(argument.value)])
async def main() -> None:
async with Client(mcp) as client:
result = await client.complete(
ref=PromptReference(type="ref/prompt", name="recommend"),
argument={"name": "genre", "value": "p"},
)
print(result.completion.values)
refsagt, welchen Prompt oder welches Template du ausfüllst: einePromptReferenceoder eineResourceTemplateReference.argumentist{"name": ..., "value": ...}: das Argument und das, was die Person bisher getippt hat.
Die Antwort steht in result.completion.values. Tippe "p", und der Server liefert ['poetry']. Die Serverseite, und wie ein Handler die anderen, bereits ausgefüllten Argumente nutzt, um seine Vorschläge einzugrenzen, steht auf der Seite Vervollständigungen.
Paginierung
Jede list_*-Methode nimmt ein Keyword-Argument cursor=, und jedes Ergebnis trägt einen next_cursor. Wenn next_cursor None ist, hast du alles.
from mcp import Client
from mcp.server import MCPServer
from mcp.types import Tool
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
@mcp.tool()
def reserve_book(title: str) -> str:
"""Put a book on hold."""
return f"Reserved {title!r}."
async def main() -> None:
async with Client(mcp) as client:
tools: list[Tool] = []
cursor: str | None = None
while True:
page = await client.list_tools(cursor=cursor)
tools.extend(page.tools)
if page.next_cursor is None:
break
cursor = page.next_cursor
print([tool.name for tool in tools])
Diese Schleife ist gegenüber jedem Server korrekt. MCPServer gibt alles auf einer Seite zurück, also ist next_cursor None und die Schleife läuft einmal – deshalb schreibt der meiste Code sie nie. Server, die wirklich paginieren, und die Regeln, denen Cursor gehorchen, stehen in Paginierung.
In Tests
Client(mcp) ohne Prozess und ohne Port ist bereits ein Test-Harness für deinen Server.
Dafür gibt es ein eigenes Konstruktor-Flag: Client(mcp, raise_exceptions=True). Es wirkt nur auf In-Memory-Verbindungen, und Testen ist die Seite, die es erklärt und das ganze Muster darum herum aufbaut.
Zusammenfassung
Client(x)verbindet sich in-memory mit einem Server-Objekt, über Streamable HTTP mit einem URL-String und über alles andere per Transport.async withist der ganze Lebenszyklus. Darin sindserver_capabilitiesundprotocol_versionbereits gefüllt;server_infoundinstructionsebenfalls, wenn der Server sie liefert.list_tools()gibt dir für jedes Toolname,title,descriptionundinput_schema.call_tool()gibtcontentfür das Modell,structured_contentfür deinen Code undis_errorzurück. Ein Tool, das eine Exception auslöst, ist ein Ergebnis, keine Exception.contentist eine Union von Blocktypen; grenze mitisinstanceein, bevor du liest.list_resources/list_resource_templates/read_resource,list_prompts/get_promptundcompleterunden die Verben ab.- Jede
list_*-Methode nimmtcursor=; iteriere, bisnext_cursorNoneist.
Was ein Server vom Client anfordern kann und wie du darauf antwortest, steht in Client-Callbacks.