Say Only That It Changed — Why the Value Must Not Ride Along

5 min
GoalLast in the mcp_server course. The screen stops asking every second. The server tells it, and the notification carries the uri and no value. And nothing at all goes to a client that did not subscribe.

Through part 5, a screen wanting the latest value had to ask — every second, or when a person refreshed.

Tell it

One line after changing the value.

waiting -= n;
_save(waiting);
// Say that it changed. Not what it changed to.
server.notifyResourceUpdated('desk://waiting');

Add a subscribable resource and turn subscribe: true on.

resources: ResourcesCapability(listChanged: true, subscribe: true),

What goes on the wire

{"jsonrpc":"2.0","method":"notifications/resources/updated","params":{"uri":"desk://waiting"}}

Only the uri. How many are waiting is not in it.

Why the value must not ride along

Putting the value in looks obvious — one round trip instead of two. But notifications guarantee no ordering.

server: waiting becomes 2 → notification A sent
server: waiting becomes 1 → notification B sent
client: B arrives (1)  →  A arrives (2)   ← the order flipped
screen: 2

With a value attached, a late old notification overwrites a newer value. The screen quietly shows the wrong number and nobody knows.

Send only "it changed" and the problem disappears. However many arrive, in whatever order, the receiver reads and gets the latest at that moment.

The kitchen screen piece reached the same conclusion. There it was a design choice; here the package only sends it that way — notifyResourceUpdated(uri) has no slot for a value.

Nothing goes out without a subscription

Caught while building. notifyResourceUpdated was called, the server logged it, and nothing appeared on the wire.

notified desk://waiting        ← the server did call it
(nothing on the client side)

Because it is not sent to clients that did not subscribe. That is the spec.

{"jsonrpc":"2.0","id":2,"method":"resources/subscribe","params":{"uri":"desk://waiting"}}

After that it arrives. An easy place to mistake for a server defect — the server was right, the receiver had not asked.

Verification — parse it to see whether a value rode along

ask step6 \
  '{"jsonrpc":"2.0","id":2,"method":"resources/subscribe","params":{"uri":"desk://waiting"}}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"desk.admit","arguments":{"count":1}}}'
params = json.loads(lines[0]).get('params', {})
extra = set(params) - {'uri'}
if extra:
    print('the notification carried', sorted(extra), '— it must carry only the uri')
    sys.exit(1)

The first attempt checked with grep waiting and produced a false positive. The uri itself is desk://waiting, so it always matches. It cannot be a string check; the keys have to be parsed.

   notification params: {"uri": "desk://waiting"}
step6  notification carried the uri and no value

Six steps done

step1  initialize -> serverInfo Course
step2  tools/list -> desk.count
step3  refused 0 and 99, admitted 1 -> waiting 2
step4  ui://desk served from ui/desk.json (6 lines), not from the code
step5  admitted 2, process ended, a new process still reads waiting 1
step6  notification carried the uri and no value

6 steps · each checked against its own claim

One server grew through six steps, and each step checks what that piece claims — not that it ran.

Run it yourself

cd content/sample/course-server
dart pub get
bash verify.sh            # all six steps
dart run bin/step6.dart   # the finished one

What to take

  1. notifyResourceUpdated(uri) — having no slot for a value is the design
  2. Subscribe first — nothing goes out without it
  3. Parse, do not grep — a string check trips on the uri

Next track

That is the server. Next is the client side — connecting, asking what it can do, and receiving a screen to draw.

Run the sample

cd course-server
dart pub get
dart run bin/step6.dart