Skip to content

feat(mcp): Add Streamable HTTP transport support for remote MCP providers #357

@nabinchha

Description

@nabinchha

Summary

MCPProvider only supports SSE (Server-Sent Events) transport for remote MCP servers. The MCP ecosystem is moving toward Streamable HTTP as the standard remote transport, and many popular MCP servers (e.g. Tavily's remote endpoint) already use it exclusively. Connecting to these servers with the current MCPProvider silently hangs until the health-check timeout (up to 5 minutes), with no indication that the transport is wrong.

Problem

The MCPProvider config class hardcodes provider_type: Literal["sse"], and MCPIOService._get_or_create_session() unconditionally uses sse_client() for all non-stdio providers:

# engine/mcp/io.py, line 214-216
else:
    headers = _build_auth_headers(provider.api_key)
    ctx = sse_client(provider.endpoint, headers=headers)

When a user points this at a Streamable HTTP endpoint (e.g. https://mcp.tavily.com/mcp/), the SSE client opens a connection expecting an SSE event stream, receives nothing, and blocks until the full timeout_sec elapses (default 300s). The resulting MCPToolError gives no hint that the transport mismatch is the root cause.

Observed behavior:

[12:00:22] [INFO] 🧰 Running health checks for MCP tools...
[12:00:22] [INFO]   |-- 👀 Checking tools for tool alias 'tavily'...
[12:05:22] [ERROR]   |-- ❌ Failed!
MCPToolError: Timed out after 300.0s while listing tools on 'tavily'.

The mcp Python SDK (v1.26.0, already a dependency) ships mcp.client.streamable_http — it just isn't wired up.

Proposed Changes

1. Add Streamable HTTP transport to MCPProvider

Either generalize the existing MCPProvider or add a new provider type:

Option A — Extend MCPProvider (minimal change):

class MCPProvider(ConfigBase):
    provider_type: Literal["sse", "streamable_http"] = "sse"
    name: str
    endpoint: str
    api_key: str | None = None

Option B — New config class (preserves backward compat):

class StreamableHTTPMCPProvider(ConfigBase):
    provider_type: Literal["streamable_http"] = "streamable_http"
    name: str
    endpoint: str
    api_key: str | None = None

Then update the discriminated union:

MCPProviderT = Annotated[
    MCPProvider | StreamableHTTPMCPProvider | LocalStdioMCPProvider,
    Field(discriminator="provider_type"),
]

2. Wire up streamable_http_client in io.py

Add a branch in _get_or_create_session():

from mcp.client.streamable_http import streamablehttp_client

if isinstance(provider, LocalStdioMCPProvider):
    ctx = stdio_client(params)
elif provider.provider_type == "streamable_http":
    headers = _build_auth_headers(provider.api_key)
    ctx = streamablehttp_client(provider.endpoint, headers=headers)
else:
    headers = _build_auth_headers(provider.api_key)
    ctx = sse_client(provider.endpoint, headers=headers)

3. (Nice-to-have) Auto-negotiate transport with fallback

Try Streamable HTTP first, fall back to SSE if the handshake fails. This would let users write MCPProvider(endpoint=...) without needing to know which transport the server uses:

async def _connect_remote(self, provider):
    headers = _build_auth_headers(provider.api_key)
    try:
        ctx = streamablehttp_client(provider.endpoint, headers=headers)
        read, write = await ctx.__aenter__()
        return ctx, read, write
    except Exception:
        logger.info("Streamable HTTP failed for %r, falling back to SSE", provider.name)
        ctx = sse_client(provider.endpoint, headers=headers)
        read, write = await ctx.__aenter__()
        return ctx, read, write

4. Shorter health-check timeout for list_tools

The 5-minute hang on a wrong transport is painful. A dedicated health_check_timeout_sec (defaulting to ~30s) on the provider or tool config would catch misconfigurations quickly, independent of the timeout_sec used for actual tool calls during generation.

Environment

  • data-designer: 0.5.1
  • mcp SDK: 1.26.0 (already includes mcp.client.streamable_http)
  • Tested against: Tavily remote MCP (https://mcp.tavily.com/mcp/)

Workaround

Use LocalStdioMCPProvider with npx -y tavily-mcp@latest (or a custom Python MCP server) instead of the remote endpoint.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions