Commit Graph

17 Commits

Author SHA1 Message Date
Max Isbey c54fe3b36a feat(mrtr): linear continuation-based handler — Option H
The Option B footgun was: await elicit() looks like a suspension point but
is actually a re-entry point, so everything above it runs twice. Option H
fixes that by making it a REAL suspension point — the coroutine frame is
held in a ContinuationStore across MRTR rounds, keyed by request_state.

Handler code stays exactly as it was in the SSE era:

    async def my_tool(ctx: LinearCtx, location: str) -> str:
        audit_log(location)      # runs exactly once
        units = await ctx.elicit("Which units?", UnitsSchema)
        return f"{location}: 22°{units.u}"

The wrapper linear_mrtr(my_tool, store=...) translates this into a standard
MRTR on_call_tool handler. Round 1 starts the coroutine; elicit() sends
IncompleteResult back through the wrapper and parks on a stream. Round 2's
retry wakes it with the answer. The coroutine continues from where it
stopped — no re-entry, no double-execution.

Trade-off: server holds the frame in memory between rounds. Client sees
pure MRTR (no SSE, independent requests), but server is stateful within
a single tool call. Horizontally-scaled deployments need sticky routing on
the request_state token. Same operational shape as Option A's SSE hold,
without the long-lived connection.

SDK pieces (src/mcp/server/experimental/mrtr/linear.py):
- LinearCtx with async elicit(message, PydanticSchema) -> instance
- ContinuationStore — owns the task group, TTL-based frame expiry
- linear_mrtr(handler, store=...) — the wrapper
- ElicitDeclined raised when user declines/cancels

7 E2E tests including the key assertion: side-effects above await fire
exactly once (the test measures audit_log count).
2026-03-20 17:06:58 +00:00
Max Isbey 29cb1ba837 examples(mrtr): handler-shape comparison deck (SEP-2322)
Python-SDK counterpart to typescript-sdk#1701. Seven ways to write the
same weather-lookup tool so the diff between files is the argument.

SDK primitives (src/mcp/server/experimental/mrtr.py):
- MrtrCtx.once() — idempotency guard tracked in request_state (Option F)
- ToolBuilder — structural step decomposition; end_step runs exactly once
  regardless of round count (Option G)
- input_response() — sugar for the guard-first pattern
- sse_retry_shim() — Option A comparison artifact (pragma no-cover until
  LATEST_PROTOCOL_VERSION bumps past the MRTR gate)
- dispatch_by_version() — Option D comparison artifact

Option examples (examples/servers/mrtr-options/):
- E (degrade-only): the SDK default. MRTR-native; pre-MRTR gets a default
  or error. Both quadrant rows collapse here.
- A (SSE shim): SDK emulates retry over SSE. Safe re-entry, hidden loop.
- B (await shim): exception-based. UNSAFE — hidden double-execution above
  await. Not a ship target; for contrast.
- C (version branch): explicit if/else in handler body.
- D (dual handler): two functions, SDK picks by version.
- F (ctx.once): idempotency guard, opt-in per side-effect.
- G (ToolBuilder): no above-the-guard zone; end_step structurally
  unreachable until all elicitations complete.

The invariant test (tests/experimental/test_mrtr.py) parametrises E/F/G
against the same Client + callback to prove identical wire behaviour —
the server's internal choice doesn't leak. The footgun test measures
audit_log count to prove F and G actually hold the guard (naive handler
fires twice; F and G fire once).

Both F and G depend on request_state integrity. The demos use plain
base64-JSON; a production SDK MUST HMAC-sign the blob.
2026-03-20 16:09:46 +00:00
Giulio Leone b33c811675 perf: use deque for InMemoryTaskMessageQueue FIFO operations (#2165) 2026-03-05 15:44:33 +00:00
Max Isbey 0a22a9dc33 refactor: replace lowlevel Server decorators with on_* constructor kwargs (#1985) 2026-02-12 15:55:54 +00:00
Marcelo Trylesinski 1a8c14a5b8 refactor: replace mcp.types as types to from mcp import types (#1986) 2026-02-03 16:42:29 +00:00
Marcelo Trylesinski 4fc49c62bd feat: add ClientRequestContext type alias for client-side handlers (#1989) 2026-02-03 17:37:38 +01:00
Marcelo Trylesinski b1f7eec3cd refactor: split RequestContext between server and client (#1987) 2026-02-03 14:35:07 +01:00
Marcelo Trylesinski acba5478a9 refactor: McpError renamed to MCPError and flatten parameters (#1956)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2026-01-26 14:37:44 +01:00
Max Isbey a7ddfdae07 ci: add strict-no-cover to detect unnecessary coverage pragmas (#1897) 2026-01-23 21:00:20 +01:00
Marcelo Trylesinski f0ab53e194 Add meta to Client methods (#1923) 2026-01-22 14:50:39 +01:00
Marcelo Trylesinski 5fdd48a0d3 Completely drop RootModel from types module (#1910) 2026-01-19 14:29:15 +01:00
Marcelo Trylesinski f4672c5084 Drop RootModel from JSONRPCMessage (#1908) 2026-01-19 14:04:15 +01:00
Felix Weinberger d41d0c0128 chore: add D212 lint rule to enforce Google-style docstrings (#1892) 2026-01-16 16:10:52 +00:00
Marcelo Trylesinski e94b386a13 refactor: use snake case instead of camel case in types (#1894) 2026-01-16 15:51:27 +01:00
Max Isbey b26e5b907f Add missing TasksCallCapability to enable proper MCP task support (#1854)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 10:55:38 +01:00
Marcelo Trylesinski 3ffe142e9a Support Python 3.14 (#1834) 2026-01-07 16:28:23 +00:00
Max Isbey c92bb2f7ff SEP-1686: Tasks (#1645) 2025-11-28 18:51:58 +00:00