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.
Streamable HTTP
Übergib einen URL-String und du bekommst Streamable HTTP – den Transport, hinter dem du bereitstellst und zu dem du zuerst greifen solltest:
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: 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),
) 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 – außer bei der Behandlung von Redirects. Hier dockt auch OAuth an:
httpx2.AsyncClient(auth=OAuthClientProvider(...)). Der ganze Ablauf steht in OAuth-Clients.
Redirects
Der Transport verbindet sich mit der URL, die du ihm gegeben hast, und nur mit diesem Origin.
- Einem
307/308-Redirect, der auf demselben Schema, Host und Port bleibt, wird gefolgt, ebensohttp://→https://auf demselben Host. Das deckt den üblichen Trailing-Slash-Redirect/mcp→/mcp/ab. -
Einem Redirect irgendwo anders hin wird nicht gefolgt. Der Aufruf schlägt fehl mit:
MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended serverIst diese URL der Server, den du meintest, trag sie in deine Konfiguration ein. Wenn nicht, ist der Server oder ein Proxy davor falsch konfiguriert.
Das gilt für jeden httpx2.AsyncClient, den du übergibst: Seine Einstellung follow_redirects wird für MCP-Requests nicht herangezogen, in keine der beiden Richtungen. Die OAuth-Provider des SDK wenden dieselbe Regel auf ihre eigenen Requests an.
Tip
Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP bedeutet, dass der
Server hinter einem TLS-terminierenden Proxy sitzt, von dem er nichts weiß, und http://-Redirects ausgibt.
Das behebst du auf dem Server (Bereitstellen und skalieren)
oder indem du genau die https://…/-URL verwendest, die die Meldung vorschlägt.
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 und übergib das Objekt an Client:
from mcp import Client, StdioServerParameters
server = StdioServerParameters(
command="uv",
args=["run", "server.py"],
env={"BOOKSHOP_API_KEY": "secret"},
)
async def main() -> None:
async with Client(server) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Beim Eintreten in den Block wird der Prozess gestartet. Beim Verlassen wird der Subprozess beendet: stdin schließen, warten, abschießen, falls er hängen bleibt. Du räumst ihn nie selbst auf.
Die stderr des Kindprozesses landet in deiner. Um sie woandershin zu leiten, baust du den Transport selbst mit stdio_client (aus mcp) und übergibst stattdessen diesen: Client(stdio_client(server, errlog=log_file)).
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.
Im Speicher
In einem Test gibt es nichts bereitzustellen und nichts zu starten. Ü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. Testen baut das ganze Muster darauf auf.
Dieselbe Form dient zugleich als Embedding-API: Eine Anwendung, die den Server selbst erzeugt, kann dessen Tools ohne Netzwerk-Hop aufrufen.
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 str wird zu streamable_http_client(url), ein StdioServerParameters wird zu stdio_client(params), ein Server-Objekt verbindet im Prozess, 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("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=. - Redirects wird nur innerhalb des eigenen Origins der URL gefolgt (ein Trailing-Slash-
307/308), plushttp→httpsauf demselben Host. Alles andere schlägt mitRedirect to … not followedfehl; konfiguriere die endgültige URL. - stdio ist
Client(StdioServerParameters(...)). Pack es nur dann selbst instdio_client(...)ein, wenn du die stderr des Kindprozesses umleiten willst. - Der Subprozess bekommt eine Umgebung per Allow-List, nicht deine;
env=ergänzt sie. Client(mcp)(das Server-Objekt) verbindet im Speicher. Nutze es in Tests oder um einen Server in die Anwendung einzubetten, die ihn gebaut hat.- Ein Transport ist alles, womit du
async with x as (read, write)schreiben kannst. Alles, was weder Server-Objekt noch URL nochStdioServerParametersist, 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.