Static resource definitions function well for data that remains constant, such as configuration files or historical records. However, modern applications frequently interact with data that changes state rapidly, such as application logs, stock tickers, or collaborative documents. Relying on the client to repeatedly request the same resource to check for updates, a technique known as polling, creates unnecessary network traffic and increases latency. The Model Context Protocol addresses this through a subscription and notification mechanism, allowing the server to alert the client only when relevant data has changed.
The MCP architecture employs a modified publish-subscribe pattern. Unlike traditional streaming where the data itself is pushed continuously, MCP decouples the notification of change from the retrieval of data. When a resource changes, the server sends a lightweight notification to the client. It is then the client's responsibility to initiate a new read request if it requires the updated content.
This separation prevents the server from overwhelming the client with large data payloads during high-frequency updates. The client maintains control over when it consumes the bandwidth to fetch the new context.
The following diagram illustrates the message flow between an MCP Client and Server during a subscription lifecycle.
The sequence demonstrates the decoupling of notification and data retrieval. The server notifies the client of a change, triggering a subsequent read request.
Before a client can subscribe to a resource, the server must explicitly declare that it supports resource notifications. This occurs during the initialization handshake. In the capabilities negotiation phase, the server includes a resources object with a subscribe boolean set to true.
If using the standard Python SDK, this configuration is often handled automatically when you register resource handlers. However, understanding the underlying JSON-RPC structure is valuable for debugging. The server initialization response includes:
capabilities={"resources":{"subscribe":true,"listChanged":true}}The listChanged capability is distinct from resource subscriptions. It informs the client that the list of available resources (the inventory) has changed, whereas subscribe relates to the content of a specific resource URI.
When a user or an LLM agent decides a specific resource is relevant to the ongoing conversation, the client sends a resources/subscribe request containing the uri of the target resource.
Upon receiving this request, the server does not need to return data immediately. Its primary task is to register the connection as an active observer of that URI. In a Python implementation, the server must maintain a mapping between the resource URI and the active client session.
Consider a server that exposes a dynamic system log. You first define the resource read logic:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("SystemMonitor")
# A simulated internal state representing a log file
log_state = ["Server started", "Initialization complete"]
@mcp.resource("system://logs/recent")
def get_recent_logs() -> str:
"""Returns the most recent log entries."""
return "\n".join(log_state)
In this state, the resource is passive. The client can read it, but it will not know if log_state changes.
To make this resource reactive, you must trigger a notification whenever the internal state changes. The MCP SDK provides methods to broadcast this update. The method usually accepts the URI of the resource that was modified.
When the data source updates, the server executes the notification logic. This is often tied to an external event loop, a file system watcher, or a database trigger.
import asyncio
async def append_log(message: str):
"""Updates the state and notifies subscribers."""
log_state.append(message)
# Notify the client that this specific resource has changed
# The client will likely issue a new read request immediately after
await mcp.server.request_context.session.send_resource_updated(
"system://logs/recent"
)
# Example background task simulating system activity
async def generate_traffic():
while True:
await asyncio.sleep(5)
await append_log("Heartbeat check: OK")
In this example, send_resource_updated constructs the JSON-RPC notification. The payload sent over the transport layer looks like this:
{
"method": "notifications/resources/updated",
"params": {
"uri": "system://logs/recent"
}
}
Notice that the payload does not contain the text "Heartbeat check: OK". It contains only the URI. This design minimizes bandwidth usage. If the client is currently busy or the user has paused the agent, the client can choose to ignore the notification or queue the read request for later.
When implementing subscriptions, you must consider the granularity of your URIs. If a resource represents a large dataset, such as a 10MB CSV file, triggering a notification for every single row edit forces the client to re-download the entire file repeatedly. This creates a performance bottleneck.
For rapidly changing data, consider two strategies:
data://rows/50-100) so updates only trigger small reads.The protocol also supports resources/unsubscribe. Standard clients will unsubscribe when the context window is cleared or when the user explicitly removes a resource from the context. Your server implementation generally relies on the SDK to handle the removal of the session from the subscription list, preventing memory leaks where the server continues trying to notify a disconnected or uninterested client.
You can verify your subscription logic using the MCP Inspector. After connecting your server:
system://logs/recent).notifications/resources/updated message followed immediately by a resources/read request initiated automatically by the Inspector.This cycle confirms that your server correctly bridges the gap between internal data changes and the external LLM context.
Was this section helpful?
© 2026 ApX Machine LearningEngineered with