Back to Python Sdk

stickynotes

examples/stories/stickynotes/README.md

2.0.0b12.9 KB
Original Source

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 mainClient(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 lifespanBoard — 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_notemcp.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_allctx.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

See also

tools/, resources/, legacy_elicitation/, lifespan/, standalone_get/ (list_changed over the GET stream).