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:

  1. Extract transport: Read the transport field from the server configuration dictionary
  2. Compare value: Check if the string equals "streamable_http"
  3. Instantiate: Create McpStreamableHttpClient for HTTP transport, or McpSseClient for 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 McpClients factory determines transport implementation by reading the transport configuration field and choosing between McpSseClient and McpStreamableHttpClient.
  • 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 in utils/mcp_client.py lines 44-66.
  • Automatic initialization happens immediately after instantiation via client.initialize(), storing the result in the _clients registry.
  • Extension requires only adding new conditional branches to init_client, following the existing pattern in junjiem/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →