Skip to content

Slim ServerMiddleware to (ctx, call_next) and add OpenTelemetryMiddleware#2941

Merged
maxisbey merged 3 commits into
mainfrom
server-middleware-ctx-call-next
Jun 22, 2026
Merged

Slim ServerMiddleware to (ctx, call_next) and add OpenTelemetryMiddleware#2941
maxisbey merged 3 commits into
mainfrom
server-middleware-ctx-call-next

Conversation

@Kludex

@Kludex Kludex commented Jun 21, 2026

Copy link
Copy Markdown
Member

What

Reshapes the context-tier ServerMiddleware interface and adds a context-tier OpenTelemetryMiddleware.

  • ServerMiddleware.__call__ goes from (ctx, method, params, call_next) to (ctx, call_next). method and params now live on ServerRequestContext as ctx.method and raw ctx.params.
  • CallNext takes the context: Callable[[ServerRequestContext], Awaitable[HandlerResult]]. Middleware passes it through with call_next(ctx), and can rewrite the inbound message via call_next(replace(ctx, params=...)).
  • New OpenTelemetryMiddleware (context-tier) in mcp/server/_otel.py, mirroring the span shape of the existing dispatch-tier otel_middleware, which is left intact. It spans both requests and notifications, setting jsonrpc.request.id only when present.

Why

The 4-arg signature duplicated method/params that already belong on the context, and call_next() could only observe, not rewrite. The new shape matches the pure-ASGI middleware model (single context in, pass it through, mutate before next) and lets middleware redirect params before the handler runs.

Notes

  • Raw ctx.params is deliberate: middleware runs before method lookup and validation (it wraps the failure path too), so no single validated type exists at that tier. Authors validate on demand with Model.model_validate(ctx.params or {}).
  • Documented in docs/migration.md (no compat shim - the 4-arg form never shipped in a release).
  • Reviewed with Codex; the notification request_id is None path was confirmed correct.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

Comment thread tests/server/test_otel.py

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize I created those tests. I don't think they are necessary. We already have tests for otel.

Comment thread src/mcp/server/runner.py Outdated
Comment thread src/mcp/server/runner.py Outdated
Comment thread src/mcp/server/runner.py
Comment thread src/mcp/server/_otel.py
Kludex and others added 2 commits June 22, 2026 11:12
…dleware`

Move `method` and `params` onto `ServerRequestContext` so context-tier
middleware reads `ctx.method`/`ctx.params` instead of separate positional
args. `CallNext` now takes the context, so middleware can rewrite the
inbound message with `call_next(replace(ctx, params=...))`.

Add a context-tier `OpenTelemetryMiddleware` alongside the existing
dispatch-tier `otel_middleware`, which is left intact.
- OpenTelemetryMiddleware: nest under the ambient span when no traceparent
  is present. Passing an explicit empty Context to start_as_current_span
  orphans the span; extract_trace_context now returns None for an absent
  or malformed carrier so callers fall through to ambient parenting.
  Adds a regression test with both span tiers installed.
- Rename 'mw' -> 'middleware' across runner.py; drop redundant CallNext
  annotation on the compose-loop accumulator.
- Document that 'initialize' is observed by ServerMiddleware but not
  rewritable: the post-chain handshake commit reads the wire params, so
  a rewritten ctx.params on initialize does not reach connection state.
  TODO at the commit site names the desync triggers; resolves when
  initialize becomes a built-in handler.
Comment thread src/mcp/shared/_otel.py
@maxisbey maxisbey force-pushed the server-middleware-ctx-call-next branch from 3dc073d to ddd79f0 Compare June 22, 2026 12:28
A non-empty _meta without a traceparent key (e.g. only a progressToken)
made extract() return an empty Context, which orphans the span when
passed explicitly to start_as_current_span. Check the extracted context
carries a valid span and return None otherwise so callers fall through
to ambient parenting.
@maxisbey maxisbey enabled auto-merge (squash) June 22, 2026 13:45
@maxisbey maxisbey merged commit ad81ca2 into main Jun 22, 2026
31 checks passed
@maxisbey maxisbey deleted the server-middleware-ctx-call-next branch June 22, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants