Spaces:
Running
Running
hopeful0 commited on
Fix stateful proxy client mixing in multi-proxies sessions (#1245)
Browse files
src/fastmcp/server/proxy.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
| 7 |
from urllib.parse import quote
|
| 8 |
|
| 9 |
import mcp.types
|
|
|
|
| 10 |
from mcp.client.session import ClientSession
|
| 11 |
from mcp.shared.context import LifespanContextT, RequestContext
|
| 12 |
from mcp.shared.exceptions import McpError
|
|
@@ -606,6 +607,10 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
| 606 |
Note that it is essential to ensure that the proxy server itself is also stateful.
|
| 607 |
"""
|
| 608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
|
| 610 |
"""
|
| 611 |
The stateful proxy client will be forced disconnected when the session is exited.
|
|
@@ -613,6 +618,14 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
| 613 |
"""
|
| 614 |
pass
|
| 615 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
def new_stateful(self) -> Client[ClientTransportT]:
|
| 617 |
"""
|
| 618 |
Create a new stateful proxy client instance with the same configuration.
|
|
@@ -620,15 +633,15 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
| 620 |
Use this method as the client factory for stateful proxy server.
|
| 621 |
"""
|
| 622 |
session = get_context().session
|
| 623 |
-
proxy_client =
|
| 624 |
|
| 625 |
if proxy_client is None:
|
| 626 |
proxy_client = self.new()
|
| 627 |
logger.debug(f"{proxy_client} created for {session}")
|
| 628 |
-
|
| 629 |
|
| 630 |
async def _on_session_exit():
|
| 631 |
-
|
| 632 |
logger.debug(f"{proxy_client} will be disconnect")
|
| 633 |
await proxy_client._disconnect(force=True)
|
| 634 |
|
|
|
|
| 7 |
from urllib.parse import quote
|
| 8 |
|
| 9 |
import mcp.types
|
| 10 |
+
from mcp import ServerSession
|
| 11 |
from mcp.client.session import ClientSession
|
| 12 |
from mcp.shared.context import LifespanContextT, RequestContext
|
| 13 |
from mcp.shared.exceptions import McpError
|
|
|
|
| 607 |
Note that it is essential to ensure that the proxy server itself is also stateful.
|
| 608 |
"""
|
| 609 |
|
| 610 |
+
def __init__(self, *args, **kwargs):
|
| 611 |
+
super().__init__(*args, **kwargs)
|
| 612 |
+
self._caches: dict[ServerSession, Client[ClientTransportT]] = {}
|
| 613 |
+
|
| 614 |
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
|
| 615 |
"""
|
| 616 |
The stateful proxy client will be forced disconnected when the session is exited.
|
|
|
|
| 618 |
"""
|
| 619 |
pass
|
| 620 |
|
| 621 |
+
async def clear(self):
|
| 622 |
+
"""
|
| 623 |
+
Clear all cached clients and force disconnect them.
|
| 624 |
+
"""
|
| 625 |
+
while self._caches:
|
| 626 |
+
_, cache = self._caches.popitem()
|
| 627 |
+
await cache._disconnect(force=True)
|
| 628 |
+
|
| 629 |
def new_stateful(self) -> Client[ClientTransportT]:
|
| 630 |
"""
|
| 631 |
Create a new stateful proxy client instance with the same configuration.
|
|
|
|
| 633 |
Use this method as the client factory for stateful proxy server.
|
| 634 |
"""
|
| 635 |
session = get_context().session
|
| 636 |
+
proxy_client = self._caches.get(session, None)
|
| 637 |
|
| 638 |
if proxy_client is None:
|
| 639 |
proxy_client = self.new()
|
| 640 |
logger.debug(f"{proxy_client} created for {session}")
|
| 641 |
+
self._caches[session] = proxy_client
|
| 642 |
|
| 643 |
async def _on_session_exit():
|
| 644 |
+
self._caches.pop(session)
|
| 645 |
logger.debug(f"{proxy_client} will be disconnect")
|
| 646 |
await proxy_client._disconnect(force=True)
|
| 647 |
|
tests/server/proxy/test_stateful_proxy_client.py
CHANGED
|
@@ -118,3 +118,31 @@ class TestStatefulProxyClient:
|
|
| 118 |
|
| 119 |
with pytest.raises(ToolError, match="Value not found"):
|
| 120 |
await client.call_tool("stateful_get", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
with pytest.raises(ToolError, match="Value not found"):
|
| 120 |
await client.call_tool("stateful_get", {})
|
| 121 |
+
|
| 122 |
+
async def test_multi_proxies_no_mixing(self):
|
| 123 |
+
"""Test that the stateful proxy client won't be mixed in multi-proxies sessions."""
|
| 124 |
+
mcp_a, mcp_b = FastMCP(), FastMCP()
|
| 125 |
+
|
| 126 |
+
@mcp_a.tool
|
| 127 |
+
def tool_a() -> str:
|
| 128 |
+
return "a"
|
| 129 |
+
|
| 130 |
+
@mcp_b.tool
|
| 131 |
+
def tool_b() -> str:
|
| 132 |
+
return "b"
|
| 133 |
+
|
| 134 |
+
proxy_mcp_a = FastMCPProxy(
|
| 135 |
+
client_factory=StatefulProxyClient(mcp_a).new_stateful
|
| 136 |
+
)
|
| 137 |
+
proxy_mcp_b = FastMCPProxy(
|
| 138 |
+
client_factory=StatefulProxyClient(mcp_b).new_stateful
|
| 139 |
+
)
|
| 140 |
+
multi_proxy_mcp = FastMCP()
|
| 141 |
+
multi_proxy_mcp.mount(proxy_mcp_a, prefix="a")
|
| 142 |
+
multi_proxy_mcp.mount(proxy_mcp_b, prefix="b")
|
| 143 |
+
|
| 144 |
+
async with Client(multi_proxy_mcp) as client:
|
| 145 |
+
result_a = await client.call_tool("a_tool_a", {})
|
| 146 |
+
result_b = await client.call_tool("b_tool_b", {})
|
| 147 |
+
assert result_a.data == "a"
|
| 148 |
+
assert result_b.data == "b"
|