Middleware
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.
Eine Middleware ist eine einzelne async-Funktion, die jede Nachricht umschließt, die dein Server empfängt.
Du schreibst sie als async (ctx, call_next) und hängst sie an server.middleware an. Das ist die ganze API.
Warning
Die Middleware-Liste ist im Quellcode als provisional markiert: Signatur und Semantik können sich in einem 2.x-Minor-Release ändern. Nutze sie zum Beobachten (Timing, Logging, Tracing) und zum Ablehnen von Nachrichten; mach sie nicht zum Fundament, auf dem dein Server steht.
MCPServer nimmt die Liste beim Erzeugen entgegen (MCPServer(name, middleware=[...])) und stellt sie als
mcp.middleware bereit; der Low-Level-Server stellt dieselbe Liste als server.middleware bereit. Das Beispiel
unten verwendet den Low-Level-Server; wenn Server(name, on_call_tool=...) neu für dich ist, lies zuerst
Der Low-Level-Server.
Eine Timing-Middleware
Ein Server, ein Tool, eine Middleware, die loggt, wie lange jede Nachricht gebraucht hat:
import logging
import time
from mcp.server import Server, ServerRequestContext
from mcp.server.context import CallNext, HandlerResult
from mcp.types import (
CallToolRequestParams,
CallToolResult,
ListToolsResult,
PaginatedRequestParams,
TextContent,
Tool,
)
logger = logging.getLogger(__name__)
async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(
tools=[
Tool(
name="search_books",
description="Search the catalog by title or author.",
input_schema={
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
)
]
)
async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
query = (params.arguments or {})["query"]
return CallToolResult(content=[TextContent(type="text", text=f"Found 3 books matching {query!r}.")])
async def log_timing(ctx: ServerRequestContext, call_next: CallNext) -> HandlerResult:
start = time.perf_counter()
try:
return await call_next(ctx)
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
logger.info("%s took %.1f ms", ctx.method, elapsed_ms)
server = Server("Bookshop", on_list_tools=on_list_tools, on_call_tool=on_call_tool)
server.middleware.append(log_timing)
ctxist derselbeServerRequestContext, den deine Handler erhalten.ctx.methodist der rohe Methoden-String;ctx.paramssind die rohen Parameter, vor jeder Validierung.call_next(ctx)führt den Rest der Kette aus: Validierung, die Suche nach dem Handler, deinen Handler. Gib zurück, was es zurückgegeben hat, und die Response bleibt unverändert.- Das
try/finallyist Absicht: Auch ein Handler, der eine Exception auslöst, wird gemessen, denn der Fehler erreicht deine Middleware als Exception auscall_next. server.middleware.append(...)registriert sie. Die Liste läuft von außen nach innen, also istmiddleware[0]die Middleware, die der Leitung am nächsten ist.
Ausprobieren
Verbinde einen Client, liste die Tools auf, rufe eines auf. Dein Log hat drei Zeilen:
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
Du hast zwei Aufrufe gemacht und drei Zeilen bekommen. Die erste ist server/discover: der Request, den der
Client geschickt hat, um die Verbindung aufzubauen, bevor du irgendetwas angefordert hast.
Genau darum geht es. Middleware umschließt jede eingehende Nachricht:
- Den Verbindungsaufbau:
server/discover, oderinitializeundnotifications/initializedauf einer Legacy-Session. - Jeden Request und jede Benachrichtigung. Bei einer Benachrichtigung gilt
ctx.request_id is None,call_next(ctx)gibtNonezurück, und was immer du zurückgibst, wird verworfen. - Sogar eine Methode, für die der Server keinen Handler hat:
call_nextwirft denMCPError(-32601, "Method not found")durch deine Middleware hindurch auf dem Weg zum Client.
Was innerhalb einer Middleware möglich ist
In aufsteigender Reihenfolge danach, wie sehr du zögern solltest:
- Beobachten. Messen, zählen, loggen. Das Beispiel oben.
- Ablehnen. Löse einen
MCPErroraus, stattcall_next(ctx)aufzurufen, und diese eine Nachricht wird mit einem JSON-RPC-Fehler beantwortet. Die Verbindung bleibt bestehen; die nächste Nachricht geht durch. So schränkt ein Serversubscriptions/listenpro Aufrufer ein: Entscheiden, wer zusehen darf auf der Seite Abonnements geht das Schritt für Schritt durch. - Umschreiben.
ctxist eine Dataclass:await call_next(dataclasses.replace(ctx, params=...))reicht dem Rest der Kette andere Parameter weiter, als der Client geschickt hat. Tu das niemals beiinitialize: Das Ergebnis, das der Client zurückbekommt, wird aus deinen umgeschriebenen Parametern gebaut, aber der Server legt seinen Verbindungszustand anhand der ursprünglichen Parameter von der Leitung fest. Beide Seiten können den Handshake abschließen und sich dabei uneinig sein, was sie ausgehandelt haben. - Beantworten. Gib ein Ergebnis zurück, ohne
call_next(ctx)aufzurufen, und es geht als deine Response an den Client.call_nextreicht dir die fertige Form für die Leitung, und die Pipeline bessert nie nach, was du zurückgibst – der ganze Umschlag gehört also dir: Auf einer Verbindung der 2026er-Generation schließt das den_meta-StempelserverInfoein, den das SDK an Handler-Ergebnisse anhängt, an deine aber nicht.
Check
initialize gehört zu den Dingen, die Middleware umschließt, und es ist der einzige Hook, den du
dafür bekommst. Versuchst du, es mit add_request_handler zu übernehmen, lehnt das SDK ab:
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
Warning
initialize wird inline verarbeitet: Der Server liest keine weiteren eingehenden Nachrichten, bis deine
Middleware-Kette zurückkehrt. Auf einen Request vom Server an den Client zu warten (ctx.session.send_request(...),
eine Elicitation (Rückfrage bei der Person am Host)), während initialize verarbeitet wird, führt daher zu
einem Deadlock der Verbindung: Die Response, auf die du wartest, kann nie gelesen werden.
Fire-and-forget-Benachrichtigungen sind in Ordnung.
Die eine Middleware, die standardmäßig aktiv ist
Das SDK liefert genau eine Middleware mit, und sie steht bereits auf der Liste deines Servers: die, die für jede Nachricht einen OpenTelemetry-Span erzeugt. Du hängst sie nicht an, und meistens denkst du gar nicht an sie. Sie tut nichts, bis du einen Exporter installierst, und sie hat ihre eigene Seite: OpenTelemetry.
Info
Wenn du schon ASGI-Middleware geschrieben hast, kennst du diese Form bereits. Starlettes
(scope, receive, send) wurde zu (ctx, call_next), und es läuft nach dem Transport, auf
der dekodierten Nachricht statt auf dem rohen HTTP-Request. Beides lässt sich kombinieren: Starlette-Middleware
auf streamable_http_app() sieht HTTP; diese hier sieht MCP.
Zusammenfassung
- Eine Middleware ist
async (ctx, call_next) -> result, übergeben alsMCPServer(middleware=[...])(oder anmcp.middlewareangehängt) und beim Low-Level-Serveranserver.middlewareangehängt. - Sie umschließt jede eingehende Nachricht (
server/discover,initialize, Requests, Benachrichtigungen, unbekannte Methoden) und läuft von außen nach innen. - An
ctx.request_id is Noneunterscheidest du eine Benachrichtigung von einem Request. - Löse eine Exception aus, statt
call_nextaufzurufen, um eine einzelne Nachricht abzulehnen; die Verbindung überlebt. - Das eigene OpenTelemetry-Tracing des SDK ist ebenfalls eine Middleware, die schon auf der Liste steht. Siehe OpenTelemetry.
- Die gesamte Oberfläche ist provisorisch. Beobachte damit; baue nicht darauf.
Das ist alles, was einen Request umschließt. Autorisierung entscheidet, ob der Request überhaupt laufen darf.