Middleware
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
Un middleware es una función asíncrona que envuelve cada mensaje que recibe el servidor.
Lo escribes como async (ctx, call_next) y lo añades a server.middleware. Esa es toda la API.
Warning
La lista de middleware está marcada como provisional en el código fuente: su firma y su semántica pueden cambiar en una versión menor 2.x. Úsala para observar (medir tiempos, registrar, trazar) y para rechazar mensajes; no la conviertas en los cimientos del servidor.
MCPServer recibe la lista en el constructor (MCPServer(name, middleware=[...])) y la expone como
mcp.middleware; el Server de bajo nivel expone la misma lista como server.middleware. El ejemplo
de abajo usa el Server de bajo nivel; si Server(name, on_call_tool=...) es nuevo para ti, lee
primero El Server de bajo nivel.
Un middleware que mide tiempos
Un servidor, una herramienta y un middleware que registra cuánto tardó cada mensaje:
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)
ctxes el mismoServerRequestContextque reciben tus handlers.ctx.methodes la cadena del método sin procesar;ctx.paramsson los parámetros sin procesar, antes de cualquier validación.call_next(ctx)ejecuta el resto de la cadena: la validación, la búsqueda del handler y tu handler. Devuelve lo que devolvió y la respuesta queda intacta.- El
try/finallyes deliberado: un handler que lanza una excepción también se cronometra, porque el fallo llega a tu middleware como la excepción que sale decall_next. server.middleware.append(...)lo registra. La lista se ejecuta de fuera hacia dentro, así quemiddleware[0]es el más cercano al canal.
Pruébalo
Conecta un cliente, lista las herramientas, llama a una. El log tiene tres líneas:
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
Hiciste dos llamadas y obtuviste tres líneas. La primera es server/discover: la solicitud que
envió el cliente para establecer la conexión, antes de que pidieras nada.
Ese es el punto. El middleware envuelve cada mensaje entrante:
- El establecimiento de la conexión:
server/discover, oinitializeynotifications/initializeden una sesión heredada. - Cada solicitud y cada notificación. Para una notificación,
ctx.request_id is None,call_next(ctx)devuelveNoney lo que devuelvas se descarta. - Incluso un método para el que el servidor no tiene handler:
call_nextlanza elMCPError(-32601, "Method not found")a través de tu middleware de camino al cliente.
Qué puedes hacer dentro de uno
En orden creciente de cuánto deberías dudar:
- Observar. Cronométralo, cuéntalo, regístralo. El ejemplo de arriba.
- Rechazar. Lanza un
MCPErroren lugar de llamar acall_next(ctx)y ese único mensaje se responde con un error JSON-RPC. La conexión sigue activa; el siguiente mensaje pasa. Así es como un servidor restringesubscriptions/listenpor llamante: Decidir quién puede observar en la página de Suscripciones lo recorre paso a paso. - Reescribir.
ctxes una dataclass:await call_next(dataclasses.replace(ctx, params=...))entrega al resto de la cadena unos parámetros distintos de los que envió el cliente. Nunca hagas esto coninitialize: el resultado que recibe el cliente se construye a partir de tus parámetros reescritos, pero el servidor fija el estado de la conexión a partir de los parámetros originales que llegaron por el canal. Los dos lados pueden terminar el handshake en desacuerdo sobre lo que negociaron. - Responder. Devuelve un resultado sin llamar a
call_next(ctx)y llega al cliente como tu respuesta.call_nextte entrega la forma final que se transmite, y la canalización nunca retoca lo que devuelves, así que todo el sobre es tuyo: en una conexión de la generación 2026 eso incluye la marca_metadeserverInfo, que el SDK añade a los resultados de los handlers pero no a los tuyos.
Check
initialize es una de las cosas que el middleware envuelve, y es el único punto de enganche
que tienes para ello. Intenta apropiártelo con add_request_handler y el SDK se niega:
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
Warning
initialize se maneja en línea: el servidor no lee más mensajes entrantes hasta que tu cadena
de middleware devuelve. Esperar con await una solicitud del servidor al cliente
(ctx.session.send_request(...), una elicitación) mientras se maneja initialize bloquea
la conexión por completo: la respuesta que esperas nunca se podrá leer. Las notificaciones
que se envían sin esperar respuesta no dan problemas.
El único middleware que viene activado por defecto
El SDK incluye exactamente un middleware, y ya está en la lista del servidor: el que emite un span de OpenTelemetry por cada mensaje. No lo añades y, la mayor parte del tiempo, ni piensas en él. No hace nada hasta que instalas un exportador, y tiene su propia página: OpenTelemetry.
Info
Si has escrito middleware ASGI, ya conoces esta forma. El (scope, receive, send) de
Starlette se convirtió en (ctx, call_next), y se ejecuta después del transporte, sobre el
mensaje ya decodificado en lugar de la solicitud HTTP sin procesar. Los dos se combinan: el
middleware de Starlette sobre streamable_http_app() ve HTTP; este ve MCP.
Resumen
- Un middleware es
async (ctx, call_next) -> result, se pasa comoMCPServer(middleware=[...])(o se añade amcp.middleware), y se añade aserver.middlewareen elServerde bajo nivel. - Envuelve cada mensaje entrante (
server/discover,initialize, solicitudes, notificaciones, métodos desconocidos) y se ejecuta de fuera hacia dentro. ctx.request_id is Nonees la forma de distinguir una notificación de una solicitud.- Lanza una excepción en lugar de llamar a
call_nextpara rechazar un mensaje; la conexión sobrevive. - El trazado con OpenTelemetry del propio SDK también es un middleware, ya incluido en la lista. Consulta OpenTelemetry.
- Toda la superficie es provisional. Observa con ella; no construyas sobre ella.
Eso es todo lo que envuelve una solicitud. Autorización es lo que decide si la solicitud llega a ejecutarse siquiera.