diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 7e3c481dc..95896a8e4 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -46,13 +46,13 @@ server2 = FastMCP("Server2", providers=[provider]) ### ProxyProvider -`ProxyProvider` (`src/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `FastMCP.as_proxy()` and `FastMCP.mount()` for remote server integration. +`ProxyProvider` (`src/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `create_proxy()` and `FastMCP.mount()` for remote server integration. ```python -from fastmcp import FastMCP +from fastmcp.server import create_proxy # Create proxy to remote server -server = FastMCP.as_proxy("http://remote-server/mcp") +server = create_proxy("http://remote-server/mcp") ``` ### OpenAPIProvider diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx index 3158a005a..598e4be5f 100644 --- a/docs/integrations/claude-desktop.mdx +++ b/docs/integrations/claude-desktop.mdx @@ -263,11 +263,11 @@ Users on Claude Pro, Max, Team, and Enterprise plans have first-class remote ser Create a proxy server that connects to a remote HTTP server: ```python proxy_server.py -from fastmcp import FastMCP +from fastmcp.server import create_proxy # Create a proxy to a remote server -proxy = FastMCP.as_proxy( - "https://example.com/mcp/sse", +proxy = create_proxy( + "https://example.com/mcp/sse", name="Remote Server Proxy" ) @@ -280,8 +280,9 @@ if __name__ == "__main__": For authenticated remote servers, create an authenticated client following the guidance in the [client auth documentation](/clients/auth/bearer) and pass it to the proxy: ```python auth_proxy_server.py {7} -from fastmcp import FastMCP, Client +from fastmcp import Client from fastmcp.client.auth import BearerAuth +from fastmcp.server import create_proxy # Create authenticated client client = Client( @@ -290,7 +291,7 @@ client = Client( ) # Create proxy using the authenticated client -proxy = FastMCP.as_proxy(client, name="Authenticated Proxy") +proxy = create_proxy(client, name="Authenticated Proxy") if __name__ == "__main__": proxy.run() diff --git a/docs/servers/providers/mounting.mdx b/docs/servers/providers/mounting.mdx index 887a82b87..f74603991 100644 --- a/docs/servers/providers/mounting.mdx +++ b/docs/servers/providers/mounting.mdx @@ -47,6 +47,77 @@ main.mount(weather_server) # Now main has access to get_forecast and data://cities ``` +## Mounting External Servers + +Mount remote HTTP servers or subprocess-based MCP servers using `create_proxy()`: + +```python +from fastmcp import FastMCP +from fastmcp.server import create_proxy + +mcp = FastMCP("Orchestrator") + +# Mount a remote HTTP server (URLs work directly) +mcp.mount(create_proxy("http://api.example.com/mcp"), namespace="api") + +# Mount local Python scripts (file paths work directly) +mcp.mount(create_proxy("./my_server.py"), namespace="local") +``` + +### Mounting npm/uvx Packages + +For npm packages or Python tools, use the config dict format: + +```python +from fastmcp import FastMCP +from fastmcp.server import create_proxy + +mcp = FastMCP("Orchestrator") + +# Mount npm package via config +github_config = { + "mcpServers": { + "default": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"] + } + } +} +mcp.mount(create_proxy(github_config), namespace="github") + +# Mount Python tool via config +sqlite_config = { + "mcpServers": { + "default": { + "command": "uvx", + "args": ["mcp-server-sqlite", "--db", "data.db"] + } + } +} +mcp.mount(create_proxy(sqlite_config), namespace="db") +``` + +Or use explicit transport classes: + +```python +from fastmcp import FastMCP +from fastmcp.server import create_proxy +from fastmcp.client.transports import NpxStdioTransport, UvxStdioTransport + +mcp = FastMCP("Orchestrator") + +mcp.mount( + create_proxy(NpxStdioTransport(package="@modelcontextprotocol/server-github")), + namespace="github" +) +mcp.mount( + create_proxy(UvxStdioTransport(tool_name="mcp-server-sqlite", tool_args=["--db", "data.db"])), + namespace="db" +) +``` + +For advanced configuration, see [Proxying](/servers/providers/proxy). + ## Namespacing @@ -145,18 +216,16 @@ main.mount(subserver, namespace="api") ### Proxy Mounting -The parent server treats the mounted server as a separate entity: + +The `as_proxy` parameter is deprecated. Mounted servers now always have their lifespan and middleware invoked. To create a proxy server explicitly, use `create_proxy()` from `fastmcp.server`. + -```python -main.mount(subserver, namespace="api", as_proxy=True) -``` +Previously, the parent server could treat the mounted server as a separate entity with its own lifecycle. This behavior is now the default for all mounted servers: - Full client lifecycle events on mounted server - Mounted server's lifespan is executed - Communication via in-memory Client transport -FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan. Override with `as_proxy=True` or `as_proxy=False`. - ## Tag Filtering diff --git a/docs/servers/providers/overview.mdx b/docs/servers/providers/overview.mdx index fe895a7a2..a9ffc9f62 100644 --- a/docs/servers/providers/overview.mdx +++ b/docs/servers/providers/overview.mdx @@ -37,7 +37,7 @@ FastMCP includes providers for common patterns: |----------|--------------|----------------| | `LocalProvider` | Stores components you define in code | `@mcp.tool`, `mcp.add_tool()` | | `FastMCPProvider` | Wraps another FastMCP server | `mcp.mount(server)` | -| `ProxyProvider` | Connects to remote MCP servers | `FastMCP.as_proxy(client)` | +| `ProxyProvider` | Connects to remote MCP servers | `create_proxy(client)` | | `TransformingProvider` | Adds prefixes to avoid name collisions | `mcp.mount(server, namespace="api")` | Most users only interact with `LocalProvider` (through decorators) and occasionally mount or proxy other servers. The provider abstraction stays invisible until you need it. diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx index 779cfc90a..5598aa047 100644 --- a/docs/servers/providers/proxy.mdx +++ b/docs/servers/providers/proxy.mdx @@ -36,17 +36,13 @@ sequenceDiagram -Create a proxy using `FastMCP.as_proxy()`: +Create a proxy using `create_proxy()`: ```python -from fastmcp import FastMCP -from fastmcp.server.proxy import ProxyClient +from fastmcp.server import create_proxy -# Create a proxy to a remote server -proxy = FastMCP.as_proxy( - ProxyClient("http://example.com/mcp"), - name="MyProxy" -) +# create_proxy() accepts URLs, file paths, and transports directly +proxy = create_proxy("http://example.com/mcp", name="MyProxy") if __name__ == "__main__": proxy.run() @@ -57,19 +53,19 @@ This gives you: - Automatic forwarding of MCP features (sampling, elicitation, etc.) - Session isolation to prevent context mixing + +To mount a proxy inside another FastMCP server, see [Mounting External Servers](/servers/providers/mounting#mounting-external-servers). + + ## Transport Bridging A common use case is bridging transports - making a remote server available locally: ```python -from fastmcp import FastMCP -from fastmcp.server.proxy import ProxyClient +from fastmcp.server import create_proxy # Bridge remote HTTP to local stdio -remote_proxy = FastMCP.as_proxy( - ProxyClient("http://example.com/mcp/sse"), - name="Remote-to-Local" -) +remote_proxy = create_proxy("http://example.com/mcp/sse", name="Remote-to-Local") # Run locally via stdio for Claude Desktop if __name__ == "__main__": @@ -79,11 +75,10 @@ if __name__ == "__main__": Or expose a local server via HTTP: ```python +from fastmcp.server import create_proxy + # Bridge local server to HTTP -local_proxy = FastMCP.as_proxy( - ProxyClient("local_server.py"), - name="Local-to-HTTP" -) +local_proxy = create_proxy("local_server.py", name="Local-to-HTTP") if __name__ == "__main__": local_proxy.run(transport="http", host="0.0.0.0", port=8080) @@ -93,13 +88,13 @@ if __name__ == "__main__": -ProxyClient provides session isolation - each request gets its own isolated backend session: +`create_proxy()` provides session isolation - each request gets its own isolated backend session: ```python -from fastmcp.server.proxy import ProxyClient +from fastmcp.server import create_proxy # Each request creates a fresh backend session (recommended) -proxy = FastMCP.as_proxy(ProxyClient("backend_server.py")) +proxy = create_proxy("backend_server.py") # Multiple clients can use this proxy simultaneously: # - Client A calls a tool → gets isolated session @@ -113,10 +108,11 @@ If you pass an already-connected client, the proxy reuses that session: ```python from fastmcp import Client +from fastmcp.server import create_proxy async with Client("backend_server.py") as connected_client: # This proxy reuses the connected session - proxy = FastMCP.as_proxy(connected_client) + proxy = create_proxy(connected_client) # ⚠️ Warning: All requests share the same session ``` @@ -129,7 +125,7 @@ Shared sessions may cause context mixing in concurrent scenarios. Use only in si -ProxyClient automatically forwards MCP protocol features: +Proxies automatically forward MCP protocol features: | Feature | Description | |---------|-------------| @@ -140,11 +136,10 @@ ProxyClient automatically forwards MCP protocol features: | Progress | Progress notifications | ```python -from fastmcp.server.proxy import ProxyClient +from fastmcp.server import create_proxy # All features forwarded automatically -backend = ProxyClient("advanced_backend.py") -proxy = FastMCP.as_proxy(backend) +proxy = create_proxy("advanced_backend.py") # When the backend: # - Requests LLM sampling → forwarded to your client @@ -157,6 +152,8 @@ proxy = FastMCP.as_proxy(backend) Selectively disable forwarding: ```python +from fastmcp.server.providers.proxy import ProxyClient + backend = ProxyClient( "backend_server.py", sampling_handler=None, # Disable LLM sampling @@ -171,7 +168,7 @@ backend = ProxyClient( Create proxies from configuration dictionaries: ```python -from fastmcp import FastMCP +from fastmcp.server import create_proxy config = { "mcpServers": { @@ -182,7 +179,7 @@ config = { } } -proxy = FastMCP.as_proxy(config, name="Config-Based Proxy") +proxy = create_proxy(config, name="Config-Based Proxy") ``` ### Multi-Server Proxies @@ -190,6 +187,8 @@ proxy = FastMCP.as_proxy(config, name="Config-Based Proxy") Combine multiple servers with automatic namespacing: ```python +from fastmcp.server import create_proxy + config = { "mcpServers": { "weather": { @@ -206,7 +205,7 @@ config = { # Creates unified proxy with prefixed components: # - weather_get_forecast # - calendar_add_event -composite = FastMCP.as_proxy(config, name="Composite") +composite = create_proxy(config, name="Composite") ``` ## Component Prefixing @@ -229,7 +228,10 @@ Components from a proxy server are "mirrored" - they reflect the remote server's To modify a proxied component (like disabling it), create a local copy: ```python -proxy = FastMCP.as_proxy("backend_server.py") +from fastmcp import FastMCP +from fastmcp.server import create_proxy + +proxy = create_proxy("backend_server.py") # Get mirrored tool mirrored_tool = await proxy.get_tool("useful_tool") @@ -258,12 +260,14 @@ When mounting proxy servers, this latency affects all operations on the parent s For low-latency requirements, consider using [`import_server()`](/servers/providers/mounting#static-importing) to copy tools at startup. -## Advanced: FastMCPProxy Class +## Advanced Usage + +### FastMCPProxy Class For explicit session control, use `FastMCPProxy` directly: ```python -from fastmcp.server.proxy import FastMCPProxy, ProxyClient +from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient # Custom session factory def create_client(): @@ -273,3 +277,25 @@ proxy = FastMCPProxy(client_factory=create_client) ``` This gives you full control over session creation and reuse strategies. + +### Adding Proxied Components to Existing Server + +Mount a proxy to add remote components to an existing server: + +```python +from fastmcp import FastMCP +from fastmcp.server import create_proxy + +server = FastMCP("My Server") + +# Add local tools +@server.tool +def local_tool() -> str: + return "Local result" + +# Mount proxied tools from remote server +remote = create_proxy("http://remote-server/mcp") +server.mount(remote) + +# Now server has both local and proxied tools +``` diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx index a7c2f4878..90cbcd7e9 100644 --- a/docs/servers/server.mdx +++ b/docs/servers/server.mdx @@ -288,17 +288,18 @@ main.mount(sub, namespace="sub") -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. +FastMCP can act as a proxy for any MCP server (local or remote) using `create_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. Proxies automatically handle concurrent operations safely by creating fresh sessions for each request when using disconnected clients. See the [Remote Proxies](/servers/providers/proxy) guide for details and advanced usage. ```python -from fastmcp import FastMCP, Client +from fastmcp import Client +from fastmcp.server import create_proxy backend = Client("http://example.com/mcp/sse") -proxy = FastMCP.as_proxy(backend, name="ProxyServer") +proxy = create_proxy(backend, name="ProxyServer") # Now use the proxy like any FastMCP server ``` diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index b82f82292..ece664b72 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -12,7 +12,7 @@ from typing import Any, Literal from mcp.server.fastmcp import FastMCP as FastMCP1x from watchfiles import Change, awatch -from fastmcp.server.server import FastMCP +from fastmcp.server.server import FastMCP, create_proxy from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_server_config import ( MCPServerConfig, @@ -45,7 +45,7 @@ def create_client_server(url: str) -> Any: import fastmcp client = fastmcp.Client(url) - server = fastmcp.FastMCP.as_proxy(client) + server = create_proxy(client) return server except Exception as e: logger.error(f"Failed to create client for URL {url}: {e}") @@ -54,12 +54,10 @@ def create_client_server(url: str) -> Any: def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]: """Create a FastMCP server from a MCPConfig.""" - from fastmcp import FastMCP - with mcp_config_path.open() as src: mcp_config = json.load(src) - server = FastMCP.as_proxy(mcp_config) + server = create_proxy(mcp_config) return server diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 34f9438d0..8365d4dce 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -43,7 +43,7 @@ from fastmcp.mcp_config import ( infer_transport_type_from_url, ) from fastmcp.server.dependencies import get_http_headers -from fastmcp.server.server import FastMCP +from fastmcp.server.server import FastMCP, create_proxy from fastmcp.server.tasks.capabilities import get_task_capabilities from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment @@ -1035,9 +1035,9 @@ class MCPConfigTransport(ClientTransport): transport = config.to_transport() client = ProxyClient(transport=transport, timeout=timeout) - proxy = FastMCP.as_proxy( + proxy = create_proxy( + client, name=f"Proxy-{name}", - backend=client, tool_transformations=tool_transforms, include_tags=include_tags, exclude_tags=exclude_tags, diff --git a/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py index 84a114c36..c55b0009b 100644 --- a/src/fastmcp/mcp_config.py +++ b/src/fastmcp/mcp_config.py @@ -95,20 +95,20 @@ class _TransformingMCPServerMixin(FastMCPBaseModel): client_name: str | None = None, ) -> tuple[FastMCP[Any], ClientTransport]: """Turn the Transforming MCPServer into a FastMCP Server and also return the underlying transport.""" - from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.client.transports import ( ClientTransport, # pyright: ignore[reportUnusedImport] ) + from fastmcp.server import create_proxy transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType] # ty: ignore[unresolved-attribute] transport = cast(ClientTransport, transport) client: Client[ClientTransport] = Client(transport=transport, name=client_name) - wrapped_mcp_server = FastMCP.as_proxy( + wrapped_mcp_server = create_proxy( + client, name=server_name, - backend=client, tool_transformations=self.tools, include_tags=self.include_tags, exclude_tags=self.exclude_tags, diff --git a/src/fastmcp/server/__init__.py b/src/fastmcp/server/__init__.py index 69ded232c..fb9afd895 100644 --- a/src/fastmcp/server/__init__.py +++ b/src/fastmcp/server/__init__.py @@ -1,6 +1,6 @@ -from .server import FastMCP from .context import Context +from .server import FastMCP, create_proxy from . import dependencies -__all__ = ["Context", "FastMCP"] +__all__ = ["Context", "FastMCP", "create_proxy"] diff --git a/src/fastmcp/server/providers/proxy.py b/src/fastmcp/server/providers/proxy.py index 65c954f78..f874e1bd4 100644 --- a/src/fastmcp/server/providers/proxy.py +++ b/src/fastmcp/server/providers/proxy.py @@ -53,6 +53,8 @@ from fastmcp.utilities.logging import get_logger if TYPE_CHECKING: from pathlib import Path + from fastmcp.client.transports import ClientTransport + logger = get_logger(__name__) # Type alias for client factory functions @@ -574,6 +576,59 @@ class ProxyProvider(Provider): # because client cleanup is handled per-request +# ----------------------------------------------------------------------------- +# Factory Functions +# ----------------------------------------------------------------------------- + + +def _create_client_factory( + target: ( + Client[ClientTransportT] + | ClientTransport + | FastMCP[Any] + | FastMCP1Server + | AnyUrl + | Path + | MCPConfig + | dict[str, Any] + | str + ), +) -> ClientFactoryT: + """Create a client factory from the given target. + + Internal helper that handles the session strategy based on the target type: + - Connected Client: reuses existing session (with warning about context mixing) + - Disconnected Client: creates fresh sessions per request + - Other targets: creates ProxyClient and fresh sessions per request + """ + if isinstance(target, Client): + client = target + if client.is_connected(): + logger.info( + "Proxy detected connected client - reusing existing session for all requests. " + "This may cause context mixing in concurrent scenarios." + ) + + def reuse_client_factory() -> Client: + return client + + return reuse_client_factory + else: + + def fresh_client_factory() -> Client: + return client.new() + + return fresh_client_factory + else: + # target is not a Client, so it's compatible with ProxyClient.__init__ + base_client = ProxyClient(cast(Any, target)) + + def proxy_client_factory() -> Client: + return base_client.new() + + return proxy_client_factory + + # ----------------------------------------------------------------------------- # FastMCPProxy - Convenience Wrapper # ----------------------------------------------------------------------------- @@ -587,14 +642,14 @@ class FastMCPProxy(FastMCP): Example: ```python - from fastmcp import FastMCP + from fastmcp.server import create_proxy from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient - # Create a proxy server - proxy = FastMCPProxy(client_factory=lambda: ProxyClient("http://localhost:8000/mcp")) + # Create a proxy server using create_proxy (recommended) + proxy = create_proxy("http://localhost:8000/mcp") - # Or use the convenience method - proxy = FastMCP.as_proxy("http://localhost:8000/mcp") + # Or use FastMCPProxy directly with explicit client factory + proxy = FastMCPProxy(client_factory=lambda: ProxyClient("http://localhost:8000/mcp")) ``` """ @@ -607,7 +662,7 @@ class FastMCPProxy(FastMCP): """Initialize the proxy server. FastMCPProxy requires explicit session management via client_factory. - Use FastMCP.as_proxy() for convenience with automatic session strategy. + Use create_proxy() for convenience with automatic session strategy. Args: client_factory: A callable that returns a Client instance when called. diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 964864c7c..c012c809a 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -2283,7 +2283,7 @@ class FastMCP(Generic[LifespanResultT]): namespace: Optional namespace to use for the mounted server's objects. If None, the server's objects are accessible with their original names. as_proxy: Deprecated. Mounted servers now always have their lifespan and - middleware invoked. To create a proxy server, use FastMCP.as_proxy() + middleware invoked. To create a proxy server, use create_proxy() explicitly before mounting. tool_names: Optional mapping of original tool names to custom names. Use this to override namespaced names. Keys are the original tool names from the @@ -2310,7 +2310,7 @@ class FastMCP(Generic[LifespanResultT]): warnings.warn( "as_proxy is deprecated and will be removed in a future version. " "Mounted servers now always have their lifespan and middleware invoked. " - "To create a proxy server, use FastMCP.as_proxy() explicitly.", + "To create a proxy server, use create_proxy() explicitly.", DeprecationWarning, stacklevel=2, ) @@ -2549,48 +2549,24 @@ class FastMCP(Generic[LifespanResultT]): ) -> FastMCPProxy: """Create a FastMCP proxy server for the given backend. + .. deprecated:: + Use :func:`fastmcp.server.create_proxy` instead. + This method will be removed in a future version. + The `backend` argument can be either an existing `fastmcp.client.Client` instance or any value accepted as the `transport` argument of `fastmcp.client.Client`. This mirrors the convenience of the `fastmcp.client.Client` constructor. """ - from fastmcp.client.client import Client - from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient - - if isinstance(backend, Client): - client = backend - # Session strategy based on client connection state: - # - Connected clients: reuse existing session for all requests - # - Disconnected clients: create fresh sessions per request for isolation - if client.is_connected(): - proxy_logger = get_logger(__name__) - proxy_logger.info( - "Proxy detected connected client - reusing existing session for all requests. " - "This may cause context mixing in concurrent scenarios." - ) - - # Reuse sessions - return the same client instance - def reuse_client_factory(): - return client - - client_factory = reuse_client_factory - else: - # Fresh sessions per request - def fresh_client_factory(): - return client.new() - - client_factory = fresh_client_factory - else: - # backend is not a Client, so it's compatible with ProxyClient.__init__ - base_client = ProxyClient(cast(Any, backend)) - - # Fresh client created from transport - use fresh sessions per request - def proxy_client_factory(): - return base_client.new() - - client_factory = proxy_client_factory - - return FastMCPProxy(client_factory=client_factory, **settings) + if fastmcp.settings.deprecation_warnings: + warnings.warn( + "FastMCP.as_proxy() is deprecated. Use create_proxy() from " + "fastmcp.server instead: `from fastmcp.server import create_proxy`", + DeprecationWarning, + stacklevel=2, + ) + # Call the module-level create_proxy function directly + return create_proxy(backend, **settings) @classmethod def generate_name(cls, name: str | None = None) -> str: @@ -2600,3 +2576,60 @@ class FastMCP(Generic[LifespanResultT]): return f"{class_name}-{secrets.token_hex(2)}" else: return f"{class_name}-{name}-{secrets.token_hex(2)}" + + +# ----------------------------------------------------------------------------- +# Module-level Factory Functions +# ----------------------------------------------------------------------------- + + +def create_proxy( + target: ( + Client[ClientTransportT] + | ClientTransport + | FastMCP[Any] + | FastMCP1Server + | AnyUrl + | Path + | MCPConfig + | dict[str, Any] + | str + ), + **settings: Any, +) -> FastMCPProxy: + """Create a FastMCP proxy server for the given target. + + This is the recommended way to create a proxy server. For lower-level control, + use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`. + + Args: + target: The backend to proxy to. Can be: + - A Client instance (connected or disconnected) + - A ClientTransport + - A FastMCP server instance + - A URL string or AnyUrl + - A Path to a server script + - An MCPConfig or dict + **settings: Additional settings passed to FastMCPProxy (name, etc.) + + Returns: + A FastMCPProxy server that proxies to the target. + + Example: + ```python + from fastmcp.server import create_proxy + + # Create a proxy to a remote server + proxy = create_proxy("http://remote-server/mcp") + + # Create a proxy to another FastMCP server + proxy = create_proxy(other_server) + ``` + """ + from fastmcp.server.providers.proxy import ( + FastMCPProxy, + _create_client_factory, + ) + + client_factory = _create_client_factory(target) + return FastMCPProxy(client_factory=client_factory, **settings) diff --git a/tests/server/proxy/__init__.py b/tests/server/providers/proxy/__init__.py similarity index 100% rename from tests/server/proxy/__init__.py rename to tests/server/providers/proxy/__init__.py diff --git a/tests/server/proxy/test_proxy_client.py b/tests/server/providers/proxy/test_proxy_client.py similarity index 100% rename from tests/server/proxy/test_proxy_client.py rename to tests/server/providers/proxy/test_proxy_client.py diff --git a/tests/server/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py similarity index 93% rename from tests/server/proxy/test_proxy_server.py rename to tests/server/providers/proxy/test_proxy_server.py index ce6d2046f..972cffb2d 100644 --- a/tests/server/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -14,7 +14,11 @@ from fastmcp.client import Client from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport from fastmcp.exceptions import ToolError from fastmcp.resources import ResourceContent, ResourceResult -from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient +from fastmcp.server import create_proxy +from fastmcp.server.providers.proxy import ( + FastMCPProxy, + ProxyClient, +) from fastmcp.tools.tool import ToolResult from fastmcp.tools.tool_transform import ( ToolTransformConfig, @@ -131,46 +135,76 @@ def fastmcp_server(): @pytest.fixture async def proxy_server(fastmcp_server): """Fixture that creates a FastMCP proxy server.""" - return FastMCP.as_proxy(ProxyClient(transport=FastMCPTransport(fastmcp_server))) + return create_proxy(ProxyClient(transport=FastMCPTransport(fastmcp_server))) -async def test_create_proxy(fastmcp_server): - """Test that the proxy server properly forwards requests to the original server.""" - # Create a client +async def test_create_proxy_with_client(fastmcp_server): + """Test create_proxy with a Client.""" client = ProxyClient(transport=FastMCPTransport(fastmcp_server)) - - server = FastMCPProxy.as_proxy(client) + server = create_proxy(client) assert isinstance(server, FastMCPProxy) assert isinstance(server, FastMCP) assert server.name.startswith("FastMCPProxy-") -async def test_as_proxy_with_server(fastmcp_server): - """FastMCP.as_proxy should accept a FastMCP instance.""" - proxy = FastMCP.as_proxy(fastmcp_server) +async def test_create_proxy_with_server(fastmcp_server): + """create_proxy should accept a FastMCP instance.""" + proxy = create_proxy(fastmcp_server) async with Client(proxy) as client: result = await client.call_tool("greet", {"name": "Test"}) assert result.data == "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)) +async def test_create_proxy_with_transport(fastmcp_server): + """create_proxy should accept a ClientTransport.""" + proxy = create_proxy(FastMCPTransport(fastmcp_server)) async with Client(proxy) as client: result = await client.call_tool("greet", {"name": "Test"}) assert result.data == "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/") +def test_create_proxy_with_url(): + """create_proxy should accept a URL without connecting.""" + proxy = create_proxy("http://example.com/mcp/") assert isinstance(proxy, FastMCPProxy) client = cast(Client, proxy.client_factory()) assert isinstance(client.transport, StreamableHttpTransport) assert client.transport.url == "http://example.com/mcp/" +# --- Deprecated as_proxy tests (verify backwards compatibility) --- + + +async def test_as_proxy_deprecated_with_server(fastmcp_server): + """FastMCP.as_proxy should work but emit deprecation warning.""" + import warnings + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + proxy = FastMCP.as_proxy(fastmcp_server) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "create_proxy" in str(w[0].message) + + async with Client(proxy) as client: + result = await client.call_tool("greet", {"name": "Test"}) + assert result.data == "Hello, Test!" + + +def test_as_proxy_deprecated_with_url(): + """FastMCP.as_proxy should work but emit deprecation warning.""" + import warnings + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + proxy = FastMCP.as_proxy("http://example.com/mcp/") + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + + assert isinstance(proxy, FastMCPProxy) + + async def test_proxy_with_async_client_factory(): """FastMCPProxy should accept an async client_factory.""" diff --git a/tests/server/proxy/test_stateful_proxy_client.py b/tests/server/providers/proxy/test_stateful_proxy_client.py similarity index 100% rename from tests/server/proxy/test_stateful_proxy_client.py rename to tests/server/providers/proxy/test_stateful_proxy_client.py