Repository navigation
feat(mcp): add SSE transport fallback and keepalive for HTTP MCP servers #3976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -89,6 +89,7 @@ | |||||||||
|
|
||||||||||
| _MCP_AVAILABLE = False | ||||||||||
| _MCP_HTTP_AVAILABLE = False | ||||||||||
| _MCP_SSE_AVAILABLE = False | ||||||||||
| _MCP_SAMPLING_TYPES = False | ||||||||||
| _MCP_NOTIFICATION_TYPES = False | ||||||||||
| _MCP_MESSAGE_HANDLER_SUPPORTED = False | ||||||||||
|
|
@@ -101,6 +102,11 @@ | |||||||||
| _MCP_HTTP_AVAILABLE = True | ||||||||||
| except ImportError: | ||||||||||
| _MCP_HTTP_AVAILABLE = False | ||||||||||
| try: | ||||||||||
| from mcp.client.sse import sse_client | ||||||||||
| _MCP_SSE_AVAILABLE = True | ||||||||||
| except ImportError: | ||||||||||
| _MCP_SSE_AVAILABLE = False | ||||||||||
| # Prefer the non-deprecated API (mcp >= 1.24.0); fall back to the | ||||||||||
| # deprecated wrapper for older SDK versions. | ||||||||||
| try: | ||||||||||
|
|
@@ -851,12 +857,16 @@ async def _run_stdio(self, config: dict): | |||||||||
| await self._shutdown_event.wait() | ||||||||||
|
|
||||||||||
| async def _run_http(self, config: dict): | ||||||||||
| """Run the server using HTTP/StreamableHTTP transport.""" | ||||||||||
| if not _MCP_HTTP_AVAILABLE: | ||||||||||
| """Run the server using HTTP transport. | ||||||||||
|
|
||||||||||
| Tries Streamable HTTP first; if that fails (e.g. server only | ||||||||||
| supports the legacy SSE protocol), falls back to SSE transport. | ||||||||||
| """ | ||||||||||
| if not _MCP_HTTP_AVAILABLE and not _MCP_SSE_AVAILABLE: | ||||||||||
| raise ImportError( | ||||||||||
| f"MCP server '{self.name}' requires HTTP transport but " | ||||||||||
| "mcp.client.streamable_http is not available. " | ||||||||||
| "Upgrade the mcp package to get HTTP support." | ||||||||||
| "neither mcp.client.streamable_http nor mcp.client.sse " | ||||||||||
| "is available. Upgrade the mcp package to get HTTP support." | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| url = config["url"] | ||||||||||
|
|
@@ -876,6 +886,44 @@ async def _run_http(self, config: dict): | |||||||||
| if _MCP_NOTIFICATION_TYPES and _MCP_MESSAGE_HANDLER_SUPPORTED: | ||||||||||
| sampling_kwargs["message_handler"] = self._make_message_handler() | ||||||||||
|
|
||||||||||
| # Try Streamable HTTP first | ||||||||||
| if _MCP_HTTP_AVAILABLE: | ||||||||||
| try: | ||||||||||
| await self._run_http_streamable( | ||||||||||
| url, headers, connect_timeout, _oauth_auth, sampling_kwargs | ||||||||||
| ) | ||||||||||
| return | ||||||||||
|
||||||||||
| return | |
| return | |
| except asyncio.CancelledError: | |
| raise |
Copilot
AI
Apr 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MCPServerTask._is_sse() is referenced here, but no such method is defined on the class. This will raise AttributeError at runtime (and will also break the newly added tests). Add an _is_sse() helper (e.g., check config.get('transport') == 'sse' or whether the parsed URL path ends with /sse, handling trailing slashes and query params) and keep the behavior consistent with the tests.
Copilot
AI
Apr 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_mcp_status() instantiates MCPServerTask just to detect SSE vs HTTP, which pulls in asyncio primitives and duplicates runtime transport detection logic. Consider extracting the SSE detection into a small pure helper (or @staticmethod) that operates directly on the config dict/URL so status rendering doesn’t need to construct a full server task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New behavior: Streamable HTTP now falls back to SSE on failure, but there are no tests asserting (1)
_run_httpcalls the SSE path when Streamable HTTP raises (e.g.,McpError('Session terminated')) and (2) the original exception is raised when SSE is unavailable. Adding focused unit tests around this branching will help prevent regressions.