Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

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.py main — opens with async 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 the audit_log result proves the wrap happened.
  • server.pyserver.middleware.append(record_calls) is the public registration point on mcp.server.lowlevel.Server.
  • client.py — the asserted log ends at "tools/call" without a :done suffix: audit_log runs inside call_next(ctx), so the finally hasn't fired yet. That's the wrap.

Caveats

  • One list, two accessors. Server.middleware on mcp.server.lowlevel.Server is the hook this story uses; MCPServer exposes the same list as MCPServer.middleware (or takes it at construction as MCPServer(name, middleware=[...])).
  • The middleware signature is provisional (see the TODO in src/mcp/server/lowlevel/server.py): it may change in a 2.x minor release, tightening to a covariant Context[L] and gaining an outbound seam.
  • ServerMiddleware / CallNext / HandlerResult are imported from mcp.server.context (helper tier); not re-exported at mcp.server.lowlevel.
  • Do not await ctx.session.send_request(...) while wrapping initializeinitialize is dispatched inline and the outbound channel isn't open yet.
  • To rewrite ctx.method / ctx.params before the handler runs, pass an adjusted context through: await call_next(dataclasses.replace(ctx, ...)). docs/migration.md shows 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).