MCP 2026-07-28 one lesson per page
23 lessons
Breaks immediatelyInteractions & Execution · 26%Track 2 · 8 / 14

Change notifications: one stream you ask for

The standalone GET stream and resources/subscribe are gone. A client opens subscriptions/listen, an ordinary request whose response stays open, and opts in by notification type.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

The tool palette that updates itself

Your IDE has an MCP tool palette. At 14:05 the docs team ships a new search_changelog tool on their server.

The IDE opened one subscriptions/listen with toolsListChanged: true when it started. Seconds later notifications/tools/list_changed arrives, the IDE invalidates its cached list, and the new tool appears without a restart.

Meanwhile a phone client listening only to resourceSubscriptions: ["file:///team/oncall.json"] gets nothing but on-call rota changes. Opting in by type keeps a phone's battery and data plan out of everyone else's noise.

  • The filter has four optional fields: toolsListChanged, promptsListChanged, resourcesListChanged (booleans) and resourceSubscriptions (an array of URIs). The server MUST NOT send types the client didn't ask for.
  • The first message is always notifications/subscriptions/acknowledged, listing the subset the server agreed to honour.
  • Every notification carries io.modelcontextprotocol/subscriptionId, which equals the JSON-RPC id of the listen request. A client can run several subscriptions at once.
  • Request-scoped messages like notifications/progress and notifications/message never travel on the listen stream. They stay on the response stream of the request they belong to.
  • The subscription ends when the client closes the stream (HTTP) or sends notifications/cancelled (stdio). When the server ends it, it SHOULD send a final complete result first.

A standing GET stream belonged to a session. Making "listen" a normal request means it gets the same auth, routing, headers and version rules as everything else, and its state lives in the request, not the connection. Opting in by type also stops notifications nobody wanted.

Same job, two eras
Compare

This is how it works in 2026-07-28. Switch to Legacy only to see what it replaced.

ClientServerPOST subscriptions/listen (id 1, filter)subscriptions/acknowledged subId 1The stream stays opentools/list_changed subId 1resources/updated subId 1
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

Subscribing

2026-07-28
{  "jsonrpc": "2.0",  "id": 1,  "method": "subscriptions/listen",  "params": {    "_meta": { … per-request fields … },    "notifications": {      "toolsListChanged": true,      "resourceSubscriptions": ["file:///project/config.json"]    }  }}
What it used to look like (legacy, for comparison only)
Legacybefore
// ① a standalone SSE stream, opened with an HTTP GETGET /mcp   Accept: text/event-stream   Mcp-Session-Id: 1868a90c-…// ② a separate POST to subscribe to one resource{"jsonrpc": "2.0", "id": 5, "method": "resources/subscribe", "params": { "uri": "file:///project/config.json" }}

What comes back on that one stream

2026-07-28
// always first{"jsonrpc": "2.0", "method": "notifications/subscriptions/acknowledged", "params": {   "_meta": { "io.modelcontextprotocol/subscriptionId": 1 },   "notifications": { "toolsListChanged": true,                      "resourceSubscriptions": ["file:///project/config.json"] } }}// later, whenever the file changes{"jsonrpc": "2.0", "method": "notifications/resources/updated", "params": {   "_meta": { "io.modelcontextprotocol/subscriptionId": 1 },   "uri": "file:///project/config.json" }}

Verbatim from the spec. subscriptionId 1 is the id of the listen request, which is how a client tells several subscriptions apart.

  • Change notifications you push outside a listen stream go nowhere. Clients that open a GET stream SHOULD get 405.
  • You must implement the acknowledgement, the filter, the subscriptionId tagging, and stopping when the stream closes.
  • On stdio, if the process restarts, the client must send subscriptions/listen again; the server keeps no subscription state across reconnections.
  • For long-lived streams, send an SSE comment line (:) now and then as a keep-alive, so proxies don't time the stream out.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
# Terminal A: open a listen stream for tool-list changes (leave it running)mcp subscriptions/listen '{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen","params":{'"$META"',"notifications":{"toolsListChanged":true}}}'# expect: first event is notifications/subscriptions/acknowledged with subscriptionId 1 # Terminal B (define the helper there too): change the tool listmcp tools/call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"toggle_beta_tool","arguments":{},'"$META"'}}' -H 'Mcp-Name: toggle_beta_tool'# expect in A: notifications/tools/list_changed carrying subscriptionId 1 # Terminal B: change a resource you did NOT subscribe tomcp tools/call '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"touch_config","arguments":{},'"$META"'}}' -H 'Mcp-Name: touch_config'# expect in A: nothing at all (the filter is strict)

Answer all 3 correctly and this lesson is marked as learned.

Q1Where do notifications/progress messages for a long tools/call arrive?

Q2What is the subscriptionId on each notification?

Q3A client opens subscriptions/listen with only {"toolsListChanged": true}. A watched resource changes on the server. What should the client receive on that stream?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.