Merge pull request #1635 from jlowin/claude/issue-1625-20250826-0138

Reuse session for `StdioTransport` in `Client.new`
This commit is contained in:
William Easton 2025-08-26 20:23:21 -05:00 committed by GitHub
commit b4cd2857bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 90 additions and 4 deletions

View file

@ -54,6 +54,7 @@ from .transports import (
PythonStdioTransport,
SessionKwargs,
SSETransport,
StdioTransport,
StreamableHttpTransport,
infer_transport,
)
@ -337,11 +338,11 @@ class Client(Generic[ClientTransportT]):
await fresh_client.call_tool("some_tool", {})
```
"""
new_client = copy.copy(self)
# Reset session state to fresh state
new_client._session_state = ClientSessionState()
if not isinstance(self.transport, StdioTransport):
# Reset session state to fresh state
new_client._session_state = ClientSessionState()
new_client.name += f":{secrets.token_hex(2)}"
@ -394,6 +395,7 @@ class Client(Generic[ClientTransportT]):
self._session_state.session_task is None
or self._session_state.session_task.done()
)
if need_to_start:
if self._session_state.nesting_counter != 0:
raise RuntimeError(
@ -419,6 +421,7 @@ class Client(Generic[ClientTransportT]):
) from exception
self._session_state.nesting_counter += 1
return self
async def _disconnect(self, force: bool = False):

View file

@ -51,6 +51,7 @@ def mcp_server_type_to_servers_and_transports(
client: ProxyClient[StreamableHttpTransport | SSETransport | StdioTransport] = (
ProxyClient(transport=transport, name=client_name)
)
server = FastMCP.as_proxy(name=server_name, backend=client)
return name, server, transport

View file

@ -7,7 +7,7 @@ import weakref
import psutil
import pytest
from fastmcp import Client
from fastmcp import Client, FastMCP
from fastmcp.client.transports import PythonStdioTransport, StdioTransport
@ -24,6 +24,44 @@ def gc_collect_harder():
gc.collect()
class TestParallelCalls:
@pytest.fixture
def stdio_script(self, tmp_path):
script = inspect.cleandoc('''
import os
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool
def pid() -> int:
"""Gets PID of server"""
return os.getpid()
if __name__ == "__main__":
mcp.run()
''')
script_file = tmp_path / "stdio.py"
script_file.write_text(script)
return script_file
async def test_parallel_calls(self, stdio_script):
backend_transport = PythonStdioTransport(script_path=stdio_script)
backend_client = Client(transport=backend_transport)
proxy = FastMCP.as_proxy(backend=backend_client, name="PROXY")
count = 10
tasks = [proxy.get_tools() for _ in range(count)]
results = await asyncio.gather(*tasks, return_exceptions=True)
assert len(results) == count
errors = [result for result in results if isinstance(result, Exception)]
assert len(errors) == 0
class TestKeepAlive:
# https://github.com/jlowin/fastmcp/issues/581

View file

@ -243,6 +243,50 @@ async def test_multi_client(tmp_path: Path):
assert result_2.data == 3
async def test_multi_client_parallel_calls(tmp_path: Path):
server_script = inspect.cleandoc("""
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
if __name__ == '__main__':
mcp.run()
""")
script_path = tmp_path / "test.py"
script_path.write_text(server_script)
config = {
"mcpServers": {
"test_1": {
"command": "python",
"args": [str(script_path)],
},
"test_2": {
"command": "python",
"args": [str(script_path)],
},
}
}
client = Client(config)
async with client:
_ = await client.list_tools()
tasks = [client.list_tools() for _ in range(40)]
results = await asyncio.gather(*tasks, return_exceptions=True)
exceptions = [result for result in results if isinstance(result, Exception)]
assert len(exceptions) == 0
assert len(results) == 40
assert all(len(result) == 2 for result in results)
@pytest.mark.skipif(
running_under_debugger() or sys.platform.startswith("win32"),
reason="Debugger holds a reference to the transport; Windows has process lifecycle issues",