Skip to content

Commit ec56fb4

Browse files
committed
fix: 优化外部资源生命周期和 MCP streamable-http 兼容
- Runner 支持通过 close_session_service_on_close / close_memory_service_on_close 控制是否在 close 时关闭外部托管的 session_service 和 memory_service。 - MCP Streamable HTTP client 优先使用新的 streamable_http_client API,并在旧版 MCP SDK 下回退到 streamablehttp_client。 - 使用 httpx.AsyncClient 承载 streamable-http 的 headers / timeout 配置,避免调用已废弃接口。 - 补充 Runner 和 MCP session manager 相关测试。
1 parent 9ab326c commit ec56fb4

5 files changed

Lines changed: 87 additions & 13 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ classifiers = [
2626
dependencies = [
2727
"pydantic>=2.11.3",
2828
"openai>=1.3.0",
29-
"mcp<1.23.4,>=1.10.1",
29+
"mcp>=1.10.1",
3030
"aiohttp",
3131
"httpx>=0.27.0",
3232
"httpx-sse>=0.4.0",

tests/test_runner.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ def test_init_with_required_params(self, mock_agent, mock_session_service):
107107
assert runner.session_service == mock_session_service
108108
assert runner.artifact_service is None
109109
assert runner.memory_service is None
110+
assert runner._close_session_service_on_close is True
111+
assert runner._close_memory_service_on_close is True
110112

111113
def test_init_with_all_params(self, mock_agent, mock_session_service):
112114
"""Test Runner initialization with all parameters."""
@@ -119,10 +121,14 @@ def test_init_with_all_params(self, mock_agent, mock_session_service):
119121
session_service=mock_session_service,
120122
artifact_service=artifact_service,
121123
memory_service=memory_service,
124+
close_session_service_on_close=False,
125+
close_memory_service_on_close=False,
122126
)
123127

124128
assert runner.artifact_service == artifact_service
125129
assert runner.memory_service == memory_service
130+
assert runner._close_session_service_on_close is False
131+
assert runner._close_memory_service_on_close is False
126132

127133

128134
# Tests for run_async method
@@ -618,6 +624,25 @@ async def test_close_calls_all_services(self, runner, mock_session_service):
618624
mock_session_service.close.assert_called_once()
619625
memory_service.close.assert_called_once()
620626

627+
@pytest.mark.asyncio
628+
async def test_close_can_keep_external_services_open(self, mock_agent, mock_session_service):
629+
"""Test close skips externally managed session and memory services."""
630+
memory_service = AsyncMock(spec=BaseMemoryService)
631+
runner = Runner(
632+
app_name="test_app",
633+
agent=mock_agent,
634+
session_service=mock_session_service,
635+
memory_service=memory_service,
636+
close_session_service_on_close=False,
637+
close_memory_service_on_close=False,
638+
)
639+
640+
with patch.object(runner, '_collect_toolset', return_value=set()):
641+
await runner.close()
642+
643+
mock_session_service.close.assert_not_called()
644+
memory_service.close.assert_not_called()
645+
621646

622647
# Tests for session history handling
623648
class TestSessionHistory:

tests/tools/mcp_tool/test_mcp_session_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,11 @@ def test_sse_client(self, mock_sse):
233233
call_kwargs = mock_sse.call_args
234234
assert call_kwargs.kwargs["headers"] == {"Auth": "token"}
235235

236-
@patch("trpc_agent_sdk.tools.mcp_tool._mcp_session_manager.streamablehttp_client")
236+
@patch.object(MCPSessionManager, "_create_streamable_http_client")
237237
def test_streamable_client(self, mock_streamable):
238238
mgr = MCPSessionManager(connection_params=_streamable_conn())
239239
mgr._create_client({"Auth": "token"})
240-
mock_streamable.assert_called_once()
240+
mock_streamable.assert_called_once_with({"Auth": "token"})
241241

242242
def test_unsupported_params_raises(self):
243243
mgr = MCPSessionManager(connection_params=_stdio_conn())

trpc_agent_sdk/runners.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ def __init__(
194194
enable_post_turn_processing: bool = True,
195195
defer_post_turn_processing: bool = False,
196196
post_turn_queue_maxsize: int = 256,
197+
close_session_service_on_close: bool = True,
198+
close_memory_service_on_close: bool = True,
197199
):
198200
"""Initializes the Runner.
199201
@@ -209,6 +211,13 @@ def __init__(
209211
persistence run in a dedicated thread/event loop so request
210212
completion is not blocked by post-turn I/O/LLM latency.
211213
post_turn_queue_maxsize: Max buffered post-turn jobs per runner.
214+
close_session_service_on_close: Whether :meth:`close` should close
215+
the session service. Set to False when the service is managed
216+
outside the runner, e.g. an application-level Redis connection
217+
pool shared by many short-lived runners.
218+
close_memory_service_on_close: Whether :meth:`close` should close
219+
the memory service. Set to False when the service is managed
220+
outside the runner.
212221
"""
213222
self.app_name = app_name
214223
self.agent = agent
@@ -219,6 +228,8 @@ def __init__(
219228
self._defer_post_turn_processing = defer_post_turn_processing
220229
self._post_turn_thread: _PostTurnWorkerThread | None = None
221230
self._post_turn_queue_maxsize = max(1, int(post_turn_queue_maxsize))
231+
self._close_session_service_on_close = close_session_service_on_close
232+
self._close_memory_service_on_close = close_memory_service_on_close
222233

223234
async def _run_post_turn_processing(
224235
self,
@@ -823,7 +834,7 @@ async def close(self):
823834
"""
824835
await self._shutdown_post_turn_worker()
825836
await self._cleanup_toolsets(self._collect_toolset(self.agent))
826-
if self.session_service:
837+
if self.session_service and self._close_session_service_on_close:
827838
await self.session_service.close()
828-
if self.memory_service:
839+
if self.memory_service and self._close_memory_service_on_close:
829840
await self.memory_service.close()

trpc_agent_sdk/tools/mcp_tool/_mcp_session_manager.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,28 @@
2626

2727
import asyncio
2828
import hashlib
29+
import inspect
2930
import json
3031
import sys
3132
from contextlib import AsyncExitStack
33+
from contextlib import asynccontextmanager
3234
from datetime import timedelta
3335
from typing import Dict
3436
from typing import Optional
3537
from typing import Union
3638

39+
# cSpell:ignore streamable
40+
41+
import httpx
3742
from mcp import ClientSession
3843
from mcp.client.sse import sse_client
3944
from mcp.client.stdio import stdio_client
40-
from mcp.client.streamable_http import streamablehttp_client
45+
try:
46+
from mcp.client.streamable_http import streamable_http_client
47+
except ImportError: # pragma: no cover - compatibility with older mcp versions
48+
from mcp.client.streamable_http import streamablehttp_client as streamable_http_client
49+
50+
_STREAMABLE_HTTP_CLIENT_SUPPORTS_HTTP_CLIENT = "http_client" in inspect.signature(streamable_http_client).parameters
4151

4252
from trpc_agent_sdk.log import logger
4353

@@ -161,7 +171,7 @@ def _create_client(self, merged_headers: Optional[Dict[str, str]] = None):
161171
if isinstance(self._connection_params, StdioConnectionParams):
162172
client = stdio_client(
163173
server=self._connection_params.server_params,
164-
errlog=sys.stderr,
174+
errlog=sys.stderr, # cSpell:ignore errlog
165175
)
166176
elif isinstance(self._connection_params, SseConnectionParams):
167177
client = sse_client(
@@ -171,18 +181,46 @@ def _create_client(self, merged_headers: Optional[Dict[str, str]] = None):
171181
sse_read_timeout=self._connection_params.sse_read_timeout,
172182
)
173183
elif isinstance(self._connection_params, StreamableHTTPConnectionParams):
174-
client = streamablehttp_client(
184+
client = self._create_streamable_http_client(merged_headers)
185+
else:
186+
raise ValueError('Unable to initialize connection. Connection should be'
187+
' StdioConnectionParams or SseConnectionParams or StreamableHTTPConnectionParams, but got'
188+
f' {type(self._connection_params)}')
189+
return client
190+
191+
def _create_streamable_http_client(self, merged_headers: Optional[Dict[str, str]] = None):
192+
"""Creates a Streamable HTTP client with compatibility for MCP SDK versions."""
193+
if not _STREAMABLE_HTTP_CLIENT_SUPPORTS_HTTP_CLIENT:
194+
return streamable_http_client(
175195
url=self._connection_params.url,
176196
headers=merged_headers,
177197
timeout=self._connection_params.timeout,
178198
sse_read_timeout=self._connection_params.sse_read_timeout,
179199
terminate_on_close=self._connection_params.terminate_on_close,
180200
)
181-
else:
182-
raise ValueError('Unable to initialize connection. Connection should be'
183-
' StdioConnectionParams or SseConnectionParams or StreamableHTTPConnectionParams, but got'
184-
f' {type(self._connection_params)}')
185-
return client
201+
202+
return self._create_streamable_http_client_with_httpx(merged_headers)
203+
204+
@asynccontextmanager
205+
async def _create_streamable_http_client_with_httpx(self, merged_headers: Optional[Dict[str, str]] = None):
206+
"""Creates a new-style Streamable HTTP client and owns its httpx client."""
207+
timeout_seconds = self._connection_params.timeout
208+
if isinstance(timeout_seconds, timedelta):
209+
timeout_seconds = timeout_seconds.total_seconds()
210+
sse_read_timeout_seconds = self._connection_params.sse_read_timeout
211+
if isinstance(sse_read_timeout_seconds, timedelta):
212+
sse_read_timeout_seconds = sse_read_timeout_seconds.total_seconds()
213+
214+
async with httpx.AsyncClient(
215+
headers=merged_headers,
216+
timeout=httpx.Timeout(timeout_seconds, read=sse_read_timeout_seconds),
217+
) as http_client:
218+
async with streamable_http_client(
219+
url=self._connection_params.url,
220+
http_client=http_client,
221+
terminate_on_close=self._connection_params.terminate_on_close,
222+
) as transports:
223+
yield transports
186224

187225
async def create_session(self, headers: Optional[Dict[str, str]] = None) -> ClientSession | None:
188226
"""Creates and initializes an MCP client session.

0 commit comments

Comments
 (0)