How the McpClients Factory Determines Transport Implementation in Dify MCP
The McpClients factory determines which transport implementation to instantiate by inspecting the transport field in each server's configuration, defaulting to McpSseClient when the value is "sse" or undefined, and instantiating McpStreamableHttpClient only when the configuration explicitly specifies "streamable_http".
The junjiem/dify-plugin-tools-mcp_sse repository implements a factory pattern that abstracts transport-specific details for MCP (Model Context Protocol) connections. This design allows Dify plugins to communicate with MCP servers through either Server-Sent Events (SSE) or HTTP transports without changing the underlying business logic. The factory centralizes transport selection logic in a single configuration-driven method, making it trivial to switch protocols by editing YAML configuration rather than code.
Transport Selection Logic
The factory's decision process follows a strict validation and branching sequence defined in utils/mcp_client.py. Each server entry in the configuration undergoes name validation before transport selection occurs.
Configuration Schema and Defaults
Every server configuration must include a transport field that dictates which concrete client class the factory instantiates. If the field is omitted, the factory falls back to the string value "sse". The configuration structure follows this pattern:
- Server name: Must match the regex pattern
^[a-zA-Z0-9_-]+$(alphanumeric, underscores, and hyphens only) - Transport value: Either
"sse"(default),"streamable_http", or potentially other custom values - URL and headers: Transport-specific connection parameters
The Decision Branch in init_client
The static method McpClients.init_client (lines 44-66 in utils/mcp_client.py) implements the selection logic through a simple conditional branch:
- Extract transport: Read the
transportfield from the server configuration dictionary - Compare value: Check if the string equals
"streamable_http" - Instantiate: Create
McpStreamableHttpClientfor HTTP transport, orMcpSseClientfor any other value including the default"sse"
The chosen client is automatically initialized via client.initialize() and stored in the factory's internal _clients dictionary for later retrieval.
Factory Implementation Details
Understanding the internal mechanics of McpClients reveals how the plugin maintains clean separation between transport protocols while providing a unified interface.
Client Instantiation Flow
When McpClients receives its configuration (typically from Dify's settings or a YAML file), it iterates through the mcpServers dictionary and calls init_client for each entry. The method validates required parameters before instantiation:
@staticmethod
def init_client(server_name: str, server_config: dict):
# Validation: server_name must match ^[a-zA-Z0-9_-]+$
# Extraction: transport = server_config.get("transport", "sse")
# Branching: if transport == "streamable_http" → McpStreamableHttpClient
# else → McpSseClient
After instantiation, the factory immediately invokes initialize() on the new client, establishing the connection to the MCP server before the client enters the internal registry.
Storage and Retrieval
The factory maintains a private dictionary _clients that maps server names to their initialized transport instances. This allows the plugin tools (mcp_list_tools.py, mcp_call_tool.py) to retrieve clients by string name without knowing the underlying transport type:
# Accessing stored clients
sse_client = clients._clients["my_sse_server"] # McpSseClient instance
http_client = clients._clients["my_http_server"] # McpStreamableHttpClient instance
Configuration Examples
Practical deployment scenarios demonstrate how the factory interprets different configuration values to instantiate the correct transport implementation.
Default SSE Transport
When the transport field is omitted, the factory defaults to SSE. This configuration creates an McpSseClient instance:
servers_config = {
"mcpServers": {
"filesystem_server": {
"url": "http://localhost:3000/mcp/sse",
# transport not specified → defaults to "sse"
}
}
}
clients = McpClients(servers_config)
# Results in _clients["filesystem_server"] = McpSseClient(...)
Explicit Streamable HTTP
To instantiate McpStreamableHttpClient, the configuration must explicitly declare the transport type. This is required for HTTP-based MCP servers:
servers_config = {
"mcpServers": {
"remote_api_server": {
"url": "https://api.example.com/mcp",
"transport": "streamable_http",
"headers": {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
}
}
}
clients = McpClients(servers_config)
# Results in _clients["remote_api_server"] = McpStreamableHttpClient(...)
Extending the Factory Pattern
The centralized transport selection in init_client makes the codebase extensible. Adding support for a hypothetical WebSocket transport would require only a new conditional branch:
if transport == "streamable_http":
return McpStreamableHttpClient(server_name, server_config)
elif transport == "websocket":
return McpWebSocketClient(server_name, server_config)
else:
return McpSseClient(server_name, server_config) # default
Because McpClient serves as an abstract base class, new transport implementations only need to satisfy the interface contract. The factory would automatically route configuration requests to the new class without modifying calling code in tools/mcp_list_tools.py or tools/mcp_call_tool.py.
Summary
- The
McpClientsfactory determines transport implementation by reading thetransportconfiguration field and choosing betweenMcpSseClientandMcpStreamableHttpClient. - Default behavior routes all undefined or
"sse"values to the SSE transport, while"streamable_http"triggers the HTTP client. - Validation occurs before instantiation: server names must match
^[a-zA-Z0-9_-]+$according to the logic inutils/mcp_client.pylines 44-66. - Automatic initialization happens immediately after instantiation via
client.initialize(), storing the result in the_clientsregistry. - Extension requires only adding new conditional branches to
init_client, following the existing pattern injunjiem/dify-plugin-tools-mcp_sse.
Frequently Asked Questions
What happens if I specify an unsupported transport name in the configuration?
The factory treats any unknown transport value as the default SSE implementation. If you specify "websocket" or "grpc" without modifying utils/mcp_client.py, the factory will instantiate McpSseClient rather than raising an error. This fallback behavior ensures backward compatibility but requires explicit implementation for new protocols.
Where is the transport selection logic located in the source code?
According to the junjiem/dify-plugin-tools-mcp_sse repository, the transport selection logic resides in utils/mcp_client.py between lines 44 and 66. The static method init_client contains the conditional branching that compares the configuration's transport field against the string "streamable_http" to determine which concrete class to instantiate.
Can I use both SSE and Streamable HTTP transports in the same Dify plugin instance?
Yes. The factory supports heterogeneous transport configurations within a single McpClients instance. You can define multiple servers in the mcpServers configuration object, with some using "sse" (or omitting the field) and others explicitly using "streamable_http". The factory creates the appropriate client type for each server and stores them in the _clients dictionary under their respective server names.
How does the factory handle client initialization errors?
The factory invokes client.initialize() immediately after instantiation within the init_client method. If the initialization fails (e.g., connection refused, authentication failure), the exception propagates up to the caller because there is no try-catch block in the standard implementation. This ensures that configuration errors surface immediately during plugin startup rather than during tool execution.
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 →