Loading
0xA0Lesson 11 of 15

Keep state with handles, and listen for changes

Carry state across calls with explicit handles, and get notified when lists or resources change.

20 min 5-question quiz 1 code exercise
By the end of this lesson you can
  • Design a tool that returns and accepts a state handle
  • Bind handles to the authenticated user
  • Use subscriptions/listen for change notifications

MCP has no protocol session, so a server can’t tie two tool calls together by connection. When it needs state - a shopping basket, an open browser tab, a database transaction - the spec’s guidance is an explicit handle:

  1. a creation tool returns a handle, e.g. {"basket_id": "bsk_a1b2c3"},
  2. the model passes it as an argument to later calls (add_item(basket_id, sku)),
  3. the server looks up the state under that key.
handles.py
1import secrets
2baskets = {}
3
4def create_basket(user):
5    basket_id = "bsk_" + secrets.token_hex(8)
6    baskets[(user, basket_id)] = []
7    return basket_id
8
9def add_item(user, basket_id, sku):
10    if (user, basket_id) not in baskets:
11        return "error: unknown or expired basket"
12    baskets[(user, basket_id)].append(sku)
13    return f"{len(baskets[(user, basket_id)])} item(s)"
14
15basket = create_basket("ana")
16print(add_item("ana", basket, "book"), "|", add_item("mallory", basket, "tv"))
Output
1 item(s) | error: unknown or expired basket

Good handles are:

  • unguessable - random (like a UUIDv4), not basket-1, basket-2;
  • bound to the user - stored as user:handle with the user taken from the verified token, so a leaked handle is useless to anyone else (a handle is a name, not a password);
  • limited in lifetime, with the policy stated in the tool description (“baskets expire after 24 hours”);
  • clear when expired - return a tool execution error saying so, so the model can create a new one.

Subscriptions and long-running work

  • Change notifications. A client opens a long-lived subscriptions/listen request, choosing what to hear about (tool, prompt or resource list changes, or updates to specific resource URIs). The response is a stream that stays open; notifications carry a subscriptionId in _meta.
  • Progress. A request can include a progressToken in _meta to receive notifications/progress while it runs.
  • Cancellation. On Streamable HTTP, closing a request’s response stream cancels it; on stdio, the client sends notifications/cancelled.
  • Tasks for work that takes minutes or hours now live in the official io.modelcontextprotocol/tasks extension, with tasks/get for polling.

Key takeaways

  • No sessions: carry state with explicit handles passed as tool arguments.

  • Make handles random, user-bound and expiring - never a substitute for authorization.

  • subscriptions/listen streams change notifications; progressToken streams progress; Tasks handle long-running work.

Lesson quiz

5 questions · pass with 4 correct · up to 50 XP

Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.

Practice: write Python

Write Python in the editor and run it against sample inputs. Python runs locally in your browser using a WebAssembly runtime.

Exercise 1

Implement user-bound handles

+25 XP

Process commands until the input ends. Each line is USER create HANDLE, USER add HANDLE ITEM or USER show HANDLE. State is stored per (user, handle).

  • create: start an empty basket and print created HANDLE.
  • add: if the user owns that basket, add the item and print added ITEM; otherwise print error: unknown basket.
  • show: print the items joined by commas (or empty), or error: unknown basket.
  • Owner and intruder
  • Empty and unknown
main.py
Loading editor…

Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.

Questions about this lesson

Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.

Loading posts…

Did you like the lesson? 😆👍
Consider a donation to support our work: