McpSseClient vs McpStreamableHttpClient: Connection Lifecycle Differences Explained
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, 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 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:
- Construction:
__init__→ creates HTTP client → callsconnect()→ spawns listener thread - Connection:
connect()blocks until the SSE thread signals_connectedor raises an error stored in_thread_exception - 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】 - 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:
- Construction:
__init__only creates thehttpx.Clientinstance - Connection: No explicit
connect(); the first request establishes a connection on-the-fly - Operation: Each
send_messagecall executes a fresh HTTP POST; if the responseContent-Typeistext/event-stream, it iterates over embedded SSE events locally within the request context 【351†L351-L384】 - Termination:
close()simply closes the underlyinghttpx.Clientwithout 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
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
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
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 |
Implements McpSseClient, McpStreamableHttpClient, and the shared McpClient abstraction. All lifecycle logic lives here. |
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 |
Shows how McpClients is used by the plugin's strategy layer, indirectly exercising both client types. |
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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →