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
2.9 KiB
2.9 KiB
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
# 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.pymain→Client(target, mode=mode, elicitation_callback=..., message_handler=...)— the construction is the example: callbacks are plain constructor kwargs, andmode=is explicit. The scripted elicitation answer and thelist_changedevent are locals ofmain, so every connection starts clean.server.pylifespan→Board— long-lived mutable state belongs in the lifespan context, never a module global. Tools reach it viactx.request_context.lifespan_context; this 2-hop path is interim and will shorten toctx.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:MCPServerhas no publicremove_resource()yet, soremove_notereaches a private attribute — do not copy that line.server_lowlevel.pyshows the clean equivalent:on_list_resourcesreads the board and builds the list fresh per call, so removal is justboard.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-tripInputRequiredResultflow (seemrtr/, not yet implemented). The client branches onclient.protocol_version.
Caveats
list_changedandctx.elicit()are skipped on modern legs: the notification needs a standalone stream andctx.elicit()would raiseNoBackChannelError.mainbranches onclient.protocol_version in HANDSHAKE_PROTOCOL_VERSIONS.
Spec
See also
tools/, resources/, legacy_elicitation/, lifespan/, standalone_get/
(list_changed over the GET stream).