# Architectural Differences Between McpSseClient and McpStreamableHttpClient in Dify MCP Tools

> Understand the architectural differences between McpSseClient and McpStreamableHttpClient in Dify MCP Tools. Learn about persistent SSE connections versus stateless HTTP POST calls for real-time messaging and RPC.

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

---

**The `McpSseClient` maintains a persistent Server‑Sent Events connection with background threading for real‑time bidirectional messaging, while `McpStreamableHttpClient` uses stateless HTTP POST requests with optional session headers for simpler, on‑demand RPC calls.**

Both transport clients implement the abstract `McpClient` base class defined in [`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/utils/mcp_client.py) (lines 21‑50) to provide identical high‑level APIs for tool discovery and execution. However, the architectural differences between `McpSseClient` and `McpStreamableHttpClient` fundamentally alter how messages traverse the network, manage state, and handle concurrency. Understanding these distinctions is critical when configuring MCP servers in the Dify plugin ecosystem.

## Transport Protocol and Connection Handling

The most significant architectural divergence lies in how each client maintains its connection to the MCP server.

**SSE Client (`McpSseClient`)** establishes a long‑lived HTTP connection that stays open for the client’s entire lifetime. It spawns a dedicated background thread (`_listen_thread`) that blocks on an SSE stream, continuously listening for server‑pushed events. This design requires careful resource management and explicit cleanup via the `close()` method to terminate the listener thread gracefully.

**Streamable HTTP Client (`McpStreamableHttpClient`)** operates statelessly. Each RPC call creates a new short‑lived HTTP POST request using `httpx`. No background threads are instantiated, and the client does not maintain an open socket between calls. This results in a lower resource footprint but eliminates the server’s ability to push unsolicited messages outside of request‑response cycles.

## Endpoint Discovery Mechanisms

The clients differ drastically in how they resolve the actual RPC endpoint URL.

**`McpSseClient`** discovers its endpoint dynamically. Upon initialization, the client connects to the configured base URL and waits for a special `"endpoint"` SSE event. According to the source code in [`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/utils/mcp_client.py) (lines 206‑215), the listener extracts the real RPC URL from this event:

```python
case "endpoint":
    self.endpoint_url = urljoin(self.url.rstrip("/"), sse.data.lstrip("/"))
    self._connected.set()

```

Subsequent JSON‑RPC payloads are POSTed to this dynamically discovered `endpoint_url` rather than the original configured URL.

**`McpStreamableHttpClient`** uses the URL supplied in the configuration directly (`self.url`). There is no discovery phase; the client immediately begins posting requests to the configured endpoint, simplifying deployment scenarios where the server URL is known ahead of time.

## Message Lifecycle and State Management

The request‑response lifecycle differs in synchronization complexity and session handling.

**SSE Message Matching** relies on an internal `message_dict` to correlate asynchronous responses. When the client sends a request (lines 247‑279), it blocks the main thread on a `threading.Event` (`response_ready`) while the background thread populates `message_dict` with incoming `"message"` events keyed by JSON‑RPC `id`:

```python
case "message":
    message = json.loads(sse.data)
    self.message_dict[message["id"]] = message
    self.response_ready.set()

```

The SSE client does not use HTTP cookies for session management; it relies solely on the persistent connection and the request/response id matching mechanism.

**Streamable HTTP** processes responses immediately within the same thread. As shown in lines 351‑384, the client inspects the `Content-Type` header to determine if the server returned a standard JSON response or an SSE stream:

```python
if "text/event-stream" in content_type:
    for sse in EventSource(response).iter_sse():
        message = json.loads(sse.data)
elif "application/json" in content_type:
    message = response.json()

```

For session continuity, `McpStreamableHttpClient` implements header‑based session tracking (lines 66‑69). It captures the `mcp-session-id` response header and replays it in subsequent requests via the `Mcp-Session-Id` header, maintaining state across otherwise stateless connections.

## Threading and Synchronization

Concurrency models separate the two implementations.

**`McpSseClient`** requires extensive synchronization primitives to coordinate between the listener thread and the main application thread. The class maintains multiple `threading.Event` objects:
- `_connected`: Signals that the endpoint URL has been discovered
- `response_ready`: Indicates a matching response has arrived
- `should_stop`: Coordinates graceful thread termination
- `_error_event`: Captures listener thread exceptions

Errors occurring in the background thread are stored in `_thread_exception` and re‑raised on the next main‑thread interaction (lines 43‑45, 48‑49), preventing silent failures.

**`McpStreamableHttpClient`** requires no threading primitives. All I/O is synchronous and blocking, with errors raised directly via `raise_for_status` and explicit status code checks (lines 63‑66). This eliminates race conditions and simplifies debugging at the cost of blocking the calling thread during network operations.

## Implementation Examples

### Using the SSE Client for Persistent Connections

When integrating with servers that push unsolicited updates or require long‑running bidirectional communication, instantiate `McpSseClient` directly:

```python
from utils.mcp_client import McpSseClient

sse_cfg = {
    "name": "my_sse_server",
    "url": "https://example.com/mcp/sse",
    "headers": {"Authorization": "Bearer <token>"},
    "timeout": 30,
    "sse_read_timeout": 30,
}

client = McpSseClient(**sse_cfg)  # Starts background listener thread

client.initialize()               # RPC initialize call

tools = client.list_tools()       # Fetch available tools

client.close()                    # Terminates listener thread

```

*Source:* Class definition in [`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/utils/mcp_client.py), lines 79‑132 (constructor and SSE logic).

### Using the Streamable HTTP Client for Stateless Operations

For simpler deployments where the server supports immediate JSON responses or single SSE events per request:

```python
from utils.mcp_client import McpStreamableHttpClient

http_cfg = {
    "name": "my_http_server",
    "url": "https://example.com/mcp/http",
    "headers": {"Authorization": "Bearer <token>"},
    "timeout": 30,
}

client = McpStreamableHttpClient(**http_cfg)  # No threading overhead

client.initialize()
tools = client.list_tools()
client.close()

```

*Source:* Implementation in [`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/utils/mcp_client.py), lines 331‑410.

### Factory‑Based Transport Selection

The `McpClients` factory class (lines 44‑64) automatically instantiates the appropriate client based on the `transport` configuration key:

```python
from utils.mcp_client import McpClients

servers_cfg = {
    "mcpServers": {
        "sse_srv": {"url": "https://sse.example.com/mcp", "transport": "sse"},
        "http_srv": {"url": "https://http.example.com/mcp", "transport": "streamable_http"},
    }
}

clients = McpClients(servers_cfg)

```

This abstraction allows the Dify plugin to handle both transport types without code changes to the tool provider logic in [`provider/mcp_tool.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/provider/mcp_tool.py).

## Summary

- **Connection Model**: `McpSseClient` maintains persistent SSE connections with background threads; `McpStreamableHttpClient` uses ephemeral HTTP POST requests.
- **Endpoint Resolution**: SSE clients dynamically discover RPC URLs via `"endpoint"` events; HTTP clients use static configuration.
- **State Management**: SSE relies on in‑memory message id matching; HTTP uses optional `Mcp-Session-Id` headers for session continuity.
- **Resource Overhead**: SSE requires thread synchronization primitives and long‑held sockets; HTTP offers lower overhead with synchronous, blocking calls.
- **Error Handling**: SSE captures background thread exceptions for later re‑raising; HTTP raises errors immediately from response status codes.

## Frequently Asked Questions

### When should I use McpSseClient versus McpStreamableHttpClient?

Choose **`McpSseClient`** when your MCP server pushes unsolicited messages, requires real‑time bidirectional communication, or implements long‑running tool calls that stream partial results. Select **`McpStreamableHttpClient`** for simpler request‑response patterns, serverless deployments, or environments where maintaining persistent connections consumes excessive resources.

### Does the Streamable HTTP client support Server‑Sent Events?

Yes. According to the implementation in [`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-tools-mcp_sse/blob/main/utils/mcp_client.py) (lines 351‑384), `McpStreamableHttpClient` detects `text/event-stream` content types and parses SSE events from the response body. However, unlike the SSE client, it processes these events synchronously within the request lifecycle rather than maintaining a persistent listener.

### How does session management differ between the two clients?

`McpSseClient` does not utilize HTTP‑level session identifiers; it correlates messages using the JSON‑RPC `id` field within the `message_dict`. Conversely, `McpStreamableHttpClient` explicitly tracks the `mcp-session-id` response header (lines 66‑69) and includes it as `Mcp-Session-Id` in subsequent requests, enabling stateful interactions across discrete HTTP calls.

### Can both clients handle the same MCP server endpoints?

Not interchangeably. An MCP server exposing an SSE endpoint typically requires the `McpSseClient` to handle the initial endpoint discovery handshake and persistent connection. Servers implementing the Streamable HTTP transport expect immediate POST requests without the preceding SSE negotiation. Always match the client type to the server’s advertised transport protocol in your Dify plugin configuration.