Introduction
MCP is not limited to request-response interactions. Servers can push notifications to clients for real-time updates, progress reporting, and resource change signals. These event-driven patterns transform MCP from a simple tool-calling protocol into a reactive system capable of keeping clients informed about long-running operations and changing state.
Key Concepts
- Server-Sent Notifications: MCP servers can send one-way notifications to clients without receiving a response. Notifications are used for signaling state changes, not for requesting action.
- Progress Notifications: For long-running tool invocations, servers can send incremental progress updates using
sendProgress(). The client receives current progress and total values, enabling progress bar UIs. - Resource Change Notifications: When a resource's content changes, the server sends a
notifications/resources/list_changednotification. Clients that have subscribed to resource updates can re-fetch the resource to get the latest content. - Tool List Change Notifications: When tools are added or removed dynamically, the server sends
notifications/tools/list_changedso clients can update their tool catalog. - Notification Handlers: Clients register handlers for specific notification types. This allows reactive behavior: when a resource changes, the client can automatically re-read it and update the LLM's context.
Real World Context
A data pipeline MCP server processes large CSV files. When the LLM invokes the process_csv tool with a 500MB file, the operation takes several minutes. Without progress notifications, the user stares at a spinner with no idea how long it will take. With progress notifications, the host displays a progress bar: "Processing: 45% complete (225MB of 500MB)." The user knows the operation is working and can estimate completion time.
Similarly, a monitoring server watches application health. When a service goes down, the server pushes a resource change notification. The client's LLM automatically re-reads the health resource and proactively informs the user: "The payments service went offline 30 seconds ago."
Deep Dive
Progress Notifications for Long-Running Tools
The MCP SDK provides a sendProgress callback in tool handlers for reporting incremental progress:
typescriptimport { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; const server = new McpServer({ name: "data-processor", version: "1.0.0" }); server.tool( "process_dataset", "Process a large dataset with progress tracking", { dataUri: z.string().describe("URI of the dataset to process") }, async ({ dataUri }, { sendProgress }) => { const chunks = await loadChunks(dataUri); const totalChunks = chunks.length; const results = []; for (let i = 0; i < totalChunks; i++) { const result = await processChunk(chunks[i]); results.push(result); // Send progress update to the client sendProgress(i + 1, totalChunks); } return { content: [{ type: "text", text: `Processed ${totalChunks} chunks. Summary: ${summarize(results)}` }] }; } );
The sendProgress(current, total) call sends a notification with the current step and total steps. The host can render this as a percentage, a progress bar, or a textual update. Progress notifications are fire-and-forget — the server does not wait for a response.
Resource Change Notifications
When your server's resources change, notify the client so it can re-fetch:
typescript// Register a dynamic resource server.resource( "service-health", "health://services/all", "Current health status of all monitored services", async () => { const health = await checkAllServices(); return { contents: [{ uri: "health://services/all", mimeType: "application/json", text: JSON.stringify(health) }] }; } ); // When service health changes, notify the client serviceMonitor.on("healthChange", () => { server.server.sendNotification({ method: "notifications/resources/list_changed" }); });
The client receives the notification and knows that the resource list or content has changed. If the client previously subscribed to specific resources, it will re-read them automatically.
Tool List Change Notifications
When your server dynamically adds or removes tools, notify clients:
typescript// Plugin loaded at runtime function loadPlugin(plugin) { server.tool( plugin.toolName, plugin.description, plugin.schema, plugin.handler ); // Tell the client to re-fetch the tool list server.server.sendNotification({ method: "notifications/tools/list_changed" }); }
This is essential for plugin architectures where capabilities change at runtime. Without this notification, the client would not know that new tools are available.
Building Reactive MCP Applications
Combine notifications to build reactive systems:
textPattern Notifications Used ──────────────────────────── ───────────────────────────────── Progress tracking sendProgress(current, total) Live data feeds notifications/resources/list_changed Plugin hot-loading notifications/tools/list_changed Operation completion sendProgress(total, total) + result
A reactive MCP application uses notifications to keep the client's LLM context fresh. Instead of the LLM polling for updates, the server pushes changes, and the client updates the LLM's available context proactively.
Client-Side Notification Handling
On the client side, notification handlers are registered to react to server events:
typescriptimport { Client } from "@modelcontextprotocol/sdk/client/index.js"; const client = new Client({ name: "my-host", version: "1.0.0" }); // Handle resource changes client.setNotificationHandler( "notifications/resources/list_changed", async () => { const { resources } = await client.listResources(); console.error("Resources updated:", resources.map(r => r.name)); } ); // Handle tool list changes client.setNotificationHandler( "notifications/tools/list_changed", async () => { const { tools } = await client.listTools(); console.error("Tools updated:", tools.map(t => t.name)); } );
Clients can use these handlers to update their internal state, refresh the LLM's context, or display notifications to the user.
Common Pitfalls
- Flooding with progress updates — Sending a progress notification for every iteration of a million-item loop overwhelms the client. Batch progress updates: send every 1% or every N seconds, not every item.
- Assuming notifications are delivered — Notifications are fire-and-forget. The server has no guarantee that the client received them. Do not use notifications for critical state that must be acknowledged.
- Not cleaning up notification handlers — If a client registers handlers but never cleans them up, it can accumulate stale handlers that reference freed resources. Clean up handlers when disconnecting.
Best Practices
- Throttle progress notifications — For operations processing thousands of items, send progress updates at most once per second or at meaningful milestones (10%, 25%, 50%, 75%, 100%).
- Use resource notifications for live data — If your server wraps a data source that changes frequently (monitoring, logs, metrics), use resource change notifications to keep the client's context current.
- Design notifications to be idempotent — Clients may receive duplicate notifications due to network retries. A
resources/list_changednotification should always cause a re-fetch, not an incremental update that could go wrong if received twice.
Summary
- MCP supports server-sent notifications for progress, resource changes, and tool list changes.
- Use
sendProgress(current, total)in tool handlers for long-running operations with progress tracking. - Send
notifications/resources/list_changedwhen resource content changes so clients can re-fetch. - Send
notifications/tools/list_changedwhen tools are dynamically added or removed. - Throttle notifications to avoid overwhelming clients, and design them to be idempotent.
Code Examples
server.tool(
"process_data",
"Process a large dataset",
{ dataUri: z.string() },
async ({ dataUri }, { sendProgress }) => {
for (let i = 0; i < 100; i++) {
await processChunk(i);
sendProgress(i + 1, 100);
}
return {
content: [{ type: "text", text: "Processing complete" }]
};
}
);
// Notify client of resource changes
server.server.sendNotification({
method: "notifications/resources/list_changed"
});