# Understanding id_counter in McpClient: JSON-RPC Message Correlation Explained

> Learn how McpClient uses id_counter to generate unique IDs for JSON-RPC requests, ensuring correct correlation with asynchronous SSE responses. Keep your communication organized.

- Repository: [Junjie.M/dify-plugin-tools-mcp_sse](https://github.com/junjiem/dify-plugin-tools-mcp_sse)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/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:

```python
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:

```python
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:

```python
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`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/utils/mcp_client.py), where the correlation logic spans the ID generation, request embedding, and response handling phases. The [`main.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/main.py) entry point and [`provider/mcp_tool.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/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:

```python
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:

```python
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:

```python

# 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`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/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`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/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.