Middleware
Tradução automática
Esta página foi traduzida automaticamente a partir da documentação em inglês, e a página em inglês é a versão de referência. Se algo parecer errado, Traduções explica como avisar.
Um middleware é uma única função async que envolve toda mensagem que o seu servidor recebe.
Você o escreve como async (ctx, call_next) e o adiciona ao fim de server.middleware. A API inteira é essa.
Warning
A lista de middlewares está marcada como provisória no código-fonte: a assinatura e a semântica podem mudar em uma versão minor 2.x. Use-a para observar (tempo, logs, tracing) e para recusar mensagens; não faça dela o alicerce sobre o qual o seu servidor se apoia.
MCPServer recebe a lista na construção (MCPServer(name, middleware=[...])) e a expõe como
mcp.middleware; o Server de baixo nível expõe a mesma lista como server.middleware. O exemplo
abaixo usa o Server de baixo nível; se Server(name, on_call_tool=...) é novidade para você, leia
O Server de baixo nível primeiro.
Um middleware de medição de tempo
Um servidor, uma ferramenta, um middleware que registra no log quanto tempo cada mensagem levou:
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)
ctxé o mesmoServerRequestContextque os seus handlers recebem.ctx.methodé a string bruta do método;ctx.paramssão os params brutos, antes de qualquer validação.call_next(ctx)executa o restante da cadeia: a validação, a busca do handler, o seu handler. Retorne o que ele retornou e a resposta fica intacta.- O
try/finallyé proposital: um handler que lança exceção ainda é cronometrado, porque a falha chega ao seu middleware como a exceção que sai decall_next. server.middleware.append(...)faz o registro. A lista executa do mais externo para o mais interno, entãomiddleware[0]é o que fica mais perto do fio.
Experimente
Conecte um cliente, liste as ferramentas, chame uma. O seu log tem três linhas:
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
Você fez duas chamadas e recebeu três linhas. A primeira é server/discover: a requisição que o
cliente enviou para estabelecer a conexão, antes de você pedir qualquer coisa.
É justamente esse o ponto. O middleware envolve toda mensagem de entrada:
- O estabelecimento da conexão:
server/discover, ouinitializeenotifications/initializedem uma sessão legada. - Toda requisição e toda notificação. Para uma notificação,
ctx.request_id is None,call_next(ctx)retornaNonee o que quer que você retorne é descartado. - Até um método para o qual o servidor não tem handler:
call_nextlança oMCPError(-32601, "Method not found")através do seu middleware a caminho do cliente.
O que você pode fazer dentro de um
Em ordem crescente do quanto você deveria hesitar:
- Observar. Cronometre, conte, registre no log. O exemplo acima.
- Recusar. Lance um
MCPErrorem vez de chamarcall_next(ctx)e essa única mensagem é respondida com um erro JSON-RPC. A conexão continua de pé; a próxima mensagem passa. É assim que um servidor controla o acesso asubscriptions/listenpor chamador: Decidindo quem pode observar, na página de Assinaturas, percorre o passo a passo. - Reescrever.
ctxé uma dataclass:await call_next(dataclasses.replace(ctx, params=...))entrega ao restante da cadeia params diferentes dos que o cliente enviou. Nunca faça isso cominitialize: o resultado que o cliente recebe de volta é construído a partir dos seus params reescritos, mas o servidor grava o estado da conexão a partir dos params originais do fio. Os dois lados podem terminar o handshake discordando sobre o que negociaram. - Responder. Retorne um resultado sem chamar
call_next(ctx)e ele vai para o cliente como a sua resposta.call_nextentrega a você a forma final do fio, e o pipeline nunca altera o que você retorna, então o envelope inteiro é seu: em uma conexão da era 2026 isso inclui o carimbo_metadeserverInfo, que o SDK adiciona aos resultados dos handlers, mas não aos seus.
Check
initialize é uma das coisas que o middleware envolve, e é o único gancho que você tem
para ele. Tente assumi-lo com add_request_handler e o SDK recusa:
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
Warning
initialize é tratado inline: o servidor não lê mais nenhuma mensagem de entrada até a sua
cadeia de middlewares retornar. Aguardar uma requisição do servidor para o cliente
(ctx.session.send_request(...), uma elicitação (elicitation)) enquanto trata initialize,
portanto, trava a conexão em deadlock: a resposta que você está esperando nunca poderá ser
lida. Notificações do tipo fire-and-forget não têm problema.
O único middleware que já vem ligado por padrão
O SDK traz exatamente um middleware, e ele já está na lista do seu servidor: o que emite um span do OpenTelemetry para cada mensagem. Você não o adiciona, e na maior parte do tempo nem pensa nele. Ele é um no-op até você instalar um exportador, e tem a própria página: OpenTelemetry.
Info
Se você já escreveu middleware ASGI, já conhece esse formato. O (scope, receive, send)
do Starlette virou (ctx, call_next), e ele executa depois do transporte, sobre a mensagem
decodificada em vez da requisição HTTP bruta. Os dois se compõem: o middleware do Starlette
em streamable_http_app() enxerga HTTP; este enxerga MCP.
Recapitulando
- Um middleware é
async (ctx, call_next) -> result, passado comoMCPServer(middleware=[...])(ou adicionado amcp.middleware) e adicionado aserver.middlewarenoServerde baixo nível. - Ele envolve toda mensagem de entrada (
server/discover,initialize, requisições, notificações, métodos desconhecidos) e executa do mais externo para o mais interno. ctx.request_id is Noneé como você distingue uma notificação de uma requisição.- Lance uma exceção em vez de chamar
call_nextpara recusar uma mensagem; a conexão sobrevive. - O tracing do OpenTelemetry do próprio SDK também é um middleware, já na lista. Veja OpenTelemetry.
- Toda essa superfície é provisória. Observe com ela; não construa em cima dela.
Isso é tudo o que envolve uma requisição. Autorização é o que decide se a requisição chega a ser executada.