chore: upgrade Pipecat to 1.13.0 and adapt MCP 2.x

This commit is contained in:
Xin Wang
2026-10-23 14:03:42 +08:00
parent 81a6fdc30c
commit d45be1e686
4 changed files with 95 additions and 15 deletions
+3 -1
View File
@@ -2,7 +2,9 @@
# webrtc -> SmallWebRTCTransport / SmallWebRTCConnection + aiortc
# silero -> 本地 VAD(判断用户说话起止),语音必备
# openai -> OpenAI 兼容的 LLM/STT/TTS 客户端(DeepSeek、SenseVoice、CosyVoice 都走它)
pipecat-ai[webrtc,websocket,silero,openai,mcp]==1.7.0
pipecat-ai[webrtc,websocket,silero,openai,mcp]==1.13.0
# MCP 2.x 使用 httpx2;普通业务 HTTP 请求继续使用 httpx。
httpx2>=2.10.0,<3
Pillow>=11.1.0,<13
# FastGPT 类型助手:本地 SDK(包 /api/v1/chat/completions 流式 + chatId 会话)
+12 -10
View File
@@ -10,13 +10,13 @@ from __future__ import annotations
import hashlib
import json
from contextlib import asynccontextmanager
from datetime import timedelta
from typing import Any, AsyncIterator
import httpx
import httpx2 as httpx
from mcp import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import PaginatedRequestParams
from models import RuntimeMcpServer
@@ -59,7 +59,7 @@ class McpToolClient:
server.url,
http_client=http_client,
) as streams:
read_stream, write_stream, _ = streams
read_stream, write_stream = streams
yield read_stream, write_stream
return
@@ -88,7 +88,7 @@ class McpToolClient:
async with ClientSession(
read_stream,
write_stream,
read_timeout_seconds=timedelta(seconds=timeout),
read_timeout_seconds=timeout,
) as session:
await session.initialize()
yield session
@@ -105,9 +105,11 @@ class McpToolClient:
async with self._session(server) as session:
cursor: str | None = None
while True:
result = await session.list_tools(cursor=cursor)
result = await session.list_tools(
params=PaginatedRequestParams(cursor=cursor) if cursor else None,
)
for tool in result.tools:
input_schema = dict(tool.inputSchema or {})
input_schema = dict(tool.input_schema or {})
discovered.append(
{
"name": tool.name,
@@ -116,7 +118,7 @@ class McpToolClient:
"schema_hash": schema_hash(input_schema),
}
)
cursor = result.nextCursor
cursor = result.next_cursor
if not cursor:
break
return discovered
@@ -132,14 +134,14 @@ class McpToolClient:
result = await session.call_tool(
remote_tool_name,
arguments=arguments,
read_timeout_seconds=timedelta(seconds=server.timeout_seconds),
read_timeout_seconds=server.timeout_seconds,
)
data = self._normalize_result(result)
encoded = json.dumps(data, ensure_ascii=False, default=str).encode("utf-8")
if len(encoded) > MAX_MCP_RESULT_BYTES:
raise McpClientError("MCP 工具响应超过 1 MB 限制")
if bool(result.isError):
if bool(result.is_error):
message = data.get("text") or "MCP 工具返回执行错误"
raise McpClientError(str(message)[:2048])
return data
@@ -171,6 +173,6 @@ class McpToolClient:
return {
"text": "\n".join(text_parts),
"structuredContent": result.structuredContent,
"structuredContent": result.structured_content,
"content": content,
}
+77 -1
View File
@@ -1,13 +1,89 @@
import json
import unittest
from contextlib import asynccontextmanager
from unittest.mock import patch
import httpx2
from models import RuntimeMcpServer
from schemas import McpServerUpsert
from services.tools.mcp_client import McpToolClient
from services.tools.mcp_client import McpClientError, McpToolClient
class McpTransportTests(unittest.IsolatedAsyncioTestCase):
async def test_streamable_http_discovery_and_call_with_real_sdk(self):
requests = []
def respond(request):
self.assertEqual(request.headers["Authorization"], "Bearer test")
message = json.loads(request.content)
requests.append(message)
if "id" not in message:
return httpx2.Response(202)
if message["method"] == "initialize":
result = {
"protocolVersion": "2025-06-18",
"capabilities": {"tools": {}},
"serverInfo": {"name": "test", "version": "1"},
}
elif message["method"] == "tools/list":
if (message.get("params") or {}).get("cursor") == "page2":
result = {"tools": []}
else:
result = {
"tools": [{
"name": "get_order",
"inputSchema": {"type": "object", "properties": {}},
}],
"nextCursor": "page2",
}
elif message["method"] == "tools/call":
result = {
"content": [{"type": "text", "text": "已发货"}],
"structuredContent": {"status": "shipped"},
}
else:
self.fail(f"Unexpected MCP method: {message['method']}")
return httpx2.Response(200, json={
"jsonrpc": "2.0", "id": message["id"], "result": result,
})
client_class = httpx2.AsyncClient
def client_factory(**kwargs):
return client_class(transport=httpx2.MockTransport(respond), **kwargs)
server = RuntimeMcpServer(
id="mcp_http",
url="https://mcp.example.com/mcp",
secret_headers={"Authorization": "Bearer test"},
)
client = McpToolClient()
with patch("services.tools.mcp_client.httpx.AsyncClient", client_factory):
tools = await client.list_tools(server)
listings = [item for item in requests if item["method"] == "tools/list"]
self.assertEqual(len(listings), 2)
self.assertEqual(listings[1]["params"]["cursor"], "page2")
result = await client.call_tool(server, "get_order", {"order_id": "A-100"})
self.assertEqual(tools[0]["name"], "get_order")
self.assertEqual(result["text"], "已发货")
self.assertEqual(result["structuredContent"], {"status": "shipped"})
call = next(item for item in requests if item["method"] == "tools/call")
self.assertEqual(call["params"]["arguments"], {"order_id": "A-100"})
async def test_sdk_timeout_has_readable_error(self):
@asynccontextmanager
async def timed_out(*args):
raise httpx2.ReadTimeout("read timed out")
yield
server = RuntimeMcpServer(id="mcp_timeout", url="https://mcp.example.com/mcp")
client = McpToolClient()
with patch.object(client, "_transport_streams", timed_out):
with self.assertRaisesRegex(McpClientError, "超时"):
await client.list_tools(server)
def test_server_schema_accepts_sse(self):
server = McpServerUpsert(
name="旧版 MCP 服务",
+3 -3
View File
@@ -13,8 +13,8 @@ from services.pipecat.service_factory import (
)
class LLMServiceFactoryTest(unittest.TestCase):
def test_openai_compatible_llm_converts_async_tool_developer_result(self):
class LLMServiceFactoryTest(unittest.IsolatedAsyncioTestCase):
async def test_openai_compatible_llm_converts_async_tool_developer_result(self):
service = create_llm(
AssistantConfig(
llm_interface_type="openai-llm",
@@ -34,7 +34,7 @@ class LLMServiceFactoryTest(unittest.TestCase):
]
)
params = service.get_llm_adapter().get_llm_invocation_params(
params = await service.get_llm_adapter().get_llm_invocation_params(
context,
convert_developer_to_user=not service.supports_developer_role,
)