Why architecture matters here

The architecture matters because it is the mechanism by which an MCP server's state can change under a client's feet without breaking the client. A server is not static: a server that exposes a user's files might gain and lose tools as plugins load, a server backed by a live database might see resources appear and vanish, a server proxying an API might have its prompt catalog updated. Without notifications, a client would either poll — wasteful and laggy — or work from a stale snapshot. Notifications let the server say 'my capabilities changed, re-list' exactly when it happens, so the client's view stays fresh without polling.

It matters for user experience during long operations. Many MCP tools do real work that takes time — a large search, a build, a data export. A pure request/response model leaves the user staring at a spinner with no signal until the whole thing finishes or times out. Progress notifications let the server report incremental advancement against a token the client supplied, turning an opaque wait into a visible one. This is not cosmetic; a visible progress stream is often the difference between a user waiting patiently and a user assuming the tool has hung and killing it.

It matters because it decouples the two sides temporally. A request ties the caller's fate to the callee's — the caller blocks until the callee answers. A notification does not; the sender continues immediately, and the receiver processes when it can. That decoupling is what lets MCP support cancellation (a client fires a cancelled notification and moves on, rather than waiting for the server to acknowledge the abort), lifecycle signaling, and telemetry (log messages) without those side channels interfering with the primary request flow.

Finally, it matters because getting the request-versus-notification choice right is a correctness decision. Anything that needs a guaranteed, acknowledged outcome — 'did this actually happen?' — must be a request, because only a request has a response to carry success or failure. Anything that is a best-effort hint the receiver can act on without confirmation should be a notification. Miscategorize, and you either build fragile systems that assume notifications were delivered, or you add needless latency by demanding responses to fire-and-forget events. The architecture teaches you where that line is.

Advertisement

The architecture: every piece explained

Structurally, a notification is a JSON-RPC message with a method and optional params and, crucially, no id. The absence of the id is the signal: the receiver must not send a response, and the sender must not wait for one. Method names for MCP's built-in notifications are namespaced, typically under notifications/ (for example notifications/tools/list_changed, notifications/progress, notifications/cancelled). Both parties can send notifications; direction depends on the type.

The list-changed family flows server→client: tools/list_changed, resources/list_changed, and prompts/list_changed. Each tells the client that the corresponding catalog has changed and should be re-fetched with the appropriate list request. The notification carries no diff — it is a hint to re-list, not the new list itself — which keeps it small and lets the client decide when to refresh. A server declares in its capabilities whether it will emit these, so the client knows whether to rely on them or fall back to periodic re-listing.

Progress notifications (notifications/progress) flow from whoever is doing the work toward whoever requested it, usually server→client. They reference a progress token that the requester included when starting the operation, so the receiver can correlate the updates with the specific request in flight. Each notification reports how far along the operation is, optionally with a total, letting the client render a determinate or indeterminate progress indicator. Related is the resource-updated notification, which fires for resources the client has explicitly subscribed to, telling it a particular resource's contents changed so it can re-read just that one.

Two more notifications round out the set. Cancellation (notifications/cancelled) lets either side signal that a previously-issued request, identified by its id, should be aborted; it is a notification precisely because the canceller does not wait for confirmation — the receiver stops the work best-effort. And lifecycle/telemetry notifications — the client's initialized signal that setup is complete, and log messages carrying server-side diagnostics — use the same fire-and-forget shape to convey information that needs no reply. Across all of them the invariant holds: no id, no response, best-effort delivery.

One design consequence deserves emphasis: because a notification carries no id, it also carries no natural way to return an error. If a receiver cannot make sense of a notification, its only recourse is to ignore it — there is nowhere to send a fault. This is the mirror image of a request, where a malformed or unauthorized call gets a structured error response. It means notification schemas should be simple and forgiving, receivers should tolerate fields they do not recognize rather than reject the message, and any information whose validity the sender must confirm has no business being a notification. The protocol's split between the two message shapes is, at bottom, a split between 'I need to know this worked' (request) and 'I am telling you something you can use if it arrives' (notification), and the missing error channel is what keeps that boundary honest.

MCP notifications — one-way, fire-and-forget messages with no responseserver and client push state changes without the request/response round tripSenderserver or clientnotify: method + paramsno id field -> no replyReceiver handlerreact, do not respondlist_changedtools/resources/promptsprogresslong-running op updatesresource updatedsubscribed resourcecancelled — abort an in-flight request by idlog message / initialized — lifecycle + telemetryOps — no delivery guarantee, idempotent handlers, backpressure, orderingsenddeliverkindsoperate
An MCP notification is a JSON-RPC message with a method and params but no id, so the receiver reacts without sending a response; they carry list-changed events, progress updates, resource-update pushes, cancellation, and log/lifecycle signals in both directions.
Advertisement

End-to-end flow

Follow a list-changed sequence. A client connects, completes the initialize handshake, and sends the initialized notification to tell the server setup is done. It then issues a tools/list request and gets back the current tools, which it presents to the model as available functions. The connection is now steady-state, with the client holding a snapshot of the server's tools.

Sometime later the server's underlying capability set changes — a plugin loads, adding two new tools. The server emits a notifications/tools/list_changed message: method, no params of substance, no id. It does not wait for anything; it has already updated its own state and simply informs the client. The client receives the notification, recognizes it as a hint that its tool snapshot is stale, and — at a moment of its choosing — issues a fresh tools/list request. Now its view matches the server's, and the model can use the new tools.

Now a progress sequence. The client calls a long-running tool, including a progress token in the request. As the server works, it emits a stream of notifications/progress messages referencing that token — 10%, 40%, 80% — each fire-and-forget. The client updates a progress bar on each. When the work completes, the server sends the actual response to the original tool-call request (that response carries the request's id), and the client knows the operation is finished. The progress notifications were an out-of-band commentary running alongside the one authoritative request/response pair.

Consider cancellation to see the fire-and-forget contract clearly. Midway through that long tool call, the user hits stop. The client sends notifications/cancelled referencing the in-flight request's id and immediately updates its UI as cancelled — it does not block waiting for the server. The server, on receiving the notification, stops the work best-effort and typically completes the original request with a cancellation result or error. If the notification is lost, the client's cancellation intent might not take effect, which is exactly why cancellation is best-effort and why robust servers also honor cancellation through other means (like the request timing out). The absence of a response channel shapes every one of these flows.