Commit Graph

875 Commits

Author SHA1 Message Date
Max Isbey 918e20aba0 test: converge span capture on capfire to fix xdist order-dependence
The previous tests/server/conftest.py called trace.set_tracer_provider()
directly, which is set-once per process and raced against logfire's capfire
fixture (tests/shared/test_otel.py) under xdist — whichever ran first in a
worker won, the other's tests broke.

Converge on capfire as the single span-capture owner since logfire.configure()
already handles repeat calls by swapping span processors instead of re-setting
the provider:

- tests/conftest.py: set LOGFIRE_DISTRIBUTED_TRACING=true so propagation tests
  don't trip logfire's 'found propagated trace context' RuntimeWarning.
- tests/server/conftest.py: SpanCapture adapter over capfire.exporter — filters
  to the mcp-python-sdk instrumentation scope and excludes logfire's
  pending_span markers, so tests assert on raw ReadableSpan without importing
  logfire types.
- tests/shared/test_otel.py: drop the now-unneeded filterwarnings decorator.
2026-04-25 22:14:12 +00:00
Max Isbey a8b669ca17 test: ServerRunner coverage to 100% — otel span assertions + connected_runner harness
- Add opentelemetry-sdk as a dev dep and a tests/server/conftest.py 'spans'
  fixture (TracerProvider + InMemorySpanExporter) so otel_middleware's span
  contract is observable.
- Replace the otel pass-through test with four span-asserting tests (name +
  target, _meta traceparent → parent, MCPError → ERROR status without
  traceback, unexpected exception → ERROR status + exception event). These
  surfaced that start_as_current_span's default set_status_on_exception /
  record_exception was overwriting the middleware's explicit set_status and
  attaching tracebacks to protocol-level MCPErrors — now disabled and handled
  explicitly.
- Add handler-return contract tests (None → {}, unsupported → INTERNAL_ERROR).
- Introduce connected_runner async-contextmanager test harness and retrofit all
  tests through runner.run(); drop two tests made redundant by that. Harness
  closes dispatchers gracefully and re-raises body exceptions outside the task
  group so failures aren't ExceptionGroup-wrapped (and to avoid a coverage.py
  trace-loss false-negative on cancel-during-aexit).
- Remove the unused Server.connection_lifespan placeholder; it lands with its
  consumer.
2026-04-25 21:40:05 +00:00
Max Isbey 4c5cd39aae feat: ServerRunner.run() and otel_middleware
run() composes dispatch_middleware over _on_request and forwards task_status
to dispatcher.run() so callers can 'await tg.start(runner.run)'.

otel_middleware is a DispatchMiddleware that wraps each request in a span,
mirroring the existing Server._handle_request span shape: name 'MCP handle
<method> [<target>]', mcp.method.name attribute, W3C trace context extracted
from params._meta (SEP-414), and ERROR status if the handler raises.

connection_lifespan plumbing (the enter-late dance) is deferred to a separate
commit since Server.connection_lifespan is None today.
2026-04-22 01:28:51 +00:00
Max Isbey 7dc1527e0e feat: ServerRunner middleware (two-tier) + _on_notify
ContextMiddleware is a Protocol[L] (contravariant) so Server[L].middleware:
list[ContextMiddleware[L]] is properly typed. App-specific middleware sees
ctx.lifespan: L; reusable middleware typed ContextMiddleware[object] registers
on any Server via contravariance. Context's covariance (previous PR3 commit)
makes Context[L, ST] <: Context[L, TransportContext] so the chain composes
without casts.

dispatch_middleware (DispatchMiddleware list on ServerRunner) wraps the raw
_on_request and sees everything including initialize/METHOD_NOT_FOUND.
server.middleware (ContextMiddleware) runs inside _on_request after
validation/ctx-build and wraps registered handlers only.

_on_notify routes notifications/initialized (sets the flag), drops
before-init and unknown methods, otherwise builds Context and calls the
registered handler.

11 tests over DirectDispatcher + a real lowlevel Server.
2026-04-20 19:02:57 +00:00
Max Isbey 52d049470a feat: ServerRunner skeleton — _on_request, initialize, init-gate
ServerRunner is the per-connection orchestrator over a Dispatcher. This commit
lands the skeleton: ServerRegistry Protocol, _on_request (lookup → validate →
build Context → call handler → dump), _handle_initialize (populates
Connection, opens the init-gate), and a basic _on_notify.

Additive methods on lowlevel Server (get_request_handler /
get_notification_handler / middleware / connection_lifespan) so it satisfies
ServerRegistry without touching the existing run() path. _PARAMS_FOR_METHOD is
scaffolding (marked TODO) until the registry stores params types directly.

5 tests over DirectDispatcher + a real lowlevel Server.
2026-04-20 19:01:26 +00:00
Max Isbey 7ed57dcf92 refactor: make BaseContext/Context covariant in their type params
LifespanT and TransportT are only exposed via read-only properties (lifespan,
transport), so covariance is sound. This lets a Context[AppState, HttpTC] be
passed where a Context[object, TransportContext] is expected — needed for
ServerRunner's middleware chain to compose without casts, and for reusable
middleware to be typed Context[object, TransportContext] instead of relying
on Any-slack.
2026-04-20 19:01:09 +00:00
Max Isbey 05a031acd5 docs: drop development-journal language from docstrings/comments
Remove references to PR numbers, internal scratch notes, and design-spike
shorthand that won't make sense to a fresh reader of the codebase.
2026-04-20 17:04:07 +00:00
Max Isbey 6f80f8d150 test: move asserts inside async-with for 3.11 coverage instrumentation
coverage.py on Python 3.11 doesn't record statements after an
'async with running_pair(...)' exit when there's a nested
'with anyio.fail_after()' inside. Same workaround as 0a8f0f4 in PR2 —
move the asserts inside the async-with block.
2026-04-20 16:58:42 +00:00
Max Isbey a3b896f214 test: close PR3 coverage gaps to 100%
- Connection.check_capability per-field branches (parametrized)
- Context.log with logger and meta supplied
- Peer.notify forwards to wrapped Outbound
2026-04-16 22:44:41 +00:00
Max Isbey 6897c9ad99 feat: Connection, server Context, typed send_request, meta kwarg
TypedServerRequestMixin (server/_typed_request.py) provides shape-2 typed
send_request: per-spec overloads (CreateMessage/Elicit/ListRoots/Ping) infer
the result type; custom requests pass result_type explicitly. Mixed into both
Connection and the server Context.

Connection (server/connection.py) wraps an Outbound for the standalone stream.
notify is best-effort (never raises); send_raw_request gated on
has_standalone_channel; check_capability mirrors v1 for now (FOLLOWUP). Holds
peer info populated at initialize time and the per-connection lifespan state.

Context (server/context.py, alongside v1's ServerRequestContext) composes
BaseContext + PeerMixin + TypedServerRequestMixin and adds lifespan/connection.
Request-scoped log() rides the request's back-channel; ctx.connection.log()
uses the standalone stream.

dump_params(model, meta) merges user-supplied meta into _meta; threaded
through every PeerMixin and Connection convenience method.

31 tests, 0.06s.
2026-04-16 22:21:02 +00:00
Max Isbey 81d58bdd5b refactor: follow Outbound.send_raw_request rename in PeerMixin/BaseContext
PeerMixin methods and Peer/BaseContext now call/expose send_raw_request.
The typed send_request lands on Connection/Context in the next commit.
2026-04-16 22:02:36 +00:00
Max Isbey de81288d34 feat: BaseContext
Composition over a DispatchContext: forwards transport/cancel_requested/
send_request/notify/progress and adds meta. Satisfies Outbound so PeerMixin
works on it (proven by Peer(bctx).ping() round-tripping).

The server Context (next commit) extends this with lifespan/connection;
ClientContext will be an alias once ClientSession is reworked.
2026-04-16 21:53:11 +00:00
Max Isbey 870cb088c7 feat: PeerMixin and Peer wrapper
PeerMixin defines the typed server-to-client request methods (sample with
overloads, elicit_form, elicit_url, list_roots, ping) once. Each method
constrains `self: Outbound` so any class with send_request/notify can mix it
in — pyright checks the host structurally at the call site. The mixin does no
capability gating; that's the host's send_request's job.

Peer is a trivial standalone wrapper for when you have a bare Outbound (e.g.
a dispatcher) and want the typed sugar without writing your own host class.

6 tests over DirectDispatcher, 0.03s.
2026-04-16 21:53:11 +00:00
Max Isbey eb74b7c8c7 refactor: rename send_request to send_raw_request in JSONRPCDispatcher
Follows the Outbound Protocol rename in the previous commit. Mechanical rename
across JSONRPCDispatcher, _JSONRPCDispatchContext, and tests.
2026-04-16 21:49:25 +00:00
Max Isbey 0a8f0f4607 test: address 3.11/3.14 coverage instrumentation quirks
3.14: nested async-with arc misreporting on three create_task_group lines
(the documented AGENTS.md case) — pragma: no branch.

3.11: lines after async-CM exit with pytest.raises mis-traced in one test —
moved the asserts inside the context manager.
2026-04-16 21:43:14 +00:00
Max Isbey cffb527956 ci: run full matrix on PRs targeting any branch
The pull_request branch filter meant the test/lint/coverage matrix only ran
on PRs targeting main or v1.x. Stacked PRs (targeting feature branches) only
got the conformance checks, which are continue-on-error and don't exercise
unit tests. Removing the filter so the full matrix runs on every PR.
2026-04-16 21:43:14 +00:00
Max Isbey 53257cafff test: JSON-RPC-specific dispatcher tests + coverage to 100%
Covers behaviors with no DirectDispatcher analog: out-of-order response
correlation, INTERNAL_ERROR over the wire, peer-cancel in interrupt and signal
modes, CONNECTION_CLOSED on stream EOF mid-await, late-response drop,
raise_handler_exceptions propagation, ServerMessageMetadata tagging on
ctx.send_request, null-id JSONRPCError drop, ValidationError->INVALID_PARAMS,
contextvar propagation via _spawn, and the defensive Broken/Closed/WouldBlock
catches.

Two small src tweaks for coverage:
- _cancel_outbound: combine the two except arms into one tuple
- _dispatch: pragma no-branch on the final case (match is exhaustive over
  JSONRPCMessage; the no-match arc is unreachable)

43 tests, 100% coverage on all PR2 modules, 0.15s wall-clock.
2026-04-16 21:43:14 +00:00
Max Isbey 200a6070db feat: JSONRPCDispatcher exception boundary (chunk c)
_handle_request is now the single exception-to-wire boundary:
- MCPError -> JSONRPCError(e.error)
- pydantic ValidationError -> INVALID_PARAMS
- Exception -> INTERNAL_ERROR(str(e)), logged, optionally re-raised
- outer-cancel (run() TG shutdown) -> shielded REQUEST_CANCELLED write, re-raise
- peer-cancel (notifications/cancelled) -> scope swallows, no response written

dctx.close() runs in an inner finally so the back-channel shuts the moment the
handler exits. _write_result/_write_error swallow Broken/ClosedResourceError so
a dropped connection during the response write doesn't crash the dispatcher.

All 22 contract tests now pass against both DirectDispatcher and
JSONRPCDispatcher; chunk-c xfail markers removed.
2026-04-16 21:43:14 +00:00
Max Isbey 136a22d9b7 feat: JSONRPCDispatcher receive loop and dispatch (chunk b)
run() drives the receive loop in a per-request task group;
task_status.started() fires once send_request is usable. _dispatch routes each
inbound message synchronously (no awaits — send_nowait/_spawn only) to avoid
head-of-line blocking. _spawn propagates the sender's contextvars via
Context.run(tg.start_soon, ...) so auth/OTel set by ASGI middleware survive.
_fan_out_closed wakes pending send_request waiters with CONNECTION_CLOSED on
shutdown (called both post-EOF and in finally; idempotent).

Wire-param extraction (progressToken, cancelled.requestId, progress fields)
uses structural match patterns — runtime narrowing, no casts, no mcp.types
model coupling; malformed input fails to match and the correlation is skipped.

_handle_request is happy-path only here (run on_request, write response); the
exception-to-wire boundary lands in the next commit.

Dispatcher.run() Protocol gained a task_status kwarg (it's a contract-level
guarantee). DirectDispatcher.run() updated to match. running_pair now uses
tg.start so the test body runs only once the dispatcher is ready.

20 contract tests pass; the 2 needing the exception boundary are strict-xfail.
2026-04-16 21:43:14 +00:00
Max Isbey d84f82aa9c feat: JSONRPCDispatcher outbound side + parametrized contract tests
Chunk (a) of JSONRPCDispatcher: constructor, _Pending/_InFlight/_JSONRPCDispatchContext,
send_request/notify and helpers. run() is stubbed.

The Dispatcher contract tests are now parametrized over a pair_factory fixture
(direct + jsonrpc). The 9 jsonrpc cases are strict-xfail until run()/
_handle_request land in the next commits; once those pass, strict xfail flips
to XPASS and forces removal of the marker.

Factories return (client, server, close) so running_pair can shut down any
implementation uniformly.
2026-04-16 21:41:31 +00:00
Max Isbey bfb5a77127 refactor: rename Outbound.send_request to send_raw_request
The dispatcher-layer raw channel is now `send_raw_request(method, params) ->
dict`. This frees the `send_request` name for the typed surface
(`send_request(req: Request) -> Result`) that Connection/Context/Client add
in later PRs.

Mechanical rename across Outbound, Dispatcher, DispatchContext,
DirectDispatcher, _DirectDispatchContext, and all tests. `can_send_request`
(the transport capability flag) is unchanged — it names the capability, not
the method.
2026-04-16 21:38:32 +00:00
Max Isbey 1da25ec182 refactor: rename Dispatcher.call to send_request, replace RequestSender with Outbound
The design doc's `send_request = call` alias only makes the concrete class
satisfy RequestSender, not the abstract Dispatcher Protocol — so any consumer
typed against `Dispatcher[TT]` (Connection, ServerRunner) couldn't pass it to
something expecting a RequestSender without a cast or hand-written bridge.

RequestSender was also half a contract: every implementor (Dispatcher,
DispatchContext, Connection, Context) has `notify` too, and PeerMixin needs
both for its typed sugar (elicit/sample are requests, log is a notification).

Outbound(Protocol) declares both methods; Dispatcher and DispatchContext extend
it. PeerMixin will wrap an Outbound. One verb everywhere, no aliases, no extra
Protocols.

- Dispatcher.call -> send_request
- OnCall -> OnRequest, on_call -> on_request
- RequestSender -> Outbound (now also declares notify)
- Dispatcher(Outbound, Protocol[TT]), DispatchContext(Outbound, Protocol[TT])
2026-04-16 12:52:58 +00:00
Max Isbey 5540d807be fix: address coverage gaps and stale RequestSender docstring
- tests: replace unreachable 'return {}' with 'raise NotImplementedError'
  (already in coverage exclude_also) and collapse send_request+return into
  one statement
- dispatcher: RequestSender docstring no longer claims Dispatcher satisfies it
  (Dispatcher exposes call(), not send_request())
2026-04-16 12:03:48 +00:00
Max Isbey f332c596aa feat: add Dispatcher Protocol and DirectDispatcher
Introduces the Dispatcher abstraction that decouples MCP request/response
handling from JSON-RPC framing. A Dispatcher exposes call/notify for outbound
messages and run(on_call, on_notify) for inbound dispatch, with no knowledge
of MCP types or wire encoding.

- shared/dispatcher.py: Dispatcher, DispatchContext, RequestSender Protocols;
  CallOptions, OnCall/OnNotify, ProgressFnT, DispatchMiddleware
- shared/transport_context.py: TransportContext base dataclass
- shared/direct_dispatcher.py: in-memory Dispatcher impl that wires two peers
  with no transport; serves as a fast test substrate and second-impl proof
- shared/exceptions.py: NoBackChannelError(MCPError) for transports without a
  server-to-client request channel
- types: REQUEST_CANCELLED SDK error code

The JSON-RPC implementation and ServerRunner that consume this Protocol land
in follow-up PRs.
2026-04-16 11:56:49 +00:00
Gyeongjun Paik (Kent) 3d7b311de0 fix: align Context logging methods with MCP spec data type (#2366)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2026-04-14 21:41:51 +00:00
Wils Dawson 437d15aa71 SEP-2207: Refresh token guidance (#2039) 2026-04-14 11:48:07 +01:00
Max 5cbd259c3b fix: catch PydanticUserError when generating output schema (pydantic 2.13 compat) (#2434) 2026-04-13 17:03:40 +01:00
Felix Weinberger 2dfb51a4d1 fix(auth): coerce empty-string optional URL fields to None in OAuthClientMetadata (#2404) 2026-04-13 15:42:35 +01:00
Max 941089e06a docs: modernize development guidelines and rename to AGENTS.md (#2413) 2026-04-13 12:50:04 +01:00
dependabot[bot] 8f806da611 chore(deps): bump cryptography from 46.0.5 to 46.0.7 in the uv group across 1 directory (#2406)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-12 12:48:13 +00:00
Matt LeMay c5f12ec1e9 Add resources parameter to MCPServer (#2414)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2026-04-12 14:42:12 +02:00
Max Isbey f27d2aac05 docs: fill migration guide gaps surfaced by automated upgrade eval (#2412) 2026-04-09 13:25:16 +01:00
Marcelo Trylesinski cf4e435db0 Use shared is_async_callable instead of inspect.iscoroutinefunction (#2389) 2026-04-08 13:12:20 +01:00
dependabot[bot] d5b9155f14 chore(deps): bump requests from 2.32.5 to 2.33.0 in the uv group across 1 directory (#2350)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 17:11:12 -04:00
Marcelo Trylesinski 37891f42a4 Add basic OpenTelemetry tracing for client and server requests (#2381) 2026-03-31 20:33:33 +00:00
Max Isbey 3ce0f76e6e Don't block the event loop on sync resource and prompt functions (#2380) 2026-03-31 13:43:56 -04:00
Marcelo Trylesinski e6235d1667 Propagate contextvars.Context through anyio streams without modifying SessionMessage (#2298) 2026-03-31 12:49:38 -04:00
Max Isbey fb2276b95f ci: remove claude-code-review workflow (#2359) 2026-03-27 14:02:41 +00:00
Max Isbey 3517a29c82 feat(server): restore dependencies parameter on MCPServer (#2358) 2026-03-27 13:42:15 +00:00
Marcelo Trylesinski 98f8ef295a Restrict httpx version to <1.0.0 (#2345)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2026-03-25 22:29:13 +00:00
Felix Weinberger 7ba4fb881d ci: skip claude.yml when comment is '@claude review' (#2337) 2026-03-24 20:36:36 +00:00
Max Isbey 92c693bb73 fix: cancel in-flight handlers when transport closes in server.run() (#2306) 2026-03-20 13:37:32 +00:00
Max Isbey 883d893097 test: rewrite cli.claude config tests to assert JSON output directly (#2311)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Felix Weinberger <felixweinberger@users.noreply.github.com>
2026-03-19 15:16:34 +00:00
Jonathan Hefner 5388bea53a docs: generate hierarchical per-module API reference pages (#2103) 2026-03-18 18:15:17 +00:00
Max Isbey 20dd94632e feat(client): store InitializeResult as initialize_result (#2300) 2026-03-18 17:31:26 +00:00
Max Isbey 67201a9bbd test: fix WS test port race; narrow to single smoke test covering both transport ends (#2267) 2026-03-18 15:48:30 +00:00
Max Isbey 7826ade12b test: convert test_integration.py to in-memory transport (fix flaky) (#2277) 2026-03-18 15:25:11 +00:00
Max Isbey ff50351f9e ci: run strict-no-cover in scripts/test to catch stale pragmas locally (#2305) 2026-03-17 19:53:39 +00:00
Max Isbey 1a2244f402 fix: handle non-UTF-8 bytes in stdio server stdin (#2302) 2026-03-17 18:40:39 +00:00
Max Isbey 75a80b6f07 refactor: connect-first stream lifecycle for sse and streamable_http (#2292)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2026-03-16 23:30:20 +00:00