hopeful0 commited on
Commit
fd0c13f
·
unverified ·
1 Parent(s): a8188bd

Proxy support advanced MCP features (#1022)

Browse files

* Proxy support advanced MCP features

* Optimize ProxyClient testing

* Add documentation for ProxyClient

docs/servers/proxy.mdx CHANGED
@@ -162,6 +162,12 @@ composite_proxy = FastMCP.as_proxy(config, name="Composite Proxy")
162
  # - weather://weather/icons/sunny, calendar://calendar/events/today
163
  ```
164
 
 
 
 
 
 
 
165
  ## `FastMCPProxy` Class
166
 
167
  Internally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally don't need to interact with this class directly, but it's available if needed.
 
162
  # - weather://weather/icons/sunny, calendar://calendar/events/today
163
  ```
164
 
165
+ ## Forwarding Interactions
166
+
167
+ `ProxyClient` is a subclass of `Client` that implements a set of default handlers to forward advanced interactions between the backend server and the client connected to the proxy. These handlers receive requests or notifications from the backend server and forward them to the client through the related request context, relaying the response back to the backend server if needed. This setup enables the proxy to support advanced MCP features, including roots, sampling, elicitation, logging, and progress.
168
+
169
+ To prevent the forwarding of any interaction, pass `None` to the corresponding handler when creating the `ProxyClient`. Typically, you should use `ProxyClient` to establish a proxy unless there is a specific reason not to. If the `transport` parameter is not an instance of `Client`, `FastMCP.as_proxy()` will automatically instantiate a `ProxyClient`.
170
+
171
  ## `FastMCPProxy` Class
172
 
173
  Internally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally don't need to interact with this class directly, but it's available if needed.
src/fastmcp/server/proxy.py CHANGED
@@ -1,9 +1,12 @@
1
  from __future__ import annotations
2
 
 
3
  from typing import TYPE_CHECKING, Any, cast
4
  from urllib.parse import quote
5
 
6
  import mcp.types
 
 
7
  from mcp.shared.exceptions import McpError
8
  from mcp.types import (
9
  METHOD_NOT_FOUND,
@@ -14,6 +17,10 @@ from mcp.types import (
14
  from pydantic.networks import AnyUrl
15
 
16
  from fastmcp.client import Client
 
 
 
 
17
  from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
18
  from fastmcp.prompts import Prompt, PromptMessage
19
  from fastmcp.prompts.prompt import PromptArgument
@@ -21,10 +28,12 @@ from fastmcp.prompts.prompt_manager import PromptManager
21
  from fastmcp.resources import Resource, ResourceTemplate
22
  from fastmcp.resources.resource_manager import ResourceManager
23
  from fastmcp.server.context import Context
 
24
  from fastmcp.server.server import FastMCP
25
  from fastmcp.tools.tool import Tool, ToolResult
26
  from fastmcp.tools.tool_manager import ToolManager
27
  from fastmcp.utilities.logging import get_logger
 
28
 
29
  if TYPE_CHECKING:
30
  from fastmcp.server import Context
@@ -406,3 +415,110 @@ class FastMCPProxy(FastMCP):
406
  self._tool_manager = ProxyToolManager(client=self.client)
407
  self._resource_manager = ProxyResourceManager(client=self.client)
408
  self._prompt_manager = ProxyPromptManager(client=self.client)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ from pathlib import Path
4
  from typing import TYPE_CHECKING, Any, cast
5
  from urllib.parse import quote
6
 
7
  import mcp.types
8
+ from mcp.client.session import ClientSession
9
+ from mcp.shared.context import LifespanContextT, RequestContext
10
  from mcp.shared.exceptions import McpError
11
  from mcp.types import (
12
  METHOD_NOT_FOUND,
 
17
  from pydantic.networks import AnyUrl
18
 
19
  from fastmcp.client import Client
20
+ from fastmcp.client.elicitation import ElicitResult
21
+ from fastmcp.client.logging import LogMessage
22
+ from fastmcp.client.roots import RootsList
23
+ from fastmcp.client.transports import ClientTransportT
24
  from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
25
  from fastmcp.prompts import Prompt, PromptMessage
26
  from fastmcp.prompts.prompt import PromptArgument
 
28
  from fastmcp.resources import Resource, ResourceTemplate
29
  from fastmcp.resources.resource_manager import ResourceManager
30
  from fastmcp.server.context import Context
31
+ from fastmcp.server.dependencies import get_context
32
  from fastmcp.server.server import FastMCP
33
  from fastmcp.tools.tool import Tool, ToolResult
34
  from fastmcp.tools.tool_manager import ToolManager
35
  from fastmcp.utilities.logging import get_logger
36
+ from fastmcp.utilities.mcp_config import MCPConfig
37
 
38
  if TYPE_CHECKING:
39
  from fastmcp.server import Context
 
415
  self._tool_manager = ProxyToolManager(client=self.client)
416
  self._resource_manager = ProxyResourceManager(client=self.client)
417
  self._prompt_manager = ProxyPromptManager(client=self.client)
418
+
419
+
420
+ async def default_proxy_roots_handler(
421
+ context: RequestContext[ClientSession, LifespanContextT],
422
+ ) -> RootsList:
423
+ """
424
+ A handler that forwards the list roots request from the remote server to the proxy's connected clients and relays the response back to the remote server.
425
+ """
426
+ ctx = get_context()
427
+ return await ctx.list_roots()
428
+
429
+
430
+ class ProxyClient(Client[ClientTransportT]):
431
+ """
432
+ A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
433
+ Supports forwarding roots, sampling, elicitation, logging, and progress.
434
+ """
435
+
436
+ def __init__(
437
+ self,
438
+ transport: (
439
+ ClientTransportT
440
+ | FastMCP
441
+ | AnyUrl
442
+ | Path
443
+ | MCPConfig
444
+ | dict[str, Any]
445
+ | str
446
+ ),
447
+ **kwargs,
448
+ ):
449
+ if "roots" not in kwargs:
450
+ kwargs["roots"] = default_proxy_roots_handler
451
+ if "sampling_handler" not in kwargs:
452
+ kwargs["sampling_handler"] = ProxyClient.default_sampling_handler
453
+ if "elicitation_handler" not in kwargs:
454
+ kwargs["elicitation_handler"] = ProxyClient.default_elicitation_handler
455
+ if "log_handler" not in kwargs:
456
+ kwargs["log_handler"] = ProxyClient.default_log_handler
457
+ if "progress_handler" not in kwargs:
458
+ kwargs["progress_handler"] = ProxyClient.default_progress_handler
459
+ super().__init__(transport, **kwargs)
460
+
461
+ @classmethod
462
+ async def default_sampling_handler(
463
+ cls,
464
+ messages: list[mcp.types.SamplingMessage],
465
+ params: mcp.types.CreateMessageRequestParams,
466
+ context: RequestContext[ClientSession, LifespanContextT],
467
+ ) -> mcp.types.CreateMessageResult:
468
+ """
469
+ A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server.
470
+ """
471
+ ctx = get_context()
472
+ content = await ctx.sample(
473
+ [msg for msg in messages],
474
+ system_prompt=params.systemPrompt,
475
+ temperature=params.temperature,
476
+ max_tokens=params.maxTokens,
477
+ model_preferences=params.modelPreferences,
478
+ )
479
+ if isinstance(content, mcp.types.ResourceLink | mcp.types.EmbeddedResource):
480
+ raise RuntimeError("Content is not supported")
481
+ return mcp.types.CreateMessageResult(
482
+ role="assistant",
483
+ model="fastmcp-client",
484
+ content=content,
485
+ )
486
+
487
+ @classmethod
488
+ async def default_elicitation_handler(
489
+ cls,
490
+ message: str,
491
+ response_type: type,
492
+ params: mcp.types.ElicitRequestParams,
493
+ context: RequestContext[ClientSession, LifespanContextT],
494
+ ) -> ElicitResult:
495
+ """
496
+ A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
497
+ """
498
+ ctx = get_context()
499
+ result = await ctx.elicit(message, response_type)
500
+ if result.action == "accept":
501
+ return result.data
502
+ else:
503
+ return ElicitResult(action=result.action)
504
+
505
+ @classmethod
506
+ async def default_log_handler(cls, message: LogMessage) -> None:
507
+ """
508
+ A handler that forwards the log notification from the remote server to the proxy's connected clients.
509
+ """
510
+ ctx = get_context()
511
+ await ctx.log(message.data, level=message.level, logger_name=message.logger)
512
+
513
+ @classmethod
514
+ async def default_progress_handler(
515
+ cls,
516
+ progress: float,
517
+ total: float | None,
518
+ message: str | None,
519
+ ) -> None:
520
+ """
521
+ A handler that forwards the progress notification from the remote server to the proxy's connected clients.
522
+ """
523
+ ctx = get_context()
524
+ await ctx.report_progress(progress, total, message)
src/fastmcp/server/server.py CHANGED
@@ -1643,9 +1643,8 @@ class FastMCP(Generic[LifespanResultT]):
1643
  resource_separator: Deprecated. Separator character for resource URIs.
1644
  prompt_separator: Deprecated. Separator character for prompt names.
1645
  """
1646
- from fastmcp import Client
1647
  from fastmcp.client.transports import FastMCPTransport
1648
- from fastmcp.server.proxy import FastMCPProxy
1649
 
1650
  # Deprecated since 2.9.0
1651
  # Prior to 2.9.0, the first positional argument was the prefix and the
@@ -1697,7 +1696,7 @@ class FastMCP(Generic[LifespanResultT]):
1697
  as_proxy = server._has_lifespan
1698
 
1699
  if as_proxy and not isinstance(server, FastMCPProxy):
1700
- server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
1701
 
1702
  # Delegate mounting to all three managers
1703
  mounted_server = MountedServer(
@@ -1908,14 +1907,16 @@ class FastMCP(Generic[LifespanResultT]):
1908
  @classmethod
1909
  def as_proxy(
1910
  cls,
1911
- backend: Client[ClientTransportT]
1912
- | ClientTransport
1913
- | FastMCP[Any]
1914
- | AnyUrl
1915
- | Path
1916
- | MCPConfig
1917
- | dict[str, Any]
1918
- | str,
 
 
1919
  **settings: Any,
1920
  ) -> FastMCPProxy:
1921
  """Create a FastMCP proxy server for the given backend.
@@ -1926,12 +1927,12 @@ class FastMCP(Generic[LifespanResultT]):
1926
  `fastmcp.client.Client` constructor.
1927
  """
1928
  from fastmcp.client.client import Client
1929
- from fastmcp.server.proxy import FastMCPProxy
1930
 
1931
  if isinstance(backend, Client):
1932
  client = backend
1933
  else:
1934
- client = Client(backend)
1935
 
1936
  return FastMCPProxy(client=client, **settings)
1937
 
 
1643
  resource_separator: Deprecated. Separator character for resource URIs.
1644
  prompt_separator: Deprecated. Separator character for prompt names.
1645
  """
 
1646
  from fastmcp.client.transports import FastMCPTransport
1647
+ from fastmcp.server.proxy import FastMCPProxy, ProxyClient
1648
 
1649
  # Deprecated since 2.9.0
1650
  # Prior to 2.9.0, the first positional argument was the prefix and the
 
1696
  as_proxy = server._has_lifespan
1697
 
1698
  if as_proxy and not isinstance(server, FastMCPProxy):
1699
+ server = FastMCPProxy(ProxyClient(transport=FastMCPTransport(server)))
1700
 
1701
  # Delegate mounting to all three managers
1702
  mounted_server = MountedServer(
 
1907
  @classmethod
1908
  def as_proxy(
1909
  cls,
1910
+ backend: (
1911
+ Client[ClientTransportT]
1912
+ | ClientTransport
1913
+ | FastMCP[Any]
1914
+ | AnyUrl
1915
+ | Path
1916
+ | MCPConfig
1917
+ | dict[str, Any]
1918
+ | str
1919
+ ),
1920
  **settings: Any,
1921
  ) -> FastMCPProxy:
1922
  """Create a FastMCP proxy server for the given backend.
 
1927
  `fastmcp.client.Client` constructor.
1928
  """
1929
  from fastmcp.client.client import Client
1930
+ from fastmcp.server.proxy import FastMCPProxy, ProxyClient
1931
 
1932
  if isinstance(backend, Client):
1933
  client = backend
1934
  else:
1935
+ client = ProxyClient(backend)
1936
 
1937
  return FastMCPProxy(client=client, **settings)
1938
 
tests/server/proxy/test_proxy_client.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import cast
3
+
4
+ import pytest
5
+ from mcp.types import LoggingLevel, ModelHint, ModelPreferences, TextContent
6
+
7
+ from fastmcp import Client, Context, FastMCP
8
+ from fastmcp.client.elicitation import ElicitRequestParams, ElicitResult
9
+ from fastmcp.client.logging import LogMessage
10
+ from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams
11
+ from fastmcp.exceptions import ToolError
12
+ from fastmcp.server.proxy import ProxyClient
13
+
14
+
15
+ @pytest.fixture
16
+ def fastmcp_server():
17
+ mcp = FastMCP("TestServer")
18
+
19
+ @mcp.tool
20
+ async def list_roots(context: Context) -> list[str]:
21
+ roots = await context.list_roots()
22
+ return [str(r.uri) for r in roots]
23
+
24
+ @mcp.tool
25
+ async def sampling(
26
+ context: Context,
27
+ ) -> str:
28
+ result = await context.sample(
29
+ "Hello, world!",
30
+ system_prompt="You love FastMCP",
31
+ temperature=0.5,
32
+ max_tokens=100,
33
+ model_preferences="gpt-4o",
34
+ )
35
+ return cast(TextContent, result).text
36
+
37
+ @dataclass
38
+ class Person:
39
+ name: str
40
+
41
+ @mcp.tool
42
+ async def elicit(context: Context) -> str:
43
+ result = await context.elicit(
44
+ message="What is your name?",
45
+ response_type=Person,
46
+ )
47
+ if result.action == "accept":
48
+ return f"Hello, {result.data.name}!"
49
+ else:
50
+ return "No name provided."
51
+
52
+ @mcp.tool
53
+ async def log(
54
+ message: str, level: LoggingLevel, logger: str, context: Context
55
+ ) -> None:
56
+ await context.log(message=message, level=level, logger_name=logger)
57
+
58
+ @mcp.tool
59
+ async def report_progress(context: Context) -> int:
60
+ for i in range(3):
61
+ await context.report_progress(
62
+ progress=i + 1,
63
+ total=3,
64
+ message=f"{(i + 1) / 3 * 100:.2f}% complete",
65
+ )
66
+ return 100
67
+
68
+ return mcp
69
+
70
+
71
+ @pytest.fixture
72
+ async def proxy_server(fastmcp_server: FastMCP):
73
+ """
74
+ A proxy server that forwards interactions with the proxy client to the given fastmcp server.
75
+ """
76
+ return FastMCP.as_proxy(ProxyClient(fastmcp_server))
77
+
78
+
79
+ class TestProxyClient:
80
+ async def test_forward_error_response(self, proxy_server: FastMCP):
81
+ """
82
+ Test that the proxy client correctly forwards an error response.
83
+ """
84
+ async with Client(proxy_server) as client:
85
+ with pytest.raises(ToolError, match="Elicitation not supported"):
86
+ await client.call_tool("elicit", {})
87
+
88
+ async def test_forward_list_roots_request(self, proxy_server: FastMCP):
89
+ """
90
+ Test that the proxy client correctly forwards the `list_roots` request.
91
+ """
92
+ roots_handler_called = False
93
+
94
+ async def roots_handler(ctx: RequestContext):
95
+ nonlocal roots_handler_called
96
+ roots_handler_called = True
97
+ return []
98
+
99
+ async with Client(proxy_server, roots=roots_handler) as client:
100
+ await client.call_tool("list_roots", {})
101
+
102
+ assert roots_handler_called
103
+
104
+ async def test_forward_list_roots_response(self, proxy_server: FastMCP):
105
+ """
106
+ Test that the proxy client correctly forwards the `list_roots` response.
107
+ """
108
+ async with Client(proxy_server, roots=["file://x/y/z"]) as client:
109
+ result = await client.call_tool("list_roots", {})
110
+ assert result.data == ["file://x/y/z"]
111
+
112
+ async def test_forward_sampling_request(self, proxy_server: FastMCP):
113
+ """
114
+ Test that the proxy client correctly forwards the `sampling` request.
115
+ """
116
+ sampling_handler_called = False
117
+
118
+ def sampling_handler(
119
+ messages: list[SamplingMessage],
120
+ params: SamplingParams,
121
+ ctx: RequestContext,
122
+ ) -> str:
123
+ nonlocal sampling_handler_called
124
+ sampling_handler_called = True
125
+ assert messages == [
126
+ SamplingMessage(
127
+ role="user",
128
+ content=TextContent(type="text", text="Hello, world!"),
129
+ )
130
+ ]
131
+ assert params.systemPrompt == "You love FastMCP"
132
+ assert params.temperature == 0.5
133
+ assert params.maxTokens == 100
134
+ assert params.modelPreferences == ModelPreferences(
135
+ hints=[ModelHint(name="gpt-4o")]
136
+ )
137
+ return ""
138
+
139
+ async with Client(proxy_server, sampling_handler=sampling_handler) as client:
140
+ await client.call_tool("sampling", {})
141
+
142
+ assert sampling_handler_called
143
+
144
+ async def test_forward_sampling_response(self, proxy_server: FastMCP):
145
+ """
146
+ Test that the proxy client correctly forwards the `sampling` response.
147
+ """
148
+ async with Client(
149
+ proxy_server, sampling_handler=lambda *args: "I love FastMCP"
150
+ ) as client:
151
+ result = await client.call_tool("sampling", {})
152
+ assert result.data == "I love FastMCP"
153
+
154
+ async def test_elicit_request(self, proxy_server: FastMCP):
155
+ """
156
+ Test that the proxy client correctly forwards the `elicit` request.
157
+ """
158
+ elicitation_handler_called = False
159
+
160
+ async def elicitation_handler(
161
+ message, response_type, params: ElicitRequestParams, ctx
162
+ ):
163
+ nonlocal elicitation_handler_called
164
+ elicitation_handler_called = True
165
+ assert message == "What is your name?"
166
+ assert "Person" in str(response_type)
167
+ assert params.requestedSchema == {
168
+ "title": "Person",
169
+ "type": "object",
170
+ "properties": {"name": {"title": "Name", "type": "string"}},
171
+ "required": ["name"],
172
+ }
173
+ return ElicitResult(action="accept", content=response_type(name="Alice"))
174
+
175
+ async with Client(
176
+ proxy_server, elicitation_handler=elicitation_handler
177
+ ) as client:
178
+ await client.call_tool("elicit", {})
179
+
180
+ assert elicitation_handler_called
181
+
182
+ async def test_elicit_accept_response(self, proxy_server: FastMCP):
183
+ """
184
+ Test that the proxy client correctly forwards the `elicit` accept response.
185
+ """
186
+
187
+ async def elicitation_handler(
188
+ message, response_type, params: ElicitRequestParams, ctx
189
+ ):
190
+ return ElicitResult(action="accept", content=response_type(name="Alice"))
191
+
192
+ async with Client(
193
+ proxy_server,
194
+ elicitation_handler=elicitation_handler,
195
+ ) as client:
196
+ result = await client.call_tool("elicit", {})
197
+ assert result.data == "Hello, Alice!"
198
+
199
+ async def test_elicit_decline_response(self, proxy_server: FastMCP):
200
+ """
201
+ Test that the proxy client correctly forwards the `elicit` decline response.
202
+ """
203
+
204
+ async def elicitation_handler(
205
+ message, response_type, params: ElicitRequestParams, ctx
206
+ ):
207
+ return ElicitResult(action="decline")
208
+
209
+ async with Client(
210
+ proxy_server, elicitation_handler=elicitation_handler
211
+ ) as client:
212
+ result = await client.call_tool("elicit", {})
213
+ assert result.data == "No name provided."
214
+
215
+ async def test_log_request(self, proxy_server: FastMCP):
216
+ """
217
+ Test that the proxy client correctly forwards the `log` request.
218
+ """
219
+ log_handler_called = False
220
+
221
+ async def log_handler(message: LogMessage) -> None:
222
+ nonlocal log_handler_called
223
+ log_handler_called = True
224
+ assert message.data == "Hello, world!"
225
+ assert message.level == "info"
226
+ assert message.logger == "test"
227
+
228
+ async with Client(proxy_server, log_handler=log_handler) as client:
229
+ await client.call_tool(
230
+ "log", {"message": "Hello, world!", "level": "info", "logger": "test"}
231
+ )
232
+
233
+ assert log_handler_called
234
+
235
+ async def test_report_progress_request(self, proxy_server: FastMCP):
236
+ """
237
+ Test that the proxy client correctly forwards the `report_progress` request.
238
+ """
239
+
240
+ EXPECTED_PROGRESS_MESSAGES = [
241
+ dict(progress=1, total=3, message="33.33% complete"),
242
+ dict(progress=2, total=3, message="66.67% complete"),
243
+ dict(progress=3, total=3, message="100.00% complete"),
244
+ ]
245
+ PROGRESS_MESSAGES = []
246
+
247
+ async def progress_handler(
248
+ progress: float, total: float | None, message: str | None
249
+ ) -> None:
250
+ PROGRESS_MESSAGES.append(
251
+ dict(progress=progress, total=total, message=message)
252
+ )
253
+
254
+ async with Client(proxy_server, progress_handler=progress_handler) as client:
255
+ await client.call_tool("report_progress", {})
256
+
257
+ assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES
tests/server/test_proxy.py CHANGED
@@ -11,7 +11,7 @@ from fastmcp import FastMCP
11
  from fastmcp.client import Client
12
  from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport
13
  from fastmcp.exceptions import ToolError
14
- from fastmcp.server.proxy import FastMCPProxy
15
 
16
  USERS = [
17
  {"id": "1", "name": "Alice", "active": True},
@@ -71,13 +71,13 @@ def fastmcp_server():
71
  @pytest.fixture
72
  async def proxy_server(fastmcp_server):
73
  """Fixture that creates a FastMCP proxy server."""
74
- return FastMCP.as_proxy(Client(transport=FastMCPTransport(fastmcp_server)))
75
 
76
 
77
  async def test_create_proxy(fastmcp_server):
78
  """Test that the proxy server properly forwards requests to the original server."""
79
  # Create a client
80
- client = Client(transport=FastMCPTransport(fastmcp_server))
81
 
82
  server = FastMCPProxy.as_proxy(client)
83
 
 
11
  from fastmcp.client import Client
12
  from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport
13
  from fastmcp.exceptions import ToolError
14
+ from fastmcp.server.proxy import FastMCPProxy, ProxyClient
15
 
16
  USERS = [
17
  {"id": "1", "name": "Alice", "active": True},
 
71
  @pytest.fixture
72
  async def proxy_server(fastmcp_server):
73
  """Fixture that creates a FastMCP proxy server."""
74
+ return FastMCP.as_proxy(ProxyClient(transport=FastMCPTransport(fastmcp_server)))
75
 
76
 
77
  async def test_create_proxy(fastmcp_server):
78
  """Test that the proxy server properly forwards requests to the original server."""
79
  # Create a client
80
+ client = ProxyClient(transport=FastMCPTransport(fastmcp_server))
81
 
82
  server = FastMCPProxy.as_proxy(client)
83