diff --git a/README.md b/README.md index cf647b881..8dd3787fd 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ FastMCP introduces powerful ways to structure and deploy your MCP applications. ### Proxy Servers -Create a FastMCP server that acts as an intermediary for another local or remote MCP server using `FastMCP.from_client()`. This is especially useful for bridging transports (e.g., remote SSE to local Stdio) or adding a layer of logic to a server you don't control. +Create a FastMCP server that acts as an intermediary for another local or remote MCP server using `FastMCP.as_proxy()`. This is especially useful for bridging transports (e.g., remote SSE to local Stdio) or adding a layer of logic to a server you don't control. Learn more in the [**Proxying Documentation**](https://gofastmcp.com/patterns/proxy). diff --git a/docs/servers/composition.mdx b/docs/servers/composition.mdx index 511c0e95d..7f14f0609 100644 --- a/docs/servers/composition.mdx +++ b/docs/servers/composition.mdx @@ -167,11 +167,11 @@ FastMCP automatically uses proxy mounting when the mounted server has a custom l #### Interaction with Proxy Servers -When using `FastMCP.from_client()` to create a proxy server, mounting that server will always use proxy mounting: +When using `FastMCP.as_proxy()` to create a proxy server, mounting that server will always use proxy mounting: ```python # Create a proxy for a remote server -remote_proxy = FastMCP.from_client(Client("http://example.com/mcp")) +remote_proxy = FastMCP.as_proxy(Client("http://example.com/mcp")) # Mount the proxy (always uses proxy mounting) main_server.mount("remote", remote_proxy) diff --git a/docs/servers/fastmcp.mdx b/docs/servers/fastmcp.mdx index 2e5e18ef2..ad2a356b0 100644 --- a/docs/servers/fastmcp.mdx +++ b/docs/servers/fastmcp.mdx @@ -156,7 +156,7 @@ main.mount("sub", sub) -FastMCP can act as a proxy for any MCP server (local or remote) using `FastMCP.from_client`, letting you bridge transports or add a frontend to existing servers. For example, you can expose a remote SSE server locally via stdio, or vice versa. +FastMCP can act as a proxy for any MCP server (local or remote) using `FastMCP.as_proxy`, letting you bridge transports or add a frontend to existing servers. For example, you can expose a remote SSE server locally via stdio, or vice versa. See the [Proxying Servers](/patterns/proxy) guide for details and advanced usage. @@ -164,7 +164,7 @@ See the [Proxying Servers](/patterns/proxy) guide for details and advanced usage from fastmcp import FastMCP, Client backend = Client("http://example.com/mcp/sse") -proxy = FastMCP.from_client(backend, name="ProxyServer") +proxy = FastMCP.as_proxy(backend, name="ProxyServer") # Now use the proxy like any FastMCP server ``` diff --git a/docs/servers/proxy.mdx b/docs/servers/proxy.mdx index 9743b629a..f755b8ead 100644 --- a/docs/servers/proxy.mdx +++ b/docs/servers/proxy.mdx @@ -8,7 +8,9 @@ import { VersionBadge } from '/snippets/version-badge.mdx' -FastMCP provides a powerful proxying capability that allows one FastMCP server instance to act as a frontend for another MCP server (which could be remote, running on a different transport, or even another FastMCP instance). This is achieved using the `FastMCP.from_client()` class method. +FastMCP provides a powerful proxying capability that allows one FastMCP server instance to act as a frontend for another MCP server (which could be remote, running on a different transport, or even another FastMCP instance). This is achieved using the `FastMCP.as_proxy()` class method. + +`as_proxy()` accepts either an existing `Client` or any argument that can be passed to a `Client` as its `transport` parameter—such as another `FastMCP` instance or a URL to a remote server. ## What is Proxying? @@ -37,26 +39,23 @@ sequenceDiagram ## Creating a Proxy -The easiest way to create a proxy is using the `FastMCP.from_client()` class method. This creates a standard FastMCP server that forwards requests to another MCP server. +The easiest way to create a proxy is using the `FastMCP.as_proxy()` class method. This creates a standard FastMCP server that forwards requests to another MCP server. ```python -from fastmcp import FastMCP, Client +from fastmcp import FastMCP -# Create a client configured to talk to the backend server -# This could be any MCP server - remote, local, or using any transport -backend_client = Client("backend_server.py") # Could be "http://remote.server/sse", etc. - -# Create the proxy server with from_client() -proxy_server = FastMCP.from_client( - backend_client, +# Provide the backend in any form accepted by Client +proxy_server = FastMCP.as_proxy( + "backend_server.py", # Could also be a FastMCP instance or a remote URL name="MyProxyServer" # Optional settings for the proxy ) -# That's it! You now have a proxy FastMCP server that can be used -# with any transport (SSE, stdio, etc.) just like any other FastMCP server +# Or create the Client yourself for custom configuration +backend_client = Client("backend_server.py") +proxy_from_client = FastMCP.as_proxy(backend_client) ``` -**How `from_client` Works:** +**How `as_proxy` Works:** 1. It connects to the backend server using the provided client. 2. It discovers all the tools, resources, resource templates, and prompts available on the backend server. @@ -72,13 +71,10 @@ Currently, proxying focuses primarily on exposing the major MCP objects (tools, A common use case is to bridge transports. For example, making a remote SSE server available locally via Stdio: ```python -from fastmcp import FastMCP, Client +from fastmcp import FastMCP -# Client targeting a remote SSE server -client = Client("http://example.com/mcp/sse") - -# Create a proxy server - it's just a regular FastMCP server -proxy = FastMCP.from_client(client, name="SSE to Stdio Proxy") +# Target a remote SSE server directly by URL +proxy = FastMCP.as_proxy("http://example.com/mcp/sse", name="SSE to Stdio Proxy") # The proxy can now be used with any transport # No special handling needed - it works like any FastMCP server @@ -89,7 +85,7 @@ proxy = FastMCP.from_client(client, name="SSE to Stdio Proxy") You can also proxy an in-memory `FastMCP` instance, which is useful for adjusting the configuration or behavior of a server you don't completely control. ```python -from fastmcp import FastMCP, Client +from fastmcp import FastMCP # Original server original_server = FastMCP(name="Original") @@ -98,12 +94,9 @@ original_server = FastMCP(name="Original") def tool_a() -> str: return "A" -# To proxy an in-memory server, first create a Client to it. -client_to_original = Client(original_server) - -# Create a proxy of the original server using the client. -proxy = FastMCP.from_client( - client_to_original, +# Create a proxy of the original server directly +proxy = FastMCP.as_proxy( + original_server, name="Proxy Server" ) @@ -113,6 +106,6 @@ proxy = FastMCP.from_client( ## `FastMCPProxy` Class -Internally, `FastMCP.from_client()` uses the `FastMCPProxy` class. You generally don't need to interact with this class directly, but it's available if needed. +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. Using the class directly might be necessary for advanced scenarios, like subclassing `FastMCPProxy` to add custom logic before or after forwarding requests. \ No newline at end of file diff --git a/examples/in_memory_proxy_example.py b/examples/in_memory_proxy_example.py index 9620fa113..45e7a10b2 100644 --- a/examples/in_memory_proxy_example.py +++ b/examples/in_memory_proxy_example.py @@ -3,9 +3,8 @@ This example demonstrates how to set up and use an in-memory FastMCP proxy. It illustrates the pattern: 1. Create an original FastMCP server with some tools. -2. Create a Client that connects to this original server (in-memory). -3. Create a proxy FastMCP server using FastMCP.from_client(), passing it the client from step 2. -4. Use another Client to connect to the proxy server (in-memory) and interact with the original server's tools through the proxy. +2. Create a proxy FastMCP server using ``FastMCP.as_proxy(original_server)``. +3. Use another Client to connect to the proxy server (in-memory) and interact with the original server's tools through the proxy. """ import asyncio @@ -36,24 +35,18 @@ async def main(): original_server.add_tool(EchoService().echo) print(f" -> Original Server '{original_server.name}' created.") - # 2. Client for Proxy - print("\nStep 2: Creating a Client to connect to the Original Server...") - print(" (This client will be used internally by the proxy server)") - client_to_original = Client(original_server) - print(f" -> Client for proxy created, targeting '{original_server.name}'.") - - # 3. Proxy Server Creation - print("\nStep 3: Creating the Proxy Server (InMemoryProxy)...") + # 2. Proxy Server Creation + print("\nStep 2: Creating the Proxy Server (InMemoryProxy)...") print( - f" (Using FastMCP.from_client, passing it the client from Step 2 that targets '{original_server.name}')" + f" (Using FastMCP.as_proxy to wrap '{original_server.name}' directly)" ) - proxy_server = FastMCP.from_client(client_to_original, name="InMemoryProxy") + proxy_server = FastMCP.as_proxy(original_server, name="InMemoryProxy") print( f" -> Proxy Server '{proxy_server.name}' created, proxying '{original_server.name}'." ) - # 4. Interacting via Proxy - print("\nStep 4: Using a new Client to connect to the Proxy Server and interact...") + # 3. Interacting via Proxy + print("\nStep 3: Using a new Client to connect to the Proxy Server and interact...") async with Client(proxy_server) as final_client: print(f" -> Successfully connected to proxy '{proxy_server.name}'.") diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index c6aaeedba..ada608760 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -11,6 +11,7 @@ from contextlib import ( asynccontextmanager, ) from functools import partial +from pathlib import Path from typing import TYPE_CHECKING, Any, Generic, Literal import anyio @@ -60,6 +61,7 @@ from fastmcp.utilities.logging import get_logger if TYPE_CHECKING: from fastmcp.client import Client + from fastmcp.client.transports import ClientTransport from fastmcp.server.openapi import FastMCPOpenAPI from fastmcp.server.proxy import FastMCPProxy logger = get_logger(__name__) @@ -1104,14 +1106,47 @@ class FastMCP(Generic[LifespanResultT]): openapi_spec=app.openapi(), client=client, name=name, **settings ) + @classmethod + def as_proxy( + cls, + backend: Client + | ClientTransport + | FastMCP[Any] + | AnyUrl + | Path + | dict[str, Any] + | str, + **settings: Any, + ) -> FastMCPProxy: + """Create a FastMCP proxy server for the given backend. + + The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client` + instance or any value accepted as the ``transport`` argument of + :class:`~fastmcp.client.Client`. This mirrors the convenience of the + ``Client`` constructor. + """ + from fastmcp.server.proxy import FastMCPProxy + + if isinstance(backend, Client): + client = backend + else: + client = Client(backend) + + return FastMCPProxy(client=client, **settings) + @classmethod def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy: """ Create a FastMCP proxy server from a FastMCP client. """ - from fastmcp.server.proxy import FastMCPProxy + # Deprecated since 2.4.0 + warnings.warn( + "FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.", + DeprecationWarning, + stacklevel=2, + ) - return FastMCPProxy(client=client, **settings) + return cls.as_proxy(client, **settings) def _validate_resource_prefix(prefix: str) -> None: diff --git a/tests/server/test_import_server.py b/tests/server/test_import_server.py index 8b7cade87..ef03ceffc 100644 --- a/tests/server/test_import_server.py +++ b/tests/server/test_import_server.py @@ -299,7 +299,7 @@ async def test_import_with_proxy_tools(): def get_data(query: str) -> str: return f"Data for query: {query}" - proxy_app = FastMCP.from_client(Client(api_app)) + proxy_app = FastMCP.as_proxy(Client(api_app)) await main_app.import_server("api", proxy_app) result = await main_app._mcp_call_tool("api_get_data", {"query": "test"}) @@ -323,7 +323,7 @@ async def test_import_with_proxy_prompts(): """Example greeting prompt.""" return f"Hello, {name} from API!" - proxy_app = FastMCP.from_client(Client(api_app)) + proxy_app = FastMCP.as_proxy(Client(api_app)) await main_app.import_server("api", proxy_app) result = await main_app._mcp_get_prompt("api_greeting", {"name": "World"}) @@ -351,7 +351,7 @@ async def test_import_with_proxy_resources(): "base_url": "https://api.example.com", } - proxy_app = FastMCP.from_client(Client(api_app)) + proxy_app = FastMCP.as_proxy(Client(api_app)) await main_app.import_server("api", proxy_app) # Access the resource through the main app with the prefixed key @@ -379,7 +379,7 @@ async def test_import_with_proxy_resource_templates(): def create_user(name: str, email: str): return {"name": name, "email": email} - proxy_app = FastMCP.from_client(Client(api_app)) + proxy_app = FastMCP.as_proxy(Client(api_app)) await main_app.import_server("api", proxy_app) # Instantiate the template through the main app with the prefixed key diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 2d8d37c07..a9eeb5a8c 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -373,7 +373,7 @@ class TestProxyServer: return f"Data for {query}" # Create proxy server - proxy_server = FastMCP.from_client( + proxy_server = FastMCP.as_proxy( Client(transport=FastMCPTransport(original_server)) ) @@ -396,7 +396,7 @@ class TestProxyServer: original_server = FastMCP("OriginalServer") # Create proxy server - proxy_server = FastMCP.from_client( + proxy_server = FastMCP.as_proxy( Client(transport=FastMCPTransport(original_server)) ) @@ -428,7 +428,7 @@ class TestProxyServer: return {"api_key": "12345"} # Create proxy server - proxy_server = FastMCP.from_client( + proxy_server = FastMCP.as_proxy( Client(transport=FastMCPTransport(original_server)) ) @@ -452,7 +452,7 @@ class TestProxyServer: return f"Welcome, {name}!" # Create proxy server - proxy_server = FastMCP.from_client( + proxy_server = FastMCP.as_proxy( Client(transport=FastMCPTransport(original_server)) ) @@ -510,7 +510,7 @@ class TestAsProxyKwarg: async def test_as_proxy_ignored_for_proxy_mounts_default(self): mcp = FastMCP("Main") sub = FastMCP("Sub") - sub_proxy = FastMCP.from_client(Client(transport=FastMCPTransport(sub))) + sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub))) mcp.mount("sub", sub_proxy) @@ -519,7 +519,7 @@ class TestAsProxyKwarg: async def test_as_proxy_ignored_for_proxy_mounts_false(self): mcp = FastMCP("Main") sub = FastMCP("Sub") - sub_proxy = FastMCP.from_client(Client(transport=FastMCPTransport(sub))) + sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub))) mcp.mount("sub", sub_proxy, as_proxy=False) @@ -528,7 +528,7 @@ class TestAsProxyKwarg: async def test_as_proxy_ignored_for_proxy_mounts_true(self): mcp = FastMCP("Main") sub = FastMCP("Sub") - sub_proxy = FastMCP.from_client(Client(transport=FastMCPTransport(sub))) + sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub))) mcp.mount("sub", sub_proxy, as_proxy=True) diff --git a/tests/server/test_proxy.py b/tests/server/test_proxy.py index 31a1c74d4..7f018332c 100644 --- a/tests/server/test_proxy.py +++ b/tests/server/test_proxy.py @@ -66,7 +66,7 @@ def fastmcp_server(): @pytest.fixture async def proxy_server(fastmcp_server): """Fixture that creates a FastMCP proxy server.""" - return FastMCP.from_client(Client(transport=FastMCPTransport(fastmcp_server))) + return FastMCP.as_proxy(Client(transport=FastMCPTransport(fastmcp_server))) async def test_create_proxy(fastmcp_server): @@ -81,6 +81,29 @@ async def test_create_proxy(fastmcp_server): assert server.name == "FastMCP" +async def test_as_proxy_with_server(fastmcp_server): + """FastMCP.as_proxy should accept a FastMCP instance.""" + proxy = FastMCP.as_proxy(fastmcp_server) + result = await proxy._mcp_call_tool("greet", {"name": "Test"}) + assert isinstance(result[0], mcp.types.TextContent) + assert result[0].text == "Hello, Test!" + + +async def test_as_proxy_with_transport(fastmcp_server): + """FastMCP.as_proxy should accept a ClientTransport.""" + proxy = FastMCP.as_proxy(FastMCPTransport(fastmcp_server)) + result = await proxy._mcp_call_tool("greet", {"name": "Test"}) + assert isinstance(result[0], mcp.types.TextContent) + assert result[0].text == "Hello, Test!" + + +def test_as_proxy_with_url(): + """FastMCP.as_proxy should accept a URL without connecting.""" + proxy = FastMCP.as_proxy("http://example.com/mcp") + assert isinstance(proxy, FastMCPProxy) + assert repr(proxy.client.transport).startswith("