Client-Transporte
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.
Jeder Client spricht mit seinem Server über einen Transport: das, was die Nachrichten tatsächlich befördert.
Du konfigurierst nie einen separat. Client nimmt ein einziges positionales Argument und leitet den Transport aus dessen Typ ab.
Die Server-Seite jedes Transports (was mcp.run() tut und was du bereitstellst) steht in Den Server betreiben.
Im Speicher
Übergib das Server-Objekt selbst:
from mcp import Client
from mcp.server import MCPServer
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}."
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool("search_books", {"query": "dune"})
print(result.structured_content)
Kein Subprozess, kein Port, keine Bytes auf einer Leitung. Client und Server sind zwei Objekte im selben Prozess, und der Aufruf läuft trotzdem durch die echte Protokollschicht: search_books wird genau so aufgelistet, validiert und aufgerufen, wie es über HTTP geschähe.
Damit ist es zwei Dinge zugleich:
- Eine Testumgebung. Jedes Beispiel in dieser Dokumentation wird so ausgeführt, und die Seite Testen baut das ganze Muster darauf auf.
- Eine Embedding-API. Eine Anwendung, die den Server selbst erzeugt, braucht keinen Netzwerk-Hop, um dessen Tools aufzurufen.
Streamable HTTP
Übergib einen URL-String und du bekommst Streamable HTTP, den Transport, hinter dem du bereitstellst:
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Das ist der ganze Produktions-Client. Client packt die URL für dich in streamable_http_client(...), auf Basis eines httpx2.AsyncClient, der so konfiguriert ist, wie MCP es braucht: follow_redirects=True, ein Timeout von 30 Sekunden für connect/write/pool und ein Read-Timeout von 300 Sekunden, weil der Server einen Response-Stream offen halten kann.
Check
Ein Client, den du erzeugt hast, ist nicht verbunden. Das Erzeugen wählt nur den Transport;
erst async with öffnet ihn. Greifst du vor dem Eintreten auf die Verbindung zu, sagt dir das SDK das:
RuntimeError: Client must be used within an async context manager
Nichts wurde aufgelöst, abgerufen oder gestartet, als du Client("http://...") geschrieben hast. Diese Zeile kostet nichts.
Einen eigenen httpx2.AsyncClient mitbringen
Sobald du einen Authorization-Header, ein Cookie, einen Proxy, mTLS oder ein anderes Timeout brauchst, baust du den httpx2.AsyncClient selbst und übergibst ihn an streamable_http_client:
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
async def main() -> None:
async with httpx2.AsyncClient(
headers={"Authorization": "Bearer ..."},
timeout=httpx2.Timeout(30.0, read=300.0),
follow_redirects=True,
) as http_client:
transport = streamable_http_client("http://localhost:8000/mcp", http_client=http_client)
async with Client(transport) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Zwei Dinge fallen auf:
- Der
httpx2.AsyncClientgehört dir, also betrittst und verlässt du ihn. Das SDK schließt nie einen Client, den es nicht selbst erzeugt hat. streamable_http_client(url, http_client=...)gibt einen Transport zurück, undClient(transport)nimmt ihn an wie alles andere auch.
Eine Anmerkung zu TLS: httpx2 prüft Zertifikate gegen den Trust Store des Betriebssystems (über
truststore), nicht gegen eine mitgelieferte CA-Liste. In einer Umgebung ohne
nutzbaren System-CA-Store (manche minimalen Container) setzt du die Standard-Umgebungsvariablen SSL_CERT_FILE/SSL_CERT_DIR
oder übergibst deinem httpx2.AsyncClient ein explizites verify=ssl_context
(Hintergrund in
httpx und httpx-sse durch httpx2 ersetzt).
Warning
streamable_http_client nahm früher headers= und timeout= direkt entgegen. Das tut er nicht mehr:
seine einzigen Parameter sind url, http_client und terminate_on_close. Greifst du aus
Gewohnheit zu headers=, bekommst du:
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
Alles, was mit HTTP zu tun hat, lebt jetzt auf dem einen httpx2.AsyncClient, den du übergibst.
Info
httpx2 behält die vertraute httpx-API bei. Wenn du httpx kennst, weißt du hier also bereits, wie Auth,
Proxys, Event-Hooks, Retries und Verbindungslimits gehen. Das SDK fügt nichts hinzu und nimmt
nichts weg. Hier dockt auch OAuth an:
httpx2.AsyncClient(auth=OAuthClientProvider(...)). Der ganze Ablauf steht in OAuth-Clients.
stdio
Ein stdio-Server ist ein Subprozess. Der Client startet ihn, schreibt JSON-RPC in seine stdin und liest JSON-RPC aus seiner stdout. So betreibt ein Desktop-Host einen Server auf deinem Rechner: Ein Host ist dieser Code plus eine UI, und Mit einem echten Host verbinden zeigt dieselbe Beziehung von der Seite des Hosts, als Konfigurationsdatei.
Beschreibe den Prozess mit StdioServerParameters, mach daraus mit stdio_client einen Transport und übergib den an Client:
from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client
server = StdioServerParameters(
command="uv",
args=["run", "server.py"],
env={"BOOKSHOP_API_KEY": "secret"},
)
async def main() -> None:
async with Client(stdio_client(server)) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Client akzeptiert das Parameter-Objekt allein nicht. StdioServerParameters ist Konfiguration; stdio_client(server) ist der Transport, der weiß, wie er daraus einen Prozess startet. Immer einpacken.
Beim Verlassen des async with-Blocks wird auch der Subprozess beendet: stdin schließen, warten, abschießen, falls er hängen bleibt. Du räumst ihn nie selbst auf.
Warning
Der Kindprozess erbt nicht deine Umgebung. Er bekommt eine minimale Allow-List (HOME, LOGNAME,
PATH, SHELL, TERM und USER auf POSIX), damit nichts Sensibles in einen Prozess durchsickert, den du
vielleicht nicht selbst geschrieben hast.
Ein Server, der einen API-Key braucht, findet ihn dort nicht. Übergib ihn explizit mit env=; diese
Variablen werden über die Allow-List gelegt. Genau das tut BOOKSHOP_API_KEY oben.
SSE
sse_client(url) aus mcp.client.sse ist der HTTP-Transport, den Streamable HTTP abgelöst hat. Pack ihn genauso ein, Client(sse_client("http://localhost:8000/sse")), um mit einem Server zu sprechen, der ihn noch verwendet – und bau nichts Neues darauf.
Das Transport-Protokoll
Für Client ist alles oben Genannte dasselbe.
Ein Transport ist ein beliebiger asynchroner Kontextmanager, der ein (read, write)-Paar von Nachrichten-Streams liefert: formal das Transport-Protokoll in mcp.client. Client löst sein Argument nach Typ auf: Ein Server-Objekt verbindet im Prozess, ein str wird zu streamable_http_client(url), und alles andere wird direkt als Transport betreten. Diese letzte Regel ist der Grund, warum stdio_client(...), streamable_http_client(...) und sse_client(...) alle in denselben Platz passen – und warum du deinen eigenen schreiben kannst.
Zusammenfassung
Client(mcp)(das Server-Objekt) verbindet im Speicher. Nutze es für Tests und zum Einbetten.Client("http://.../mcp")(eine URL) verbindet über Streamable HTTP, den Produktions-Transport.- Header, Auth, Proxys und Timeouts gehören auf einen
httpx2.AsyncClient, den du anstreamable_http_client(url, http_client=...)übergibst. Es gibt kein Keywordheaders=. - stdio ist
Client(stdio_client(StdioServerParameters(...))), nie das Parameter-Objekt allein. - Der Subprozess bekommt eine Umgebung per Allow-List, nicht deine;
env=ergänzt sie. - Ein Transport ist alles, womit du
async with x as (read, write)schreiben kannst. Alles, was weder Server-Objekt noch URL ist, reichtClientdirekt an dieses Protokoll weiter. - Das Erzeugen eines
Clientwählt den Transport.async withöffnet ihn.
Sobald der Transport offen ist, müssen sich beide Seiten auf eine Protokollversion einigen. Normalerweise denkst du nie darüber nach; wenn doch, ist Protokollversionen die richtige Seite.