hopeful0 commited on
Commit
d80180f
·
unverified ·
1 Parent(s): 2ff991c

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 = session.__dict__.get("_proxy_client", None)
624
 
625
  if proxy_client is None:
626
  proxy_client = self.new()
627
  logger.debug(f"{proxy_client} created for {session}")
628
- session.__dict__["_proxy_client"] = proxy_client
629
 
630
  async def _on_session_exit():
631
- proxy_client: Client = session.__dict__.pop("_proxy_client")
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"