Architectural Differences Between McpSseClient and McpStreamableHttpClient in Dify MCP Tools
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 (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 (lines 206‑215), the listener extracts the real RPC URL from this event:
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:
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:
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 discoveredresponse_ready: Indicates a matching response has arrivedshould_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:
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, 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:
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, 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:
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.
Summary
- Connection Model:
McpSseClientmaintains persistent SSE connections with background threads;McpStreamableHttpClientuses 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-Idheaders 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 (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.
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 →