# McpSseClient vs McpStreamableHttpClient: Connection Lifecycle Differences Explained

> Compare McpSseClient and McpStreamableHttpClient connection lifecycles. Discover persistent SSE streams versus per-request HTTP POST connections and their implications.

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

---

**McpSseClient maintains a single persistent Server-Sent Events (SSE) stream with a dedicated background listener thread for the entire session, while McpStreamableHttpClient creates independent HTTP POST connections per request with optional session ID reuse but no persistent connection.**

The `junjiem/dify-plugin-agent-mcp_sse` repository provides two distinct client implementations for the Model Context Protocol (MCP). Both classes implement the abstract `McpClient` interface, but they handle the **connection lifecycle** through fundamentally different architectural patterns. Understanding these differences is critical for selecting the appropriate transport for your MCP integration.

## Connection Model: Persistent Stream vs Per-Request HTTP

### McpSseClient: Long-Lived SSE Connection

`McpSseClient` establishes a **single persistent SSE stream** at construction time that remains active for the entire client lifetime. In [`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-agent-mcp_sse/blob/main/utils/mcp_client.py), the `__init__` method creates an `httpx.Client` and immediately invokes `self.connect()`, which spawns a daemon listener thread (`_listen_thread`) running the `_listen_messages` method 【81†L81-L102】.

This background thread continuously reads SSE events via `httpx_sse.connect_sse`, processing special `"endpoint"` events to negotiate the JSON-RPC endpoint URL and `"message"` events to handle incoming responses 【108†L108-L132】【128†L128-L138】. All subsequent JSON-RPC calls are posted to the negotiated endpoint, while the SSE connection remains open to receive asynchronous server pushes.

### McpStreamableHttpClient: Stateless HTTP Posts

`McpStreamableHttpClient` operates on a **per-request basis** with no persistent connection. The `__init__` method in [`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-agent-mcp_sse/blob/main/utils/mcp_client.py) only instantiates a plain `httpx.Client` without spawning any background threads 【332†L332-L340】.

Every `send_message` call creates an **independent HTTP POST** request. If the server returns an `Mcp-Session-Id` header, the client stores it and includes it in subsequent requests to maintain session affinity, but the underlying TCP connection is closed after each response completes 【351†L351-L384】.

## Lifecycle Hooks and Thread Management

### McpSseClient Lifecycle

The lifecycle of `McpSseClient` involves explicit connection management and thread synchronization:

1. **Construction**: `__init__` → creates HTTP client → calls `connect()` → spawns listener thread
2. **Connection**: `connect()` blocks until the SSE thread signals `_connected` or raises an error stored in `_thread_exception`
3. **Operation**: `send_message()` posts to the negotiated endpoint and waits (`self.response_ready.wait()`) for the matching SSE `"message"` event 【48†L48-L78】【70†L70-L78】
4. **Termination**: `close()` sets a stop flag, shuts down the HTTP client, and joins the listener thread to ensure clean shutdown 【96†L96-L103】

### McpStreamableHttpClient Lifecycle

The lifecycle is significantly simpler with no background thread management:

1. **Construction**: `__init__` only creates the `httpx.Client` instance
2. **Connection**: No explicit `connect()`; the first request establishes a connection on-the-fly
3. **Operation**: Each `send_message` call executes a fresh HTTP POST; if the response `Content-Type` is `text/event-stream`, it iterates over embedded SSE events locally within the request context 【351†L351-L384】
4. **Termination**: `close()` simply closes the underlying `httpx.Client` without thread joining concerns 【45†L45-L48】

## Error Handling and Reconnection Strategies

Errors in `McpSseClient` manifest in the listener thread and are captured in `_thread_exception`, then re-raised by `connect()` or `send_message()`. Because the SSE connection is persistent, network interruptions require manual client recreation to re-establish the stream and re-negotiate the endpoint.

`McpStreamableHttpClient` returns errors as standard HTTP exceptions (e.g., `httpx.HTTPStatusError`) from individual request calls. Since there is no long-running thread, transient failures affect only the specific request, and the next `send_message` call automatically attempts a fresh connection without explicit reconnection logic.

## Code Examples: Lifecycle in Practice

### Using the SSE Client with Persistent Connection

```python
from utils.mcp_client import McpSseClient

# Create a client that will connect immediately (SSE stream stays alive)

sse_client = McpSseClient(
    name="my_sse",
    url="https://example.com/mcp/sse",
    headers={"Authorization": "Bearer <token>"},
    timeout=30,
    sse_read_timeout=30,
)

# Perform the standard JSON-RPC initialise handshake

sse_client.initialize()

# Call a tool – the request is POSTed, response arrives via the SSE thread

result = sse_client.call_tool("search", {"query": "weather today"})
print(result)

# When finished, shut down the persistent connection cleanly

sse_client.close()

```

*Key lifecycle points:* `__init__` → `connect()` (spawns listener) → `initialize()` → `call_tool()` (waits for SSE event) → `close()` (stops thread).

### Using the Streamable HTTP Client with Per-Request Connections

```python
from utils.mcp_client import McpStreamableHttpClient

http_client = McpStreamableHttpClient(
    name="my_http",
    url="https://example.com/mcp/http",
    headers={"Authorization": "Bearer <token>"},
    timeout=30,
)

# Initialise – each call creates its own HTTP request

http_client.initialize()

# Call a tool – a single POST/response cycle; no background thread

result = http_client.call_tool("search", {"query": "weather today"})
print(result)

# Close the underlying HTTP client (no thread to join)

http_client.close()

```

*Key lifecycle points:* `__init__` (no connection) → `initialize()` (first request) → each `send_message` = fresh HTTP POST → optional `session_id` reuse → `close()`.

### Creating Clients via the High-Level Helper

```python
from utils.mcp_client import McpClients

config = {
    "mcpServers": {
        "sse_server": {
            "url": "https://example.com/mcp/sse",
            "transport": "sse",
            "headers": {"Authorization": "Bearer <token>"}
        },
        "http_server": {
            "url": "https://example.com/mcp/http",
            "transport": "streamable_http"
        }
    }
}

clients = McpClients(servers_config=config)

# All servers are initialised automatically (SSE server starts a listener thread)

tools = clients.fetch_tools()
print(tools)

# Use a tool from the SSE server

output = clients.execute_tool("sse_server__search", {"query": "AI news"})
print(output)

clients.close()   # gracefully shuts down both the SSE listener and the HTTP client

```

## Key Implementation Files

| File | Purpose |
|------|---------|
| **[`utils/mcp_client.py`](https://github.com/junjiem/dify-plugin-agent-mcp_sse/blob/main/utils/mcp_client.py)** | Implements `McpSseClient`, `McpStreamableHttpClient`, and the shared `McpClient` abstraction. All lifecycle logic lives here. |
| **[`provider/agent.yaml`](https://github.com/junjiem/dify-plugin-agent-mcp_sse/blob/main/provider/agent.yaml)** | Example configuration that selects the transport (`sse` vs `streamable_http`) for each server, driving which client class is instantiated. |
| **[`strategies/base.py`](https://github.com/junjiem/dify-plugin-agent-mcp_sse/blob/main/strategies/base.py)** | Shows how `McpClients` is used by the plugin's strategy layer, indirectly exercising both client types. |
| **[`README.md`](https://github.com/junjiem/dify-plugin-agent-mcp_sse/blob/main/README.md)** | High-level documentation of the plugin; contains diagrams of the SSE vs HTTP flow. |

## Summary

- **McpSseClient** maintains a **single persistent SSE stream** throughout the session, spawning a background listener thread at initialization that continuously reads server events and handles asynchronous message delivery.
- **McpStreamableHttpClient** operates on a **per-request basis** without background threads, creating independent HTTP POST connections for each JSON-RPC call and optionally reusing session IDs across requests.
- **Lifecycle complexity** differs significantly: SSE clients require explicit connection management, thread synchronization, and graceful shutdown of the listener thread, while HTTP clients have simpler initialization and teardown with no thread management overhead.
- **Error handling** in SSE clients captures thread exceptions for re-raising, requiring client recreation on connection failures, whereas HTTP clients treat errors as standard request exceptions with automatic fresh connections on subsequent calls.

## Frequently Asked Questions

### What happens if the SSE connection drops in McpSseClient?

If the SSE connection drops, the background listener thread (`_listen_thread`) captures the exception in `_thread_exception`. This error is re-raised by subsequent calls to `connect()` or `send_message()`. Unlike the HTTP client, the SSE client does not automatically reconnect; you must recreate the `McpSseClient` instance to re-establish the stream and re-negotiate the endpoint URL.

### Can McpStreamableHttpClient maintain state across multiple tool calls?

Yes, but only partially. While `McpStreamableHttpClient` does not maintain a persistent TCP connection, it can reuse a session identifier. If the server returns an `Mcp-Session-Id` header in a response, the client stores it and includes this header in subsequent requests. However, each `send_message` call still creates a fresh HTTP connection, and there is no background thread listening for server-initiated events outside of active requests.

### Which client should I use for long-running tool executions?

Use **McpSseClient** for long-running or asynchronous operations. Because it maintains a persistent SSE stream with a dedicated listener thread, it can receive server-pushed messages and progress updates at any time without the client polling. The `McpStreamableHttpClient` is better suited for quick request/response interactions where the server returns results immediately within a single HTTP response, potentially using SSE only within that specific response body rather than as a persistent transport.

### How do I properly shut down an McpSseClient to avoid thread leaks?

Always call `close()` on your `McpSseClient` instance when finished. This method sets an internal stop flag to signal the background listener thread (`_listen_thread`) to exit, closes the underlying `httpx.Client`, and then joins the listener thread to ensure it terminates cleanly 【96†L96-L103】. Failing to call `close()` will leave the daemon thread running, potentially causing resource leaks or preventing clean application shutdown. For `McpStreamableHttpClient`, `close()` simply shuts down the HTTP client without thread management concerns.