Development Choices

Listen for MCP Change Notifications

Author
Joseph TrasattiMember of technical staff
Published
Section
MCP
Length
5 min read3 sources cited

In MCP revision 2026-07-28, a client opens `subscriptions/listen`, requests only notification types that both it wants and the server advertises, and treats stream completion differently from failure. On an unexpected close, it reconnects with bounded backoff, invalidates the affected cache, and refetches authoritative state.

Prerequisites

Before listening, retain the server-advertised capabilities from discovery and define which notification types your application actually uses. You also need to know which cached result each requested type can invalidate.

The subscription pattern in MCP revision 2026-07-28 makes change delivery explicit: the client opens subscriptions/listen and selects notification types instead of passively receiving unsolicited list-change events. If the client has not completed capability negotiation, it does not yet have enough information to construct a valid filter.

Notifications are explicitly subscribed

An MCP client opens a listen stream, receives a change, invalidates its cache, and refetches
Subscriptions complement TTL-based caching when changes matter immediately.
  1. Define the client-interest set.

    Start with the consumers of the data, not with every notification type you know about. For each cache or view that needs prompt change handling, record the notification type it needs and the invalidation action that type triggers.

    The TypeScript SDK migration guidance for revision 2026-07-28 reflects the move to explicit subscription handling. Keep this interest set in application code or configuration so it can be reviewed alongside the feature that depends on it.

    Requesting a type without a consumer adds handling work without improving freshness. Omitting a type that a consumer relies on leaves that consumer dependent on its existing refetch path. The right set is therefore the smallest set that covers the caches whose changes matter to this client.

  2. Intersect interest with advertised capabilities.

    Build the listen filter from the intersection of the client-interest set and the server-advertised notification capabilities. Do not send the client-interest set directly: it may contain types supported by another server or by a newer deployment but not by the server currently connected.

    The selection rule can remain independent of any SDK API:

    const listenTypes = interestedTypes.filter((type) =>
      advertisedTypes.has(type)
    );

    This intersection is the condition that decides whether a type belongs in the request: the client must want it, and the server must advertise it. When the intersection is empty, there is nothing for this client to request from that server. Preserve the client-interest set, however, because a later discovery result may advertise a different capability set.

    Recompute the intersection whenever the server capabilities used by the connection change. Do not carry a filter forward merely because it worked with an earlier server.

  3. Open subscriptions/listen with the filtered types.

    Open the listen operation explicitly and pass only the intersected notification types. The MCP revision 2026-07-28 changelog records the protocol-era change away from unsolicited list-change delivery. Code migrated from the previous behavior must not wait for events it never requested.

    Keep opening the operation separate from computing the filter. That separation makes it possible to test unsupported-type removal without starting a stream and to inspect exactly what the client requested. For a broader protocol upgrade, treat this as part of the migration from MCP 2025-11-25 to 2026-07-28, not as a transparent transport change.

  4. Classify how the listen operation ends.

    Give the listen adapter two distinct terminal paths: a graceful result and an unexpected stream close. Do not reduce both to a generic completion callback. A graceful result means the operation produced its expected terminal outcome; route it to the client’s completed state. An unexpected close means the client can no longer rely on that listen operation to deliver selected changes; route it to recovery.

    The application-level distinction can be represented without pretending that these names are SDK methods:

    if (outcome.kind === 'graceful-result') {
      markListenCompleted();
    } else if (outcome.kind === 'unexpected-close') {
      beginRecovery();
    }

    Also keep notification handling separate from both terminal paths. A notification is work to process; it is not evidence that the operation ended. The cost of this separation is an extra state branch, but it prevents ordinary completion and loss of the stream from triggering the same response.

  5. Invalidate before refetching.

    When a selected notification arrives, identify the cached data affected by that notification type. Mark that cache invalid before starting its refetch. The required order is:

    1. receive the selected change notification;
    2. invalidate the affected cache;
    3. refetch the affected data;
    4. replace the invalid entry with the refetched result.

    Starting the refetch first leaves a period in which another reader can still treat the old entry as valid. Invalidation first makes the application’s uncertainty explicit. Do not clear unrelated caches merely because one selected type fired; broad invalidation increases refetch work without evidence that the other data changed.

    This notification path should complement the client’s existing server-discovery and cache-hint handling. The subscription tells the client when selected data may have changed; the refetch obtains the replacement state.

  6. Recover unexpected closes with bounded backoff.

    On an unexpected stream close, enter a reconnect loop with a bound. The bound may be a maximum delay, a maximum attempt count, or both, but it must prevent retries from continuing without limit. The supplied revision facts do not provide universal numeric values, so choose and document limits that match the application’s tolerance for delayed notifications and repeated connection attempts.

    Re-run the capability intersection before reopening subscriptions/listen; recovery must not assume that the next server advertises the same notification types. Invalidate caches affected by the selected types before refetching them, because the client cannot establish which changes occurred while the stream was unavailable.

    A graceful result does not enter this unexpected-close loop. Keeping that branch separate is what prevents a normal terminal result from being treated as a connection failure. Once the configured retry bound is reached, expose a degraded state to the application rather than silently leaving the cache presented as notification-backed.

  7. Test the three paths independently.

    Verify one successful notification, one graceful result, and one unexpected close. The notification test should show invalidation before refetch. The graceful-result test should finish without entering failure recovery. The close test should show bounded reconnect attempts, a newly computed capability intersection, and invalidation of the caches affected by the interrupted subscription.

    Also test a server that advertises none of the client’s desired notification types. The resulting listen filter is empty, and the client must not manufacture unsupported types merely to open the operation.

Expected result

The finished client opens subscriptions/listen only for notification types present in both its interest set and the server’s advertised capabilities. Selected changes invalidate affected caches before refetching. Graceful results complete normally; unexpected closes enter bounded recovery, renegotiate the usable filter, and do not leave affected caches presented as current.

Sources

  1. subscription pattern in MCP revision 2026-07-28modelcontextprotocol.io
  2. TypeScript SDK migration guidance for revision 2026-07-28ts.sdk.modelcontextprotocol.io
  3. MCP revision 2026-07-28 changelogmodelcontextprotocol.io

See also