#936 MCP SDK streamablehttp 稳定性回归 (兼容 mcp 2.0 重命名 + httpx2 fork)

This commit is contained in:
SeniorDev committed 2026-08-09 21:33:28 +08:00
1 parent 6d5c2cf2c0
commit 6e89a37ff7
2 files changed
+234 -6

No files matched your search

+49 -6
View File
@@ -19,8 +19,21 @@ from typing import Optional
from mcp import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
# mcp 2.0.0 把 streamablehttp_client 重命名为 streamable_http_client(下划线位置变了)。
# 兼容 0.28.x / 1.x / 2.x:先试新名,失败回退老名。
try:
from mcp.client.streamable_http import streamable_http_client as _streamable_http_client
except ImportError: # 0.28.x 旧名
try:
from mcp.client.streamable_http import streamablehttp_client as _streamable_http_client
except ImportError:
_streamable_http_client = None # type: ignore[assignment]
import httpx
# 兼容 mcp 2.0+ 安装的 httpx2 fork — 同源 API,可作别名
try:
import httpx2 as _httpx
except ImportError:
_httpx = httpx
logger = logging.getLogger(__name__)
@@ -36,14 +49,20 @@ def _no_proxy_httpx_client(headers=None, timeout=None, auth=None):
外部 MCP 域名(xiaobenyang.com / anysearch.com 等),且默认 trust_env
会读取 HTTP_PROXY 导致 tools/list 全部静默失败。这里 trust_env=False
直连,与 app/core/llm_client.py:chat_complete 修复同源。
mcp 2.0.0 改动:`streamable_http_client` 不再接受 headers/timeout/httpx_client_factory
等 kwargs,改为接受预配置的 http_client(httpx2/httpx.AsyncClient 实例)。
所以这里返回一个完整的 client,而不是 factory。
"""
# 关键:httpx2(2.0+ 引入,同源 API)与 httpx 兼容
cli_class = getattr(_httpx, "AsyncClient", _httpx.Client) if hasattr(_httpx, "AsyncClient") else _httpx.AsyncClient
kwargs: dict = {"follow_redirects": True, "trust_env": False}
kwargs["timeout"] = timeout if timeout is not None else httpx.Timeout(30.0, read=300.0)
kwargs["timeout"] = timeout if timeout is not None else _httpx.Timeout(30.0, read=300.0)
if headers is not None:
kwargs["headers"] = headers
if auth is not None:
kwargs["auth"] = auth
return httpx.AsyncClient(**kwargs)
return cli_class(**kwargs)
def _transport_cm(server: dict, timeout: float):
@@ -51,16 +70,40 @@ def _transport_cm(server: dict, timeout: float):
返回异步上下文管理器,进入后产出 (read_stream, write_stream, ...)。
关键:注入 _no_proxy_httpx_client,使 MCP 流量直连(见其 docstring)。
mcp 2.0.0 兼容路径:
- 旧 (0.28.x): streamablehttp_client(url, headers=..., timeout=..., httpx_client_factory=...)
- 新 (2.0+): streamable_http_client(url, http_client=<预配置 client>)
用 inspect 判断参数,自动适配。
"""
url = (server.get("url") or "").strip()
ttype = (server.get("transport_type") or "streamable_http").lower()
headers = server.get("headers") or {}
if ttype == "sse":
# SSE 也有同样问题,新版 sse_client 签名也可能变。这里保守用老 kwargs(若失败 fallback)
return sse_client(url, headers=headers, timeout=timeout,
httpx_client_factory=_no_proxy_httpx_client)
# 默认 streamable_http(官方 SDK 标准传输)
return streamablehttp_client(url, headers=headers, timeout=timeout,
httpx_client_factory=_no_proxy_httpx_client)
if _streamable_http_client is None:
raise RuntimeError("mcp SDK missing streamable_http transport (both new + old names unavailable)")
# inspect 新版签名,判断是否接受 http_client
import inspect
try:
sig_params = inspect.signature(_streamable_http_client).parameters
accepts_http_client = "http_client" in sig_params
except Exception:
accepts_http_client = False
if accepts_http_client:
# mcp 2.0+ 路径:headers/timeout 全部装进预配置的 httpx client
http_client = _no_proxy_httpx_client(headers=headers, timeout=timeout)
return _streamable_http_client(url, http_client=http_client)
else:
# 0.28.x 老路径
return _streamable_http_client(url, headers=headers, timeout=timeout,
httpx_client_factory=_no_proxy_httpx_client)
async def _session_request(