Zum Inhalt

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:

client.py
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:

client.py
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:

client.py
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.AsyncClient gehö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, und Client(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:

client.py
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 an streamable_http_client(url, http_client=...) übergibst. Es gibt kein Keyword headers=.
  • 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, reicht Client direkt an dieses Protokoll weiter.
  • Das Erzeugen eines Client wä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.