49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
63 lines
2.9 KiB
Markdown
63 lines
2.9 KiB
Markdown
# stickynotes
|
|
|
|
The "real app" capstone: tools mutate a sticky-notes board held in the
|
|
server's lifespan context, each note is a `note:///{id}` resource,
|
|
`notifications/resources/list_changed` fires on add/remove, and `remove_all`
|
|
blocks on a form-mode elicitation so the user must explicitly confirm a
|
|
destructive clear.
|
|
|
|
## Run it
|
|
|
|
```bash
|
|
# stdio (default — the client spawns the server as a subprocess)
|
|
uv run python -m stories.stickynotes.client
|
|
|
|
# HTTP — the client self-hosts the server on a free port, runs, then tears it down
|
|
uv run python -m stories.stickynotes.client --http
|
|
# same, against the lowlevel-API server variant
|
|
uv run python -m stories.stickynotes.client --http --server server_lowlevel
|
|
```
|
|
|
|
## What to look at
|
|
|
|
- **`client.py` `main` → `Client(target, mode=mode, elicitation_callback=...,
|
|
message_handler=...)`** — the construction is the example: callbacks are
|
|
plain constructor kwargs, and `mode=` is explicit. The scripted elicitation
|
|
answer and the `list_changed` event are locals of `main`, so every
|
|
connection starts clean.
|
|
- **`server.py` `lifespan` → `Board`** — long-lived mutable state belongs in
|
|
the lifespan context, never a module global. Tools reach it via
|
|
`ctx.request_context.lifespan_context`; this 2-hop path is interim and will
|
|
shorten to `ctx.state.*` in a later release.
|
|
- **`add_note` / `remove_note`** — `mcp.add_resource(FunctionResource(...))`
|
|
registers a concrete resource at runtime; `ctx.session.send_resource_list_changed()`
|
|
tells connected clients to re-list. **Gap:** `MCPServer` has no public
|
|
`remove_resource()` yet, so `remove_note` reaches a private attribute — do
|
|
not copy that line. `server_lowlevel.py` shows the clean equivalent:
|
|
`on_list_resources` reads the board and builds the list fresh per call, so
|
|
removal is just `board.notes.pop(...)` with no registry mutation.
|
|
- **`remove_all` → `ctx.elicit(...)`** — push-style server→client elicitation
|
|
needs a back-channel and an advertised client capability, so it only runs on
|
|
the legacy-era legs. On a modern connection there is no server→client
|
|
request channel; the modern equivalent is the multi-round-trip
|
|
`InputRequiredResult` flow (see `mrtr/`, not yet implemented). The client
|
|
branches on `client.protocol_version`.
|
|
|
|
## Caveats
|
|
|
|
- `list_changed` and `ctx.elicit()` are skipped on modern legs: the
|
|
notification needs a standalone stream and `ctx.elicit()` would raise
|
|
`NoBackChannelError`. `main` branches on
|
|
`client.protocol_version in HANDSHAKE_PROTOCOL_VERSIONS`.
|
|
|
|
## Spec
|
|
|
|
- [Tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)
|
|
- [Resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources)
|
|
- [Elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation)
|
|
|
|
## See also
|
|
|
|
`tools/`, `resources/`, `legacy_elicitation/`, `lifespan/`, `standalone_get/`
|
|
(`list_changed` over the GET stream).
|