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_changed notification. 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_changed so 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:

typescript
import { 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:

text
Pattern                       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:

typescript
import { 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

  1. 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.
  2. 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.
  3. 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

  1. 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%).
  2. 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.
  3. Design notifications to be idempotent — Clients may receive duplicate notifications due to network retries. A resources/list_changed notification 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_changed when resource content changes so clients can re-fetch.
  • Send notifications/tools/list_changed when tools are dynamically added or removed.
  • Throttle notifications to avoid overwhelming clients, and design them to be idempotent.

Code Examples

typescript
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"
});
✓ Completed