49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
2.4 KiB
2.4 KiB
middleware
Register a single async (ctx, call_next) -> result function on
Server.middleware to observe or alter every request and notification the
server receives, across both protocol eras and any transport. Middleware sits
outside method lookup and params validation, so it sees initialize,
server/discover, notifications/*, and unknown methods too. The chain runs
outermost-first.
Run it
# stdio (default — the client spawns the server as a subprocess)
uv run python -m stories.middleware.client
# HTTP — the client self-hosts the server on a free port, runs, then tears it down
uv run python -m stories.middleware.client --http
What to look at
client.pymain— opens withasync with Client(target, mode=mode). The story owns that construction; the harness only picks the target and era. Middleware is invisible from this side — only theaudit_logresult proves the wrap happened.server.py—server.middleware.append(record_calls)is the public registration point onmcp.server.lowlevel.Server.client.py— the asserted log ends at"tools/call"without a:donesuffix:audit_logruns insidecall_next(ctx), so thefinallyhasn't fired yet. That's the wrap.
Caveats
- Lowlevel-only.
Server.middlewareonmcp.server.lowlevel.Serveris the one public hook;MCPServerhas no public accessor for it yet (aMCPServer.middlewareaccessor is planned before beta). - The middleware signature is provisional (see the TODO in
src/mcp/server/lowlevel/server.py): it tightens to a covariantContext[L]and gains an outbound seam before v2 final. ServerMiddleware/CallNext/HandlerResultare imported frommcp.server.context(helper tier); not re-exported atmcp.server.lowlevel.- Do not
await ctx.session.send_request(...)while wrappinginitialize—initializeis dispatched inline and the outbound channel isn't open yet. - To rewrite
ctx.method/ctx.paramsbefore the handler runs, pass an adjusted context through:await call_next(dataclasses.replace(ctx, ...)).docs/migration.mdshows the full recipe.
Spec
Middleware is SDK architecture, not an MCP spec feature.
See also
custom_methods/ (a vendor acme/search handler registered with
add_request_handler — middleware wraps it like any spec method),
src/mcp/server/_otel.py (OpenTelemetryMiddleware, the SDK's own consumer).