Skip to content

Commit d1812a9

Browse files
committed
Adopt the resolver framework for the interactive tools
Rewrite clear_done, prioritize, and brainstorm_tasks from hand-rolled InputRequiredResult state machines onto Resolve dependencies, now that Sample landed alongside Elicit: the framework carries the rounds on 2026-07-28 connections and push-style requests on pre-2026 ones, seals the multi-round state, validates answers against the form schemas, and gates on client capabilities. This deletes the example's local legacy fulfilment driver, literal schema dicts, and JSON step state. The elicitation forms render byte-identical to the TypeScript reference's literals; the count-form theme field advertises its default via json_schema_extra while staying nullable, so an omitted answer still falls back to the tool argument like the reference. Resolver outcomes, not board recounts, drive the empty-board paths so concurrent mutations cannot desync the resolver from the tool body.
1 parent e3a3316 commit d1812a9

2 files changed

Lines changed: 176 additions & 272 deletions

File tree

examples/servers/todos-server/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ pnpm --filter @mcp-examples/cli-client start -- --server http://127.0.0.1:3000/m
4444
| Tools | `add_task`, `add_tasks`, `list_tasks`, `complete_task` | plain CRUD; `add_task` also returns `structuredContent` against an `outputSchema` |
4545
| Sampling | `prioritize`, `brainstorm_tasks` | the server borrows the _host's_ model; the host shows the request for approval first |
4646
| Elicitation (form) | `clear_done`, `brainstorm_tasks` | schema-driven forms; accept / decline / cancel all handled |
47-
| Multi-round input_required | `brainstorm_tasks` | theme+count form → optional custom-amount round → sampling round; state rides `request_state` as a step-discriminated JSON object, sealed by the SDK |
47+
| Multi-round input_required | `brainstorm_tasks` | theme+count form → optional custom-amount round → sampling round, as a resolver chain; the framework carries the recorded answers in sealed `request_state` |
4848
| Progress | `work_through_tasks`, `add_tasks` | paced per-task progress notifications via `ctx.report_progress` |
4949
| Logging | every mutating tool, via `log_info` | honours `logging/setLevel` on 2025 connections and the per-request log-level `_meta` opt-in on 2026-07-28 |
5050
| Resources | `todos://board`, `todos://tasks/{id}` | one concrete resource + a URI template; every task also appears in `resources/list` |
5151
| Subscriptions | the board | `resources/subscribe`/`unsubscribe` handlers for 2025-era clients; `subscriptions/listen` streams (over HTTP) for 2026-07-28; every mutation notifies |
5252
| list_changed | every mutation | resource list + resource updated notifications on both eras |
5353
| Prompts + completions | `plan-my-day`, `seed-board` | argument completion (project names, themes, task ids) wired to `completion/complete` via `@mcp.completion()` |
5454

55-
The two protocol eras differ in how interactive conversations travel: on 2025-era connections the wire carries _pushed_ `elicitation/create` / `sampling/createMessage` requests; on 2026-07-28 the server returns `input_required` results and the client retries the call with the answers. The interactive tools (`brainstorm_tasks`, `clear_done`, `prioritize`) are written **once**, as state machines over `input_required` rounds — on 2025-era connections the example's small `run_interactive` driver fulfils the same rounds as real push-style requests (the job the TypeScript SDK's built-in legacy shim does), so there is no era branch in any handler. For single-question preconditions, the SDK's own era-agnostic form is a `Resolve(...)` dependency that returns `Elicit(...)` — see the [Dependencies tutorial](https://py.sdk.modelcontextprotocol.io/v2/tutorial/dependencies/); this example hand-rolls the rounds instead so the multi-round flow, the sampling rounds, and the carried state are all visible in one place.
55+
The two protocol eras differ in how interactive conversations travel: on 2025-era connections the wire carries _pushed_ `elicitation/create` / `sampling/createMessage` requests; on 2026-07-28 the server returns `input_required` results and the client retries the call with the answers. The interactive tools (`brainstorm_tasks`, `clear_done`, `prioritize`) are written **once**, as [resolver dependencies](https://py.sdk.modelcontextprotocol.io/v2/handlers/dependencies/): a parameter annotated `Annotated[T, Resolve(fn)]` is filled before the tool body, and a resolver that returns `Elicit(...)` or `Sample(...)` has the framework put the question to the client over whichever transport the connection negotiated — so there is no era branch in any handler, and no round bookkeeping in the example. `brainstorm_tasks` shows the multi-round shape as a chain: a count/theme form, a conditional custom-amount form that only asks when the first answer was "custom", and a sampling round derived from the recorded answers.
5656

5757
## Configuration
5858

@@ -78,4 +78,5 @@ This port is verified against the TypeScript `todos-server` by driving both over
7878
- **`subscriptions/listen` over stdio.** The Python SDK serves 2026-era listen streams on streamable HTTP only; over stdio a listen request is rejected. Board-change notifications over stdio therefore reach 2025-era subscribers only.
7979
- **Legacy HTTP interactivity.** The TypeScript server's per-request HTTP posture refuses push-style sampling/elicitation for 2025-era HTTP clients; the Python server's default Streamable HTTP mode is stateful, so those tools work on that leg here.
8080
- **Legacy HTTP fan-out.** Pre-2026 board-change notifications go to the session that made the mutating call. Over stdio that is every subscriber; with several concurrent 2025-era HTTP sessions, the others don't hear about it (the TypeScript entry broadcasts via its handler notifier). Pre-2026 HTTP handshakes also advertise `listChanged: false` — the SDK exposes no seam to change that on the HTTP path (stdio is patched, see `serve_stdio`).
81-
- **Cancellation granularity.** When a 2025-era client cancels `work_through_tasks`, this SDK interrupts the handler at its next `await` (the in-flight pretend task stays open); the TypeScript server checks between tasks and finishes the in-flight one.
81+
- **Cancellation granularity.** When a client cancels `work_through_tasks`, this SDK interrupts the handler at its next `await` (the in-flight pretend task stays open); the TypeScript server checks between tasks and finishes the in-flight one.
82+
- **Elicited answers are schema-validated.** The resolver framework validates every accepted form answer against the requested schema, so a non-conforming answer (an out-of-range custom count, a value outside the count enum) fails the call with a validation error; the TypeScript handlers parse manually and report those as polite "Nothing added" declines. Similarly, a client that never declared the sampling/elicitation capability gets a structured `-32021` protocol error naming the missing capability, where the TypeScript server returns an `isError` tool result.

0 commit comments

Comments
 (0)