This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# todos-server — the reference MCP server, in Python
A small project todo board where **every server-side MCP feature has a real job**: tools that mutate state, resources that expose it, prompts that seed conversations, sampling that borrows the connected host's model, elicitation that asks the user, progress and logs while it works, and per-resource subscriptions that announce every change. It is a faithful port of the TypeScript SDK's [`examples/todos-server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/todos-server) — think of it as the "polls app" of MCP servers: small enough to read in one sitting, real enough that nothing in it is contrived.
It serves **both protocol revisions at once** — 2026-07-28 and 2025-11-25 are negotiated per connection, from the same handlers — and **both transports**: stdio and Streamable HTTP.
## Run it
From this directory:
```bash
# stdio — for hosts that spawn their servers as child processes
uv run python -m mcp_todos_server
# Streamable HTTP — for remote-style connections (default port 3000; --port or $PORT to change)
uv run python -m mcp_todos_server --transport streamable-http
```
Over stdio the server speaks on stdin/stdout (its own diagnostics go to stderr). Over HTTP it serves `http://127.0.0.1:3000/mcp`.
There is no era flag: both entries detect each connection's revision during the handshake, so a 2025-era client and a 2026-era client can talk to the same process — simultaneously, over HTTP.
The TypeScript SDK's reference host, [`cli-client`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/cli-client), connects to the HTTP entry out of the box:
```bash
uv run python -m mcp_todos_server --transport streamable-http # terminal A, this repo
| 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` |
| 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 |
| Resources | `todos://board`, `todos://tasks/{id}` | one concrete resource + a URI template; every task also appears in `resources/list` |
| Subscriptions | the board | `resources/subscribe`/`unsubscribe` handlers for 2025-era clients; `subscriptions/listen` streams (over HTTP) for 2026-07-28, consumed client-side with [`client.listen(...)`](https://py.sdk.modelcontextprotocol.io/v2/client/subscriptions/); every mutation notifies |
| list_changed | every mutation | resource list + resource updated notifications on both eras |
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.
| `REQUEST_STATE_SECRET` | Key for the sealed `request_state` (≥ 32 bytes). Unset, the SDK generates a per-process key — fine whenever a single process serves the whole flow. |
| `PORT` | HTTP port when `--port` isn't passed (default 3000). |
## Layout
```text
mcp_todos_server/
server.py transport entry: stdio by default, streamable HTTP behind --transport
todos.py the application: state, tools, resources, prompts, subscriptions — every feature above
```
## Fidelity to the TypeScript reference
This port is verified against the TypeScript `todos-server` by driving both over stdio and HTTP, on both protocol eras, through an identical scripted scenario (same tool calls, elicitation answers, and sampling replies): every tool result text, structured output, elicitation form, sampling request, progress sequence, and log line matches. Known, deliberate differences:
- **JSON Schema style.** Input schemas come from pydantic here and zod there, so cosmetics differ (pydantic emits `title`s and `$defs` refs for the nested `add_tasks` items). The schemas are semantically identical.
- **`resources/list` composition.** The TypeScript `ResourceTemplate` has a `list` callback; `MCPServer` doesn't, so this example overrides the low-level `resources/list` handler to append one entry per task (the same private-API pattern the everything-server uses for `resources/subscribe` and `logging/setLevel`).
- **`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.
- **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.
- **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 subscription set is also process-wide, so a mutating session that never subscribed is notified once any other session has subscribed (the TypeScript entry scopes subscriptions per session and 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`).
- **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.
- **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. On 2026-07-28 connections, both servers answer a client that never declared the sampling/elicitation capability with a structured `-32021` protocol error naming the missing capability; on pre-2026 connections this server still answers `-32021`, where the TypeScript server reports an `isError` tool result.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add the todos-server reference example #3048
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Add the todos-server reference example #3048
Filter by extension
Only manifest files
Viewed files
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There are no files selected for viewing
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.