diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx index a8acad175..955ce11bf 100644 --- a/docs/getting-started/upgrading/from-fastmcp-2.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx @@ -331,22 +331,22 @@ Without the extra, configuring a tool with `task=True` or `TaskConfig` will rais ### Deprecated Features -These still work but emit warnings. Update when convenient. +These were deprecated in v3. Items marked **Removed in v4** no longer work at all — update to the replacement shown. The rest still work but emit warnings; update when convenient. -**mount() prefix → namespace** +**mount() prefix → namespace** (Removed in v4) ```python -# Deprecated +# Removed in v4 main.mount(subserver, prefix="api") # New main.mount(subserver, namespace="api") ``` -**import_server() → mount()** +**import_server() → mount()** (Removed in v4) ```python -# Deprecated +# Removed in v4 main.import_server(subserver) # New @@ -380,10 +380,10 @@ from fastmcp.server.providers.openapi import OpenAPIProvider server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)]) ``` -**add_tool_transformation() → add_transform()** +**add_tool_transformation() → add_transform()** (Removed in v4) ```python -# Deprecated +# Removed in v4 mcp.add_tool_transformation("name", config) # New @@ -391,15 +391,19 @@ from fastmcp.server.transforms import ToolTransform mcp.add_transform(ToolTransform({"name": config})) ``` -**FastMCP.as_proxy() → create_proxy()** +**FastMCP.as_proxy() → create_proxy()** (Removed in v4) + +The proxy target is passed positionally in both APIs, so most calls migrate unchanged. If you passed the target by keyword, note that the parameter was renamed from `backend=` to `target=`. ```python -# Deprecated +# Removed in v4 proxy = FastMCP.as_proxy("http://example.com/mcp") +proxy = FastMCP.as_proxy(backend="http://example.com/mcp") # keyword form # New from fastmcp.server import create_proxy proxy = create_proxy("http://example.com/mcp") +proxy = create_proxy(target="http://example.com/mcp") # as_proxy(backend=X) → create_proxy(target=X) ``` ## v2.14.0 diff --git a/examples/in_memory_proxy_example.py b/examples/in_memory_proxy_example.py index 45e7a10b2..305e9aba2 100644 --- a/examples/in_memory_proxy_example.py +++ b/examples/in_memory_proxy_example.py @@ -3,7 +3,7 @@ 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 proxy FastMCP server using ``FastMCP.as_proxy(original_server)``. +2. Create a proxy FastMCP server using ``create_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. """ @@ -13,6 +13,7 @@ from mcp.types import TextContent from fastmcp import FastMCP from fastmcp.client import Client +from fastmcp.server import create_proxy class EchoService: @@ -37,10 +38,8 @@ async def main(): # 2. Proxy Server Creation print("\nStep 2: Creating the Proxy Server (InMemoryProxy)...") - print( - f" (Using FastMCP.as_proxy to wrap '{original_server.name}' directly)" - ) - proxy_server = FastMCP.as_proxy(original_server, name="InMemoryProxy") + print(f" (Using create_proxy to wrap '{original_server.name}' directly)") + proxy_server = create_proxy(original_server, name="InMemoryProxy") print( f" -> Proxy Server '{proxy_server.name}' created, proxying '{original_server.name}'." ) diff --git a/examples/mount_example.py b/examples/mount_example.py index 9e8c0fb42..5844ebe4c 100644 --- a/examples/mount_example.py +++ b/examples/mount_example.py @@ -63,9 +63,9 @@ def check_app_status() -> dict[str, str]: # Mount sub-applications -app.mount(server=weather_app, prefix="weather") +app.mount(server=weather_app, namespace="weather") -app.mount(server=news_app, prefix="news") +app.mount(server=news_app, namespace="news") async def get_server_details(): diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py index dc64c44c2..ed0112256 100644 --- a/fastmcp_slim/fastmcp/server/server.py +++ b/fastmcp_slim/fastmcp/server/server.py @@ -4,9 +4,7 @@ from __future__ import annotations import asyncio import logging -import re import secrets -import warnings from collections.abc import ( AsyncIterator, Callable, @@ -47,7 +45,6 @@ from fastmcp.apps.config import ( ) from fastmcp.exceptions import ( AuthorizationError, - FastMCPDeprecationWarning, FastMCPError, NotFoundError, PromptError, @@ -179,9 +176,6 @@ def _check_removed_kwargs(kwargs: dict[str, Any]) -> None: Transport = Literal["stdio", "http", "sse", "streamable-http"] -# Compiled URI parsing regex to split a URI into protocol and path components -URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$") - LifespanCallable = Callable[ ["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT] @@ -624,38 +618,6 @@ class FastMCP( """ self._transforms.append(transform) - def add_tool_transformation( - self, tool_name: str, transformation: ToolTransformConfig - ) -> None: - """Add a tool transformation. - - .. deprecated:: - Use ``add_transform(ToolTransform({...}))`` instead. - """ - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "add_tool_transformation is deprecated. Use " - "server.add_transform(ToolTransform({tool_name: config})) instead.", - FastMCPDeprecationWarning, - stacklevel=2, - ) - self.add_transform(ToolTransform({tool_name: transformation})) - - def remove_tool_transformation(self, _tool_name: str) -> None: - """Remove a tool transformation. - - .. deprecated:: - Tool transformations are now immutable. Use enable/disable controls instead. - """ - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "remove_tool_transformation is deprecated and has no effect. " - "Transforms are immutable once added. Use server.disable(keys=[...]) " - "to hide tools instead.", - FastMCPDeprecationWarning, - stacklevel=2, - ) - async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]: """List all enabled tools from providers. @@ -1674,35 +1636,6 @@ class FastMCP( """ return self._local_provider.add_tool(tool) - def remove_tool(self, name: str, version: str | None = None) -> None: - """Remove tool(s) from the server. - - .. deprecated:: - Use ``mcp.local_provider.remove_tool(name)`` instead. - - Args: - name: The name of the tool to remove. - version: If None, removes ALL versions. If specified, removes only that version. - - Raises: - NotFoundError: If no matching tool is found. - """ - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "remove_tool() is deprecated. Use " - "mcp.local_provider.remove_tool(name) instead.", - FastMCPDeprecationWarning, - stacklevel=2, - ) - try: - self._local_provider.remove_tool(name, version) - except KeyError: - if version is None: - raise NotFoundError(f"Tool {name!r} not found") from None - raise NotFoundError( - f"Tool {name!r} version {version!r} not found" - ) from None - @overload def tool( self, @@ -2138,17 +2071,14 @@ class FastMCP( self, server: FastMCP[LifespanResultT], namespace: str | None = None, - as_proxy: bool | None = None, tool_names: dict[str, str] | None = None, - prefix: str | None = None, # deprecated, use namespace ) -> None: """Mount another FastMCP server on this server with an optional namespace. - Unlike importing (with import_server), mounting establishes a dynamic connection - between servers. When a client interacts with a mounted server's objects through - the parent server, requests are forwarded to the mounted server in real-time. - This means changes to the mounted server are immediately reflected when accessed - through the parent. + Mounting establishes a dynamic connection between servers. When a client + interacts with a mounted server's objects through the parent server, requests + are forwarded to the mounted server in real-time. This means changes to the + mounted server are immediately reflected when accessed through the parent. When a server is mounted with a namespace: - Tools from the mounted server are accessible with namespaced names. @@ -2174,48 +2104,15 @@ class FastMCP( server: The FastMCP server to mount. 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 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 mounted server. - prefix: Deprecated. Use namespace instead. """ - import warnings - from fastmcp.server.providers.fastmcp_provider import FastMCPProvider if server is self: raise ValueError("Cannot mount a server onto itself") - # Handle deprecated prefix parameter - if prefix is not None: - warnings.warn( - "The 'prefix' parameter is deprecated, use 'namespace' instead", - FastMCPDeprecationWarning, - stacklevel=2, - ) - if namespace is None: - namespace = prefix - else: - raise ValueError("Cannot specify both 'prefix' and 'namespace'") - - if as_proxy is not None: - 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 create_proxy() explicitly.", - FastMCPDeprecationWarning, - stacklevel=2, - ) - # Still honor the flag for backward compatibility - if as_proxy: - from fastmcp.server.providers.proxy import FastMCPProxy - - if not isinstance(server, FastMCPProxy): - server = FastMCP.as_proxy(server) - # Warn if parent masks errors but child doesn't (or vice versa) if self._mask_error_details and not server._mask_error_details: logger.warning( @@ -2240,105 +2137,6 @@ class FastMCP( # Use add_provider with namespace (applies namespace in AggregateProvider) self.add_provider(provider, namespace=namespace or "") - async def import_server( - self, - server: FastMCP[LifespanResultT], - prefix: str | None = None, - ) -> None: - """ - Import the MCP objects from another FastMCP server into this one, - optionally with a given prefix. - - .. deprecated:: - Use :meth:`mount` instead. ``import_server`` will be removed in a - future version. - - Note that when a server is *imported*, its objects are immediately - registered to the importing server. This is a one-time operation and - future changes to the imported server will not be reflected in the - importing server. Server-level configurations and lifespans are not imported. - - When a server is imported with a prefix: - - The tools are imported with prefixed names - Example: If server has a tool named "get_weather", it will be - available as "prefix_get_weather" - - The resources are imported with prefixed URIs using the new format - Example: If server has a resource with URI "weather://forecast", it will - be available as "weather://prefix/forecast" - - The templates are imported with prefixed URI templates using the new format - Example: If server has a template with URI "weather://location/{id}", it will - be available as "weather://prefix/location/{id}" - - The prompts are imported with prefixed names - Example: If server has a prompt named "weather_prompt", it will be available as - "prefix_weather_prompt" - - When a server is imported without a prefix (prefix=None), its tools, resources, - templates, and prompts are imported with their original names. - - Args: - server: The FastMCP server to import - prefix: Optional prefix to use for the imported server's objects. If None, - objects are imported with their original names. - """ - import warnings - - warnings.warn( - "import_server is deprecated, use mount() instead", - FastMCPDeprecationWarning, - stacklevel=2, - ) - - def add_resource_prefix(uri: str, prefix: str) -> str: - """Add prefix to resource URI: protocol://path → protocol://prefix/path.""" - match = URI_PATTERN.match(uri) - if match: - protocol, path = match.groups() - return f"{protocol}{prefix}/{path}" - return uri - - # Import tools from the server - for tool in await server.list_tools(): - if prefix: - tool = tool.model_copy(update={"name": f"{prefix}_{tool.name}"}) - self.add_tool(tool) - - # Import resources and templates from the server - for resource in await server.list_resources(): - if prefix: - new_uri = add_resource_prefix(str(resource.uri), prefix) - resource = resource.model_copy(update={"uri": new_uri}) - self.add_resource(resource) - - for template in await server.list_resource_templates(): - if prefix: - new_uri_template = add_resource_prefix(template.uri_template, prefix) - template = template.model_copy( - update={"uri_template": new_uri_template} - ) - self.add_template(template) - - # Import prompts from the server - for prompt in await server.list_prompts(): - if prefix: - prompt = prompt.model_copy(update={"name": f"{prefix}_{prompt.name}"}) - self.add_prompt(prompt) - - if server._lifespan != default_lifespan: - from warnings import warn - - warn( - message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.", - category=RuntimeWarning, - stacklevel=2, - ) - - if prefix: - logger.debug( - f"[{self.name}] Imported server {server.name} with prefix '{prefix}'" - ) - else: - logger.debug(f"[{self.name}] Imported server {server.name}") - @classmethod def from_openapi( cls, @@ -2445,43 +2243,6 @@ class FastMCP( ) return cls(name=server_name, providers=[provider], **settings) - @classmethod - def as_proxy( - cls, - backend: ( - Client[ClientTransportT] - | ClientTransport - | FastMCP[Any] - | SDKServer - | AnyUrl - | Path - | MCPConfig - | dict[str, Any] - | str - ), - **settings: Any, - ) -> 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. - """ - 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`", - FastMCPDeprecationWarning, - 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: class_name = cls.__name__ diff --git a/tests/client/test_openapi.py b/tests/client/test_openapi.py index fa3dcd3ca..236e6efb3 100644 --- a/tests/client/test_openapi.py +++ b/tests/client/test_openapi.py @@ -6,6 +6,7 @@ from mcp_types import TextResourceContents from fastmcp import Client, FastMCP from fastmcp.client.transports import SSETransport, StreamableHttpTransport +from fastmcp.server import create_proxy from fastmcp.server.providers.openapi import MCPType, RouteMap from fastmcp.utilities.tests import run_server_async @@ -63,7 +64,7 @@ async def sse_server(): @pytest.fixture async def proxy_server(shttp_server: str): """Start a proxy server.""" - proxy = FastMCP.as_proxy(StreamableHttpTransport(shttp_server)) + proxy = create_proxy(StreamableHttpTransport(shttp_server)) async with run_server_async(proxy, transport="http") as url: yield url diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index 3d1335ae5..af4995203 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -7,7 +7,7 @@ import weakref import psutil import pytest -from fastmcp import Client, FastMCP +from fastmcp import Client from fastmcp.client.transports import PythonStdioTransport, StdioTransport @@ -46,10 +46,12 @@ class TestParallelCalls: return script_file async def test_parallel_calls(self, stdio_script): + from fastmcp.server import create_proxy + backend_transport = PythonStdioTransport(script_path=stdio_script) backend_client = Client(transport=backend_transport) - proxy = FastMCP.as_proxy(backend=backend_client, name="PROXY") + proxy = create_proxy(backend_client, name="PROXY") count = 10 diff --git a/tests/deprecated/test_add_tool_transformation.py b/tests/deprecated/test_add_tool_transformation.py deleted file mode 100644 index 0228b8b60..000000000 --- a/tests/deprecated/test_add_tool_transformation.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Tests for deprecated add_tool_transformation API.""" - -import warnings - -from fastmcp import FastMCP -from fastmcp.client import Client -from fastmcp.tools.tool_transform import ToolTransformConfig - - -class TestAddToolTransformationDeprecated: - """Test that add_tool_transformation still works but emits deprecation warning.""" - - async def test_add_tool_transformation_emits_warning(self): - """add_tool_transformation should emit deprecation warning.""" - mcp = FastMCP("test") - - @mcp.tool - def my_tool() -> str: - return "hello" - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - mcp.add_tool_transformation( - "my_tool", ToolTransformConfig(name="renamed_tool") - ) - - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "add_tool_transformation is deprecated" in str(w[0].message) - - async def test_add_tool_transformation_still_works(self): - """add_tool_transformation should still apply the transformation.""" - mcp = FastMCP("test") - - @mcp.tool - def verbose_tool_name() -> str: - return "result" - - # Suppress warning for this test - we just want to verify it works - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - mcp.add_tool_transformation( - "verbose_tool_name", ToolTransformConfig(name="short") - ) - - async with Client(mcp) as client: - tools = await client.list_tools() - tool_names = [t.name for t in tools] - - # Original name should be gone, renamed version should exist - assert "verbose_tool_name" not in tool_names - assert "short" in tool_names - - # Should be callable by new name - result = await client.call_tool("short", {}) - assert result.content[0].text == "result" - - async def test_remove_tool_transformation_emits_warning(self): - """remove_tool_transformation should emit deprecation warning.""" - mcp = FastMCP("test") - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - mcp.remove_tool_transformation("any_tool") - - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "remove_tool_transformation is deprecated" in str(w[0].message) - assert "no effect" in str(w[0].message) - - async def test_tool_transformations_constructor_raises_type_error(self): - """tool_transformations constructor param should raise TypeError.""" - import pytest - - with pytest.raises(TypeError, match="no longer accepts `tool_transformations`"): - FastMCP( - "test", - tool_transformations={"my_tool": ToolTransformConfig(name="renamed")}, - ) diff --git a/tests/deprecated/test_import_server.py b/tests/deprecated/test_import_server.py deleted file mode 100644 index d43382ad6..000000000 --- a/tests/deprecated/test_import_server.py +++ /dev/null @@ -1,714 +0,0 @@ -import json -from urllib.parse import quote - -from mcp_types import TextContent, TextResourceContents - -from fastmcp.client.client import Client -from fastmcp.server.server import FastMCP -from fastmcp.tools.base import Tool -from fastmcp.tools.function_tool import FunctionTool -from tests.conftest import get_fn_name - - -async def test_import_basic_functionality(): - """Test that the import method properly imports tools and other resources.""" - # Create main app and sub-app - main_app = FastMCP("MainApp") - sub_app = FastMCP("SubApp") - - # Add a tool to the sub-app - @sub_app.tool - def sub_tool() -> str: - return "This is from the sub app" - - # Import the sub-app to the main app - await main_app.import_server(sub_app, "sub") - - # Verify the tool was imported with the prefix - main_tools = await main_app.list_tools() - sub_tools = await sub_app.list_tools() - assert any(t.name == "sub_sub_tool" for t in main_tools) - assert any(t.name == "sub_tool" for t in sub_tools) - - # Verify the original tool still exists in the sub-app - tool = await main_app.get_tool("sub_sub_tool") - assert tool is not None - # import_server creates copies with prefixed names (unlike mount which proxies) - assert tool.name == "sub_sub_tool" - assert isinstance(tool, FunctionTool) - assert callable(tool.fn) - - -async def test_import_multiple_apps(): - """Test importing multiple apps to a main app.""" - # Create main app and multiple sub-apps - main_app = FastMCP("MainApp") - weather_app = FastMCP("WeatherApp") - news_app = FastMCP("NewsApp") - - # Add tools to each sub-app - @weather_app.tool - def get_forecast() -> str: - return "Weather forecast" - - @news_app.tool - def get_headlines() -> str: - return "News headlines" - - # Import both sub-apps to the main app - await main_app.import_server(weather_app, "weather") - await main_app.import_server(news_app, "news") - - # Verify tools were imported with the correct prefixes - tools = await main_app.list_tools() - assert any(t.name == "weather_get_forecast" for t in tools) - assert any(t.name == "news_get_headlines" for t in tools) - - -async def test_import_combines_tools(): - """Test that importing preserves existing tools with the same prefix.""" - # Create apps - main_app = FastMCP("MainApp") - first_app = FastMCP("FirstApp") - second_app = FastMCP("SecondApp") - - # Add tools to each sub-app - @first_app.tool - def first_tool() -> str: - return "First app tool" - - @second_app.tool - def second_tool() -> str: - return "Second app tool" - - # Import first app - await main_app.import_server(first_app, "api") - tools = await main_app.list_tools() - assert any(t.name == "api_first_tool" for t in tools) - - # Import second app to same prefix - await main_app.import_server(second_app, "api") - - # Verify second tool is there - tools = await main_app.list_tools() - assert any(t.name == "api_second_tool" for t in tools) - - # Tools from both imports are combined - assert any(t.name == "api_first_tool" for t in tools) - - -async def test_import_with_resources(): - """Test importing with resources.""" - # Create apps - main_app = FastMCP("MainApp") - data_app = FastMCP("DataApp") - - # Add a resource to the data app - @data_app.resource(uri="data://users") - async def get_users() -> str: - return "user1, user2" - - # Import the data app - await main_app.import_server(data_app, "data") - - # Verify the resource was imported with the prefix - resources = await main_app.list_resources() - assert any(str(r.uri) == "data://data/users" for r in resources) - - -async def test_import_with_resource_templates(): - """Test importing with resource templates.""" - # Create apps - main_app = FastMCP("MainApp") - user_app = FastMCP("UserApp") - - # Add a resource template to the user app - @user_app.resource(uri="users://{user_id}/profile") - def get_user_profile(user_id: str) -> str: - import json - - return json.dumps( - {"id": user_id, "name": f"User {user_id}"}, separators=(",", ":") - ) - - # Import the user app - await main_app.import_server(user_app, "api") - - # Verify the template was imported with the prefix - templates = await main_app.list_resource_templates() - assert any(t.uri_template == "users://api/{user_id}/profile" for t in templates) - - -async def test_import_with_prompts(): - """Test importing with prompts.""" - # Create apps - main_app = FastMCP("MainApp") - assistant_app = FastMCP("AssistantApp") - - # Add a prompt to the assistant app - @assistant_app.prompt - def greeting(name: str) -> str: - return f"Hello, {name}!" - - # Import the assistant app - await main_app.import_server(assistant_app, "assistant") - - # Verify the prompt was imported with the prefix - prompts = await main_app.list_prompts() - assert any(p.name == "assistant_greeting" for p in prompts) - - -async def test_import_multiple_resource_templates(): - """Test importing multiple apps with resource templates.""" - # Create apps - main_app = FastMCP("MainApp") - weather_app = FastMCP("WeatherApp") - news_app = FastMCP("NewsApp") - - # Add templates to each app - @weather_app.resource(uri="weather://{city}") - def get_weather(city: str) -> str: - return f"Weather for {city}" - - @news_app.resource(uri="news://{category}") - def get_news(category: str) -> str: - return f"News for {category}" - - # Import both apps - await main_app.import_server(weather_app, "data") - await main_app.import_server(news_app, "content") - - # Verify templates were imported with correct prefixes - templates = await main_app.list_resource_templates() - assert any(t.uri_template == "weather://data/{city}" for t in templates) - assert any(t.uri_template == "news://content/{category}" for t in templates) - - -async def test_import_multiple_prompts(): - """Test importing multiple apps with prompts.""" - # Create apps - main_app = FastMCP("MainApp") - python_app = FastMCP("PythonApp") - sql_app = FastMCP("SQLApp") - - # Add prompts to each app - @python_app.prompt - def review_python(code: str) -> str: - return f"Reviewing Python code:\n{code}" - - @sql_app.prompt - def explain_sql(query: str) -> str: - return f"Explaining SQL query:\n{query}" - - # Import both apps - await main_app.import_server(python_app, "python") - await main_app.import_server(sql_app, "sql") - - # Verify prompts were imported with correct prefixes - prompts = await main_app.list_prompts() - assert any(p.name == "python_review_python" for p in prompts) - assert any(p.name == "sql_explain_sql" for p in prompts) - - -async def test_tool_custom_name_preserved_when_imported(): - """Test that a tool's custom name is preserved when imported.""" - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - def fetch_data(query: str) -> str: - return f"Data for query: {query}" - - api_app.add_tool(Tool.from_function(fetch_data, name="get_data")) - await main_app.import_server(api_app, "api") - - # Check that the tool is accessible by its prefixed name - tool = await main_app.get_tool("api_get_data") - assert tool is not None - - # Check that the function name is preserved - assert isinstance(tool, FunctionTool) - assert get_fn_name(tool.fn) == "fetch_data" - - -async def test_call_imported_custom_named_tool(): - """Test calling an imported tool with a custom name.""" - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - def fetch_data(query: str) -> str: - return f"Data for query: {query}" - - api_app.add_tool(Tool.from_function(fetch_data, name="get_data")) - await main_app.import_server(api_app, "api") - - async with Client(main_app) as client: - result = await client.call_tool("api_get_data", {"query": "test"}) - assert result.data == "Data for query: test" - - -async def test_first_level_importing_with_custom_name(): - """Test that a tool with a custom name is correctly imported at the first level.""" - service_app = FastMCP("ServiceApp") - provider_app = FastMCP("ProviderApp") - - def calculate_value(input: int) -> int: - return input * 2 - - provider_app.add_tool(Tool.from_function(calculate_value, name="compute")) - await service_app.import_server(provider_app, "provider") - - # Tool is accessible in the service app with the first prefix - tool = await service_app.get_tool("provider_compute") - assert tool is not None - assert isinstance(tool, FunctionTool) - assert get_fn_name(tool.fn) == "calculate_value" - - -async def test_nested_importing_preserves_prefixes(): - """Test that importing a previously imported app preserves prefixes.""" - main_app = FastMCP("MainApp") - service_app = FastMCP("ServiceApp") - provider_app = FastMCP("ProviderApp") - - def calculate_value(input: int) -> int: - return input * 2 - - provider_app.add_tool(Tool.from_function(calculate_value, name="compute")) - await service_app.import_server(provider_app, "provider") - await main_app.import_server(service_app, "service") - - # Tool is accessible in the main app with both prefixes - tool = await main_app.get_tool("service_provider_compute") - assert tool is not None - - -async def test_call_nested_imported_tool(): - """Test calling a tool through multiple levels of importing.""" - main_app = FastMCP("MainApp") - service_app = FastMCP("ServiceApp") - provider_app = FastMCP("ProviderApp") - - def calculate_value(input: int) -> int: - return input * 2 - - provider_app.add_tool(Tool.from_function(calculate_value, name="compute")) - await service_app.import_server(provider_app, "provider") - await main_app.import_server(service_app, "service") - - async with Client(main_app) as client: - result = await client.call_tool("service_provider_compute", {"input": 21}) - assert result.data == 42 - - -async def test_import_with_proxy_tools(): - """ - Test importing with tools that have custom names (proxy tools). - - This tests that the tool's name doesn't change even though the registered - name does, which is important because we need to forward that name to the - proxy server correctly. - """ - # Create apps - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - @api_app.tool - def get_data(query: str) -> str: - return f"Data for query: {query}" - - proxy_app = FastMCP.as_proxy(api_app) - await main_app.import_server(proxy_app, "api") - - async with Client(main_app) as client: - result = await client.call_tool("api_get_data", {"query": "test"}) - assert result.data == "Data for query: test" - - -async def test_import_with_proxy_prompts(): - """ - Test importing with prompts that have custom keys. - - This tests that the prompt's name doesn't change even though the registered - key does, which is important for correct rendering. - """ - # Create apps - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - @api_app.prompt - def greeting(name: str) -> str: - """Example greeting prompt.""" - return f"Hello, {name} from API!" - - proxy_app = FastMCP.as_proxy(api_app) - await main_app.import_server(proxy_app, "api") - - async with Client(main_app) as client: - result = await client.get_prompt("api_greeting", {"name": "World"}) - assert isinstance(result.messages[0].content, TextContent) - assert result.messages[0].content.text == "Hello, World from API!" - assert result.description == "Example greeting prompt." - - -async def test_import_with_proxy_resources(): - """ - Test importing with resources that have custom keys. - - This tests that the resource's name doesn't change even though the registered - key does, which is important for correct access. - """ - # Create apps - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - # Create a resource in the API app - @api_app.resource(uri="config://settings") - def get_config() -> str: - import json - - return json.dumps( - { - "api_key": "12345", - "base_url": "https://api.example.com", - } - ) - - proxy_app = FastMCP.as_proxy(api_app) - await main_app.import_server(proxy_app, "api") - - # Access the resource through the main app with the prefixed key - async with Client(main_app) as client: - result = await client.read_resource("config://api/settings") - assert isinstance(result[0], TextResourceContents) - content = json.loads(result[0].text) - assert content["api_key"] == "12345" - assert content["base_url"] == "https://api.example.com" - - -async def test_import_with_proxy_resource_templates(): - """ - Test importing with resource templates that have custom keys. - - This tests that the template's name doesn't change even though the registered - key does, which is important for correct instantiation. - """ - # Create apps - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - # Create a resource template in the API app - @api_app.resource(uri="user://{name}/{email}") - def create_user(name: str, email: str) -> str: - import json - - return json.dumps({"name": name, "email": email}) - - proxy_app = FastMCP.as_proxy(api_app) - await main_app.import_server(proxy_app, "api") - - # Instantiate the template through the main app with the prefixed key - - quoted_name = quote("John Doe", safe="") - quoted_email = quote("john@example.com", safe="") - async with Client(main_app) as client: - result = await client.read_resource(f"user://api/{quoted_name}/{quoted_email}") - assert isinstance(result[0], TextResourceContents) - content = json.loads(result[0].text) - assert content["name"] == "John Doe" - assert content["email"] == "john@example.com" - - -async def test_import_with_no_prefix(): - """Test importing a server without providing a prefix.""" - main_app = FastMCP("MainApp") - sub_app = FastMCP("SubApp") - - @sub_app.tool - def sub_tool() -> str: - return "Sub tool result" - - @sub_app.resource(uri="data://config") - def sub_resource(): - return "Sub resource data" - - @sub_app.resource(uri="users://{user_id}/info") - def sub_template(user_id: str): - return f"Sub template for user {user_id}" - - @sub_app.prompt - def sub_prompt() -> str: - return "Sub prompt content" - - # Import without prefix - await main_app.import_server(sub_app) - - # Verify all component types are accessible with original names - tools = await main_app.list_tools() - resources = await main_app.list_resources() - templates = await main_app.list_resource_templates() - prompts = await main_app.list_prompts() - assert any(t.name == "sub_tool" for t in tools) - assert any(str(r.uri) == "data://config" for r in resources) - assert any(t.uri_template == "users://{user_id}/info" for t in templates) - assert any(p.name == "sub_prompt" for p in prompts) - - # Test actual functionality through Client - async with Client(main_app) as client: - # Test tool - tool_result = await client.call_tool("sub_tool", {}) - assert tool_result.data == "Sub tool result" - - # Test resource - resource_result = await client.read_resource("data://config") - assert isinstance(resource_result[0], TextResourceContents) - assert resource_result[0].text == "Sub resource data" - - # Test template - template_result = await client.read_resource("users://123/info") - assert isinstance(template_result[0], TextResourceContents) - assert template_result[0].text == "Sub template for user 123" - - # Test prompt - prompt_result = await client.get_prompt("sub_prompt", {}) - assert prompt_result.messages is not None - assert isinstance(prompt_result.messages[0].content, TextContent) - assert prompt_result.messages[0].content.text == "Sub prompt content" - - -async def test_import_conflict_resolution_tools(): - """Test that later imported tools overwrite earlier ones when names conflict.""" - main_app = FastMCP("MainApp") - first_app = FastMCP("FirstApp") - second_app = FastMCP("SecondApp") - - @first_app.tool(name="shared_tool") - def first_shared_tool() -> str: - return "First app tool" - - @second_app.tool(name="shared_tool") - def second_shared_tool() -> str: - return "Second app tool" - - # Import both apps without prefix - await main_app.import_server(first_app) - await main_app.import_server(second_app) - - async with Client(main_app) as client: - # The later imported server should win - tools = await client.list_tools() - tool_names = [t.name for t in tools] - assert "shared_tool" in tool_names - assert tool_names.count("shared_tool") == 1 # Should only appear once - - result = await client.call_tool("shared_tool", {}) - assert result.data == "Second app tool" - - -async def test_import_conflict_resolution_resources(): - """Test that later imported resources overwrite earlier ones when URIs conflict.""" - main_app = FastMCP("MainApp") - first_app = FastMCP("FirstApp") - second_app = FastMCP("SecondApp") - - @first_app.resource(uri="shared://data") - def first_resource(): - return "First app data" - - @second_app.resource(uri="shared://data") - def second_resource(): - return "Second app data" - - # Import both apps without prefix - await main_app.import_server(first_app) - await main_app.import_server(second_app) - - async with Client(main_app) as client: - # The later imported server should win - resources = await client.list_resources() - resource_uris = [str(r.uri) for r in resources] - assert "shared://data" in resource_uris - assert resource_uris.count("shared://data") == 1 # Should only appear once - - result = await client.read_resource("shared://data") - assert isinstance(result[0], TextResourceContents) - assert result[0].text == "Second app data" - - -async def test_import_conflict_resolution_templates(): - """Test that later imported templates overwrite earlier ones when URI templates conflict.""" - main_app = FastMCP("MainApp") - first_app = FastMCP("FirstApp") - second_app = FastMCP("SecondApp") - - @first_app.resource(uri="users://{user_id}/profile") - def first_template(user_id: str): - return f"First app user {user_id}" - - @second_app.resource(uri="users://{user_id}/profile") - def second_template(user_id: str): - return f"Second app user {user_id}" - - # Import both apps without prefix - await main_app.import_server(first_app) - await main_app.import_server(second_app) - - async with Client(main_app) as client: - # The later imported server should win - templates = await client.list_resource_templates() - template_uris = [t.uri_template for t in templates] - assert "users://{user_id}/profile" in template_uris - assert ( - template_uris.count("users://{user_id}/profile") == 1 - ) # Should only appear once - - result = await client.read_resource("users://123/profile") - assert isinstance(result[0], TextResourceContents) - assert result[0].text == "Second app user 123" - - -async def test_import_conflict_resolution_prompts(): - """Test that later imported prompts overwrite earlier ones when names conflict.""" - main_app = FastMCP("MainApp") - first_app = FastMCP("FirstApp") - second_app = FastMCP("SecondApp") - - @first_app.prompt(name="shared_prompt") - def first_shared_prompt() -> str: - return "First app prompt" - - @second_app.prompt(name="shared_prompt") - def second_shared_prompt() -> str: - return "Second app prompt" - - # Import both apps without prefix - await main_app.import_server(first_app) - await main_app.import_server(second_app) - - async with Client(main_app) as client: - # The later imported server should win - prompts = await client.list_prompts() - prompt_names = [p.name for p in prompts] - assert "shared_prompt" in prompt_names - assert prompt_names.count("shared_prompt") == 1 # Should only appear once - - result = await client.get_prompt("shared_prompt", {}) - assert result.messages is not None - assert isinstance(result.messages[0].content, TextContent) - assert result.messages[0].content.text == "Second app prompt" - - -async def test_import_conflict_resolution_with_prefix(): - """Test that later imported components overwrite earlier ones when prefixed names conflict.""" - main_app = FastMCP("MainApp") - first_app = FastMCP("FirstApp") - second_app = FastMCP("SecondApp") - - @first_app.tool(name="shared_tool") - def first_shared_tool() -> str: - return "First app tool" - - @second_app.tool(name="shared_tool") - def second_shared_tool() -> str: - return "Second app tool" - - # Import both apps with same prefix - await main_app.import_server(first_app, "api") - await main_app.import_server(second_app, "api") - - async with Client(main_app) as client: - # The later imported server should win - tools = await client.list_tools() - tool_names = [t.name for t in tools] - assert "api_shared_tool" in tool_names - assert tool_names.count("api_shared_tool") == 1 # Should only appear once - - result = await client.call_tool("api_shared_tool", {}) - assert result.data == "Second app tool" - - -async def test_import_server_resource_uri_prefixing(): - """Test that resource URIs are prefixed when using import_server (names are NOT prefixed).""" - # Create a sub-server with a resource - sub_server = FastMCP("SubServer") - - @sub_server.resource("resource://test_resource") - def test_resource() -> str: - return "Test content" - - # Create main server and import sub-server with prefix - main_server = FastMCP("MainServer") - await main_server.import_server(sub_server, prefix="imported") - - # Get resources and verify URI prefixing (name should NOT be prefixed) - resources = await main_server.list_resources() - resource = next( - r for r in resources if str(r.uri) == "resource://imported/test_resource" - ) - assert resource.name == "test_resource" - - -async def test_import_server_resource_template_uri_prefixing(): - """Test that resource template URIs are prefixed when using import_server (names are NOT prefixed).""" - # Create a sub-server with a resource template - sub_server = FastMCP("SubServer") - - @sub_server.resource("resource://data/{item_id}") - def data_template(item_id: str) -> str: - return f"Data for {item_id}" - - # Create main server and import sub-server with prefix - main_server = FastMCP("MainServer") - await main_server.import_server(sub_server, prefix="imported") - - # Get resource templates and verify URI prefixing (name should NOT be prefixed) - templates = await main_server.list_resource_templates() - template = next( - t for t in templates if t.uri_template == "resource://imported/data/{item_id}" - ) - assert template.name == "data_template" - - -async def test_import_server_with_new_prefix_format(): - """Test that import_server correctly uses the new prefix format.""" - # Create a server with resources - source_server = FastMCP(name="SourceServer") - - @source_server.resource("resource://test-resource") - def get_resource(): - return "Resource content" - - @source_server.resource("resource:///absolute/path") - def get_absolute_resource(): - return "Absolute resource content" - - @source_server.resource("resource://{param}/template") - def get_template_resource(param: str): - return f"Template resource with {param}" - - # Create target server and import the source server - target_server = FastMCP(name="TargetServer") - await target_server.import_server(source_server, "imported") - - # Check that the resources were imported with the correct prefixes - resources = await target_server.list_resources() - templates = await target_server.list_resource_templates() - - assert any(str(r.uri) == "resource://imported/test-resource" for r in resources) - assert any(str(r.uri) == "resource://imported//absolute/path" for r in resources) - assert any( - t.uri_template == "resource://imported/{param}/template" for t in templates - ) - - # Verify we can access the resources - async with Client(target_server) as client: - result = await client.read_resource("resource://imported/test-resource") - assert isinstance(result[0], TextResourceContents) - assert result[0].text == "Resource content" - - result = await client.read_resource("resource://imported//absolute/path") - assert isinstance(result[0], TextResourceContents) - assert result[0].text == "Absolute resource content" - - result = await client.read_resource("resource://imported/param-value/template") - assert isinstance(result[0], TextResourceContents) - assert result[0].text == "Template resource with param-value" diff --git a/tests/server/middleware/test_middleware_nested.py b/tests/server/middleware/test_middleware_nested.py index 2f6d2593e..cc2eea6c0 100644 --- a/tests/server/middleware/test_middleware_nested.py +++ b/tests/server/middleware/test_middleware_nested.py @@ -8,6 +8,7 @@ from mcp.server.context import ServerRequestContext from fastmcp import Client, FastMCP from fastmcp.exceptions import ToolError +from fastmcp.server import create_proxy from fastmcp.server.context import Context from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext from fastmcp.tools.base import ToolResult @@ -475,7 +476,7 @@ class TestProxyServer: ): # proxy server will have its tools listed as well as called in order to # apply transforms and filters prior to the call. - proxy_server = FastMCP.as_proxy(mcp_server, name="Proxy Server") + proxy_server = create_proxy(mcp_server, name="Proxy Server") async with Client(proxy_server) as client: await client.call_tool("add", {"a": 1, "b": 2}) @@ -492,7 +493,7 @@ class TestProxyServer: ): """Tests that tags on remote FastMCP servers are visible to middleware via proxy. See https://github.com/PrefectHQ/fastmcp/issues/1300""" - proxy_server = FastMCP.as_proxy(mcp_server, name="Proxy Server") + proxy_server = create_proxy(mcp_server, name="Proxy Server") TAGS = [] diff --git a/tests/server/mount/test_mount.py b/tests/server/mount/test_mount.py index fdd555138..5f099a352 100644 --- a/tests/server/mount/test_mount.py +++ b/tests/server/mount/test_mount.py @@ -9,6 +9,7 @@ from mcp_types import TextContent from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.client.transports import SSETransport +from fastmcp.server import create_proxy from fastmcp.tools.base import Tool from fastmcp.tools.tool_transform import TransformedTool @@ -272,9 +273,7 @@ class TestMultipleServerMount: ) # Create a proxy server that will fail to connect - unreachable_proxy = FastMCP.as_proxy( - unreachable_client, name="unreachable_proxy" - ) + unreachable_proxy = create_proxy(unreachable_client, name="unreachable_proxy") # Mount the unreachable proxy main_app.mount(unreachable_proxy, "unreachable") diff --git a/tests/server/mount/test_proxy.py b/tests/server/mount/test_proxy.py index 08caa36a0..e514a5ff4 100644 --- a/tests/server/mount/test_proxy.py +++ b/tests/server/mount/test_proxy.py @@ -6,8 +6,8 @@ from contextlib import asynccontextmanager from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.client.transports import FastMCPTransport +from fastmcp.server import create_proxy from fastmcp.server.providers import FastMCPProvider -from fastmcp.server.providers.proxy import FastMCPProxy from fastmcp.server.providers.wrapped_provider import _WrappedProvider from fastmcp.server.transforms import Namespace @@ -25,7 +25,7 @@ class TestProxyServer: return f"Data for {query}" # Create proxy server - proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server)) + proxy_server = create_proxy(FastMCPTransport(original_server)) # Mount proxy server main_app = FastMCP("MainApp") @@ -45,7 +45,7 @@ class TestProxyServer: original_server = FastMCP("OriginalServer") # Create proxy server - proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server)) + proxy_server = create_proxy(FastMCPTransport(original_server)) # Mount proxy server main_app = FastMCP("MainApp") @@ -74,7 +74,7 @@ class TestProxyServer: return json.dumps({"api_key": "12345"}) # Create proxy server - proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server)) + proxy_server = create_proxy(FastMCPTransport(original_server)) # Mount proxy server main_app = FastMCP("MainApp") @@ -96,7 +96,7 @@ class TestProxyServer: return f"Welcome, {name}!" # Create proxy server - proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server)) + proxy_server = create_proxy(FastMCPTransport(original_server)) # Mount proxy server main_app = FastMCP("MainApp") @@ -108,10 +108,10 @@ class TestProxyServer: # The message should contain our welcome text -class TestAsProxyKwarg: - """Test the as_proxy kwarg.""" +class TestMountProviderStructure: + """Test the provider structure produced by mounting.""" - async def test_as_proxy_defaults_false(self): + async def test_mount_wraps_in_fastmcp_provider(self): mcp = FastMCP("Main") sub = FastMCP("Sub") @@ -133,53 +133,6 @@ class TestAsProxyKwarg: tools = await mcp.list_tools() assert {t.name for t in tools} == {"sub_sub_tool"} - async def test_as_proxy_false(self): - mcp = FastMCP("Main") - sub = FastMCP("Sub") - - @sub.tool - def sub_tool() -> str: - return "test" - - mcp.mount(sub, "sub", as_proxy=False) - - # Index 1 because LocalProvider is at index 0 - provider = mcp.providers[1] - # Provider is wrapped with Namespace transform - assert isinstance(provider, _WrappedProvider) - assert len(provider._transforms) == 1 - assert isinstance(provider._transforms[0], Namespace) - # Inner provider is FastMCPProvider - assert isinstance(provider._inner, FastMCPProvider) - assert provider._inner.server is sub - # Verify namespace is applied - tools = await mcp.list_tools() - assert {t.name for t in tools} == {"sub_sub_tool"} - - async def test_as_proxy_true(self): - mcp = FastMCP("Main") - sub = FastMCP("Sub") - - @sub.tool - def sub_tool() -> str: - return "test" - - mcp.mount(sub, "sub", as_proxy=True) - - # Index 1 because LocalProvider is at index 0 - provider = mcp.providers[1] - # Provider is wrapped with Namespace transform - assert isinstance(provider, _WrappedProvider) - assert len(provider._transforms) == 1 - assert isinstance(provider._transforms[0], Namespace) - # Inner provider is FastMCPProvider wrapping a proxy - assert isinstance(provider._inner, FastMCPProvider) - assert provider._inner.server is not sub - assert isinstance(provider._inner.server, FastMCPProxy) - # Verify namespace is applied - tools = await mcp.list_tools() - assert {t.name for t in tools} == {"sub_sub_tool"} - async def test_lifespan_server_mounted_directly(self): """Test that servers with lifespan are mounted directly (not auto-proxied). @@ -214,10 +167,10 @@ class TestAsProxyKwarg: tools = await mcp.list_tools() assert {t.name for t in tools} == {"sub_sub_tool"} - async def test_as_proxy_ignored_for_proxy_mounts_default(self): + async def test_mounting_a_proxy_preserves_the_proxy(self): mcp = FastMCP("Main") sub = FastMCP("Sub") - sub_proxy = FastMCP.as_proxy(FastMCPTransport(sub)) + sub_proxy = create_proxy(FastMCPTransport(sub)) mcp.mount(sub_proxy, "sub") @@ -231,45 +184,11 @@ class TestAsProxyKwarg: assert isinstance(provider._inner, FastMCPProvider) assert provider._inner.server is sub_proxy - async def test_as_proxy_ignored_for_proxy_mounts_false(self): - mcp = FastMCP("Main") - sub = FastMCP("Sub") - sub_proxy = FastMCP.as_proxy(FastMCPTransport(sub)) - - mcp.mount(sub_proxy, "sub", as_proxy=False) - - # Index 1 because LocalProvider is at index 0 - provider = mcp.providers[1] - # Provider is wrapped with Namespace transform - assert isinstance(provider, _WrappedProvider) - assert len(provider._transforms) == 1 - assert isinstance(provider._transforms[0], Namespace) - # Inner provider is FastMCPProvider - assert isinstance(provider._inner, FastMCPProvider) - assert provider._inner.server is sub_proxy - - async def test_as_proxy_ignored_for_proxy_mounts_true(self): - mcp = FastMCP("Main") - sub = FastMCP("Sub") - sub_proxy = FastMCP.as_proxy(FastMCPTransport(sub)) - - mcp.mount(sub_proxy, "sub", as_proxy=True) - - # Index 1 because LocalProvider is at index 0 - provider = mcp.providers[1] - # Provider is wrapped with Namespace transform - assert isinstance(provider, _WrappedProvider) - assert len(provider._transforms) == 1 - assert isinstance(provider._transforms[0], Namespace) - # Inner provider is FastMCPProvider - assert isinstance(provider._inner, FastMCPProvider) - assert provider._inner.server is sub_proxy - - async def test_as_proxy_mounts_still_have_live_link(self): + async def test_mounts_have_live_link(self): mcp = FastMCP("Main") sub = FastMCP("Sub") - mcp.mount(sub, "sub", as_proxy=True) + mcp.mount(sub, "sub") assert len(await mcp.list_tools()) == 0 @@ -294,7 +213,7 @@ class TestAsProxyKwarg: def hello(): return "hi" - mcp.mount(sub, as_proxy=True) + mcp.mount(sub) assert lifespan_check == [] diff --git a/tests/server/providers/proxy/test_proxy_client.py b/tests/server/providers/proxy/test_proxy_client.py index 80ee1862b..216d283e8 100644 --- a/tests/server/providers/proxy/test_proxy_client.py +++ b/tests/server/providers/proxy/test_proxy_client.py @@ -16,6 +16,7 @@ from fastmcp.client.elicitation import ElicitRequestParams, ElicitResult from fastmcp.client.logging import LogMessage from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams from fastmcp.exceptions import ToolError +from fastmcp.server import create_proxy from fastmcp.server.elicitation import AcceptedElicitation from fastmcp.server.providers.proxy import ProxyClient, _create_client_factory @@ -88,7 +89,7 @@ async def proxy_server(fastmcp_server: FastMCP): """ A proxy server that forwards interactions with the proxy client to the given fastmcp server. """ - return FastMCP.as_proxy(ProxyClient(fastmcp_server)) + return create_proxy(ProxyClient(fastmcp_server)) class TestProxyClient: @@ -377,7 +378,7 @@ class TestProxyClient: else: return f"Elicitation {result.action}" - proxy_server = FastMCP.as_proxy(ProxyClient(fastmcp_server)) + proxy_server = create_proxy(ProxyClient(fastmcp_server)) # Test that elicitation works correctly through the proxy async def elicitation_handler( @@ -407,16 +408,16 @@ class TestProxyClient: # Create a disconnected client (should use fresh sessions per request) base_client = Client(fastmcp_server) - # Test both as_proxy convenience method and direct client_factory usage - proxy_via_as_proxy = FastMCP.as_proxy(base_client) + # Test both create_proxy convenience function and direct client_factory usage + proxy_via_create_proxy = create_proxy(base_client) proxy_via_factory = FastMCPProxy(client_factory=base_client.new) # Verify the proxies are created successfully - this tests the client factory pattern - assert proxy_via_as_proxy is not None + assert proxy_via_create_proxy is not None assert proxy_via_factory is not None # Verify they have the expected client factory behavior - assert hasattr(proxy_via_as_proxy, "_local_provider") + assert hasattr(proxy_via_create_proxy, "_local_provider") assert hasattr(proxy_via_factory, "_local_provider") async def test_connected_proxy_client_uses_fresh_sessions( @@ -464,7 +465,7 @@ def roots_backend_server(): @pytest.fixture async def roots_proxy_server(roots_backend_server: FastMCP): - return FastMCP.as_proxy(ProxyClient(roots_backend_server)) + return create_proxy(ProxyClient(roots_backend_server)) class TestProxyServerInitiatedForwardingNonTool: diff --git a/tests/server/providers/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py index 525012d1a..762a09abc 100644 --- a/tests/server/providers/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -198,38 +198,6 @@ def test_create_proxy_with_url(): 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/test_server.py b/tests/server/test_server.py index 83374362d..d1aae6206 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -1,5 +1,4 @@ import os -import warnings from pathlib import Path from tempfile import TemporaryDirectory from textwrap import dedent @@ -161,37 +160,6 @@ class TestLocalProviderProperty: assert not any(p.name == "my_prompt" for p in prompts) -class TestRemoveToolDeprecation: - async def test_remove_tool_emits_deprecation_warning(self): - mcp = FastMCP() - - @mcp.tool - def my_tool() -> str: - return "result" - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - mcp.remove_tool("my_tool") - - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "local_provider" in str(w[0].message) - - async def test_remove_tool_still_works(self): - mcp = FastMCP() - - @mcp.tool - def my_tool() -> str: - return "result" - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - mcp.remove_tool("my_tool") - - tools = await mcp.list_tools() - assert not any(t.name == "my_tool" for t in tools) - - class TestResourcePrefixMounting: """Test resource prefixing in mounted servers.""" diff --git a/tests/tools/tool_transform/test_tool_transform.py b/tests/tools/tool_transform/test_tool_transform.py index dcee4cf97..f9e50df72 100644 --- a/tests/tools/tool_transform/test_tool_transform.py +++ b/tests/tools/tool_transform/test_tool_transform.py @@ -719,8 +719,9 @@ class TestProxy: @pytest.fixture def proxy_server(self, mcp_server: FastMCP) -> FastMCP: from fastmcp.client.transports import FastMCPTransport + from fastmcp.server import create_proxy - proxy = FastMCP.as_proxy(FastMCPTransport(mcp_server)) + proxy = create_proxy(FastMCPTransport(mcp_server)) return proxy async def test_transform_proxy(self, proxy_server: FastMCP):