Understanding id_counter in McpClient: JSON-RPC Message Correlation Explained

The id_counter is a monotonically increasing integer that generates unique identifiers for every JSON-RPC request, enabling the McpClient to match asynchronous SSE responses to their originating requests.

In the junjiem/dify-plugin-tools-mcp_sse repository, the McpClient class implements the Model Context Protocol (MCP) using JSON-RPC over Server-Sent Events (SSE). The id_counter instance variable serves as the backbone of request-response correlation, ensuring that concurrent tool calls and resource requests never mix up their replies.

What is id_counter in McpClient?

The id_counter is a private integer attribute initialized within the McpClient class that tracks the sequence of outgoing JSON-RPC requests. It guarantees that every message sent to the MCP server carries a globally unique identifier within the client session, which is essential for the stateless request-response pattern required by JSON-RPC 2.0.

The _get_next_id() Method

In utils/mcp_client.py, the _get_next_id() method atomically increments self.id_counter and returns the new value. This private method, located around lines 32-36, ensures thread-safe ID generation for concurrent operations:

def _get_next_id(self):
    self.id_counter += 1
    return self.id_counter

How id_counter Ensures JSON-RPC Message Correlation

JSON-RPC requires the server to echo the request's id field in its response. The McpClient leverages this specification to bridge the asynchronous gap inherent in SSE communication, where responses arrive via a separate event stream rather than direct HTTP responses.

Embedding Unique IDs in Outbound Requests

Every high-level client method embeds a fresh ID into the JSON-RPC payload. When you invoke list_tools(), call_tool(), list_resources(), read_resource(), list_resources_templates(), list_prompts(), or get_prompt(), each constructs a request dictionary containing "id": self._get_next_id() at lines 52-55, 71-74, 89-92, 108-111, 124-127, 141-144, and 161-164 respectively.

Correlating Asynchronous SSE Responses

The McpSseClient maintains a message_dict dictionary that maps incoming response IDs to their full message payloads. When the SSE listener receives a message from the server (lines 36-39), it immediately stores the payload using the echoed ID as the key:

self.message_dict[message["id"]] = message

Blocking and Retrieval with send_message()

The send_message() method blocks on a response_ready event until the specific message_id appears in message_dict. Once detected, it pops and returns the matching message (lines 66-78), completing the correlation cycle:

while True:
    self.response_ready.wait()
    self.response_ready.clear()
    if message_id in self.message_dict:
        message = self.message_dict.pop(message_id)
        return message

Source Code Implementation Details

The implementation resides primarily in utils/mcp_client.py, where the correlation logic spans the ID generation, request embedding, and response handling phases. The main.py entry point and provider/mcp_tool.py demonstrate real-world usage of this mechanism, but the core logic—all ID assignment and correlation—lives within the client utility.

Practical Code Examples

Listing Tools with Automatic ID Generation

This example demonstrates how list_tools() automatically handles ID generation and correlation:

from utils.mcp_client import McpSseClient

# Initialize the client

client = McpSseClient(name="demo", url="https://example.com/mcp/sse")

# This call automatically generates id=1, embeds it in the request,

# and waits for the response containing the same id

tools = client.list_tools()
print(tools)

The internal request structure becomes:

request = {
    "jsonrpc": "2.0",
    "id": self._get_next_id(),   # Returns unique integer, e.g., 1

    "method": "tools/list",
    "params": {}
}
response = self.send_message(request)  # Waits for id=1 in response

Manual ID Correlation Flow

To understand the correlation mechanism explicitly:


# 1. Generate unique ID

request_id = client._get_next_id()  # e.g., returns 2

# 2. Construct request

request = {
    "jsonrpc": "2.0",
    "id": request_id,
    "method": "tools/call",
    "params": {"name": "read_file", "arguments": {"path": "/tmp/test.txt"}}
}

# 3. SSE listener receives: {"jsonrpc":"2.0","id":2,"result":{...}}

#    and stores: self.message_dict[2] = response_data

# 4. send_message(request_id) detects id=2 in message_dict,

#    returns the response, and removes it from the dict

Summary

  • The id_counter generates monotonically increasing integers via _get_next_id() in utils/mcp_client.py.
  • Every JSON-RPC request includes a unique ID created from this counter before transmission.
  • The SSE listener stores incoming responses in message_dict using the echoed ID as the dictionary key.
  • The send_message() method blocks on response_ready until the specific ID appears, ensuring request-response pairing.
  • This mechanism safely supports concurrent requests without response mix-ups or race conditions.

Frequently Asked Questions

Why does McpClient use an integer counter instead of UUIDs?

The integer counter provides a lightweight, sequential identifier that is easier to debug and sufficient for single-client sessions. Since the MCP client maintains a stateful SSE connection where the server must echo the request ID, monotonic integers uniquely identify in-flight requests without the computational overhead of UUID generation and comparison.

How does id_counter handle concurrent requests?

The _get_next_id() method increments the counter atomically, ensuring that even when multiple threads simultaneously invoke call_tool() or list_resources(), each receives a distinct ID. The send_message() method then uses these unique IDs as keys to retrieve the correct response from the shared message_dict, preventing cross-talk between concurrent operations.

What happens if the SSE connection drops before a response arrives?

According to the implementation in utils/mcp_client.py, the send_message() method relies on the response_ready threading event. If the SSE connection drops, the listener stops populating message_dict, causing send_message() to block indefinitely until the configured timeout or connection restoration. Client implementations should wrap calls in appropriate timeout logic to handle transport failures gracefully.

Can the id_counter overflow during long-running sessions?

Python integers have arbitrary precision and will not overflow like fixed-width integers. While theoretically a counter could grow infinitely large over months of continuous operation, practical MCP client usage involves periodic restarts, and the memory overhead of large integers is negligible for the quantities of requests typically processed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →