diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx index cf15f236b..dddeb658a 100644 --- a/docs/python-sdk/fastmcp-server-server.mdx +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -26,7 +26,7 @@ Default lifespan context manager that does nothing. - An empty dictionary as the lifespan result. -### `create_proxy` +### `create_proxy` ```python create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy @@ -100,13 +100,28 @@ website_url(self) -> str | None icons(self) -> list[mcp.types.Icon] ``` -#### `add_middleware` +#### `local_provider` + +```python +local_provider(self) -> LocalProvider +``` + +The server's local provider, which stores directly-registered components. + +Use this to remove components: + + mcp.local_provider.remove_tool("my_tool") + mcp.local_provider.remove_resource("data://info") + mcp.local_provider.remove_prompt("my_prompt") + + +#### `add_middleware` ```python add_middleware(self, middleware: Middleware) -> None ``` -#### `add_provider` +#### `add_provider` ```python add_provider(self, provider: Provider) -> None @@ -126,7 +141,7 @@ always take precedence over providers. - Prompts become "namespace_promptname" -#### `get_tasks` +#### `get_tasks` ```python get_tasks(self) -> Sequence[FastMCPComponent] @@ -138,7 +153,7 @@ Overrides AggregateProvider.get_tasks() to apply server-level transforms after aggregation. AggregateProvider handles provider-level namespacing. -#### `add_transform` +#### `add_transform` ```python add_transform(self, transform: Transform) -> None @@ -153,7 +168,7 @@ They transform tools, resources, and prompts from ALL providers. - `transform`: The transform to add. -#### `add_tool_transformation` +#### `add_tool_transformation` ```python add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None @@ -165,7 +180,7 @@ Add a tool transformation. Use ``add_transform(ToolTransform({...}))`` instead. -#### `remove_tool_transformation` +#### `remove_tool_transformation` ```python remove_tool_transformation(self, _tool_name: str) -> None @@ -177,7 +192,7 @@ Remove a tool transformation. Tool transformations are now immutable. Use enable/disable controls instead. -#### `list_tools` +#### `list_tools` ```python list_tools(self) -> Sequence[Tool] @@ -190,7 +205,7 @@ and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. -#### `get_tool` +#### `get_tool` ```python get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None @@ -210,7 +225,7 @@ session transforms can override provider-level disables. - The tool if found and enabled, None otherwise. -#### `list_resources` +#### `list_resources` ```python list_resources(self) -> Sequence[Resource] @@ -223,7 +238,7 @@ and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. -#### `get_resource` +#### `get_resource` ```python get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None @@ -242,7 +257,7 @@ transforms (including session-level) have been applied. - The resource if found and enabled, None otherwise. -#### `list_resource_templates` +#### `list_resource_templates` ```python list_resource_templates(self) -> Sequence[ResourceTemplate] @@ -255,7 +270,7 @@ auth filtering, and middleware execution. Returns all versions (no deduplication Protocol handlers deduplicate for MCP wire format. -#### `get_resource_template` +#### `get_resource_template` ```python get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None @@ -274,7 +289,7 @@ all transforms (including session-level) have been applied. - The template if found and enabled, None otherwise. -#### `list_prompts` +#### `list_prompts` ```python list_prompts(self) -> Sequence[Prompt] @@ -287,7 +302,7 @@ and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. -#### `get_prompt` +#### `get_prompt` ```python get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None @@ -306,19 +321,19 @@ transforms (including session-level) have been applied. - The prompt if found and enabled, None otherwise. -#### `call_tool` +#### `call_tool` ```python call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult ``` -#### `call_tool` +#### `call_tool` ```python call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult ``` -#### `call_tool` +#### `call_tool` ```python call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult @@ -348,19 +363,19 @@ return ToolResult. - `ValidationError`: If arguments fail validation -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str) -> ResourceResult ``` -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str) -> mcp.types.CreateTaskResult ``` -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult @@ -389,19 +404,19 @@ return ResourceResult. - `ResourceError`: If resource read fails -#### `render_prompt` +#### `render_prompt` ```python render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult ``` -#### `render_prompt` +#### `render_prompt` ```python render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult ``` -#### `render_prompt` +#### `render_prompt` ```python render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult @@ -431,7 +446,7 @@ return PromptResult. - `PromptError`: If prompt rendering fails -#### `add_tool` +#### `add_tool` ```python add_tool(self, tool: Tool | Callable[..., Any]) -> Tool @@ -449,7 +464,7 @@ with the Context type annotation. See the @tool decorator for examples. - The tool instance that was added to the server. -#### `remove_tool` +#### `remove_tool` ```python remove_tool(self, name: str, version: str | None = None) -> None @@ -457,6 +472,9 @@ 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. @@ -465,19 +483,19 @@ Remove tool(s) from the server. - `NotFoundError`: If no matching tool is found. -#### `tool` +#### `tool` ```python tool(self, name_or_fn: AnyFunction) -> FunctionTool ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool] ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool] @@ -533,7 +551,7 @@ server.tool(my_function, name="custom_name") ``` -#### `add_resource` +#### `add_resource` ```python add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate @@ -548,7 +566,7 @@ Add a resource to the server. - The resource instance that was added to the server. -#### `add_template` +#### `add_template` ```python add_template(self, template: ResourceTemplate) -> ResourceTemplate @@ -563,7 +581,7 @@ Add a resource template to the server. - The template instance that was added to the server. -#### `resource` +#### `resource` ```python resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction] @@ -622,7 +640,7 @@ async def get_weather(city: str) -> str: ``` -#### `add_prompt` +#### `add_prompt` ```python add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt @@ -637,19 +655,19 @@ Add a prompt to the server. - The prompt instance that was added to the server. -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt] ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt] @@ -726,7 +744,7 @@ Decorator to register a prompt. ``` -#### `mount` +#### `mount` ```python mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, as_proxy: bool | None = None, tool_names: dict[str, str] | None = None, prefix: str | None = None) -> None @@ -773,7 +791,7 @@ mounted server. - `prefix`: Deprecated. Use namespace instead. -#### `import_server` +#### `import_server` ```python import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None @@ -814,7 +832,7 @@ templates, and prompts are imported with their original names. objects are imported with their original names. -#### `from_openapi` +#### `from_openapi` ```python from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self @@ -843,7 +861,7 @@ response structure while still returning structured JSON. - A FastMCP server with an OpenAPIProvider attached. -#### `from_fastapi` +#### `from_fastapi` ```python from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self @@ -867,7 +885,7 @@ Use this to configure timeout and other client settings. - A FastMCP server with an OpenAPIProvider attached. -#### `as_proxy` +#### `as_proxy` ```python as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy @@ -885,7 +903,7 @@ instance or any value accepted as the `transport` argument of `fastmcp.client.Client` constructor. -#### `generate_name` +#### `generate_name` ```python generate_name(cls, name: str | None = None) -> str diff --git a/docs/servers/providers/local.mdx b/docs/servers/providers/local.mdx index ae69d9063..86726655a 100644 --- a/docs/servers/providers/local.mdx +++ b/docs/servers/providers/local.mdx @@ -81,9 +81,9 @@ mcp.add_prompt(my_prompt) Remove components by name or URI: ```python -mcp.remove_tool("my_tool") -mcp.remove_resource("data://info") -mcp.remove_prompt("my_prompt") +mcp.local_provider.remove_tool("my_tool") +mcp.local_provider.remove_resource("data://info") +mcp.local_provider.remove_prompt("my_prompt") ``` ## Duplicate Handling diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index ee5004ae6..41d72f470 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -975,7 +975,7 @@ def example_tool() -> str: mcp.add_tool(example_tool) # Sends tools/list_changed notification mcp.disable(keys={"tool:example_tool"}) # Sends tools/list_changed notification mcp.enable(keys={"tool:example_tool"}) # Sends tools/list_changed notification -mcp.remove_tool("example_tool") # Sends tools/list_changed notification +mcp.local_provider.remove_tool("example_tool") # Sends tools/list_changed notification ``` Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications. @@ -1060,7 +1060,7 @@ The duplicate behavior options are: -You can dynamically remove tools from a server using the `remove_tool` method: +You can dynamically remove tools from a server through its [local provider](/servers/providers/local): ```python from fastmcp import FastMCP @@ -1072,7 +1072,7 @@ def calculate_sum(a: int, b: int) -> int: """Add two numbers together.""" return a + b -mcp.remove_tool("calculate_sum") +mcp.local_provider.remove_tool("calculate_sum") ``` ## Versioning diff --git a/docs/servers/versioning.mdx b/docs/servers/versioning.mdx index bdcefb217..2d180a851 100644 --- a/docs/servers/versioning.mdx +++ b/docs/servers/versioning.mdx @@ -293,14 +293,14 @@ If the requested version doesn't exist, a `NotFoundError` is raised. ## Removing Versions -The `remove_tool`, `remove_resource`, and `remove_prompt` methods accept an optional `version` parameter that controls what gets removed. +The `remove_tool`, `remove_resource`, and `remove_prompt` methods on the server's [local provider](/servers/providers/local) accept an optional `version` parameter that controls what gets removed. ```python # Remove ALL versions of a component -mcp.remove_tool("calculate") +mcp.local_provider.remove_tool("calculate") # Remove only a specific version -mcp.remove_tool("calculate", version="1.0") +mcp.local_provider.remove_tool("calculate", version="1.0") ``` When you remove a specific version, other versions remain registered. When you remove without specifying a version, all versions are removed. @@ -332,5 +332,5 @@ Clients automatically see version 2.0 (the highest). During the transition, your Once the migration is complete, remove the old version. ```python -mcp.remove_tool("process_data", version="1.0") +mcp.local_provider.remove_tool("process_data", version="1.0") ``` diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 9d1347789..8ad329b2d 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -368,6 +368,18 @@ class FastMCP( else: return list(self._mcp_server.icons) + @property + def local_provider(self) -> LocalProvider: + """The server's local provider, which stores directly-registered components. + + Use this to remove components: + + mcp.local_provider.remove_tool("my_tool") + mcp.local_provider.remove_resource("data://info") + mcp.local_provider.remove_prompt("my_prompt") + """ + return self._local_provider + async def _run_middleware( self, context: MiddlewareContext[Any], @@ -1257,6 +1269,9 @@ class FastMCP( 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. @@ -1264,6 +1279,13 @@ class FastMCP( 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.", + DeprecationWarning, + stacklevel=2, + ) try: self._local_provider.remove_tool(name, version) except KeyError: diff --git a/tests/server/mount/test_advanced.py b/tests/server/mount/test_advanced.py index 84619b478..b8434b3a3 100644 --- a/tests/server/mount/test_advanced.py +++ b/tests/server/mount/test_advanced.py @@ -53,8 +53,8 @@ class TestDynamicChanges: tools = await main_app.list_tools() assert any(t.name == "sub_temp_tool" for t in tools) - # Remove the tool from sub_app using public API - sub_app.remove_tool("temp_tool") + # Remove the tool from sub_app + sub_app.local_provider.remove_tool("temp_tool") # The tool should no longer be accessible tools = await main_app.list_tools() diff --git a/tests/server/test_server.py b/tests/server/test_server.py index ba7af98d5..e3a7f5125 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -1,4 +1,5 @@ import os +import warnings from pathlib import Path from tempfile import TemporaryDirectory from textwrap import dedent @@ -7,6 +8,7 @@ from unittest import mock from mcp.types import TextContent, TextResourceContents from fastmcp import Client, FastMCP +from fastmcp.server.providers import LocalProvider from fastmcp.tools import FunctionTool from fastmcp.tools.tool import Tool from fastmcp.utilities.tests import temporary_settings @@ -116,6 +118,80 @@ class TestServerDelegation: assert any(t.name == "local_tool" for t in tools) +class TestLocalProviderProperty: + """Test the public local_provider property.""" + + async def test_local_provider_returns_local_provider(self): + mcp = FastMCP() + assert isinstance(mcp.local_provider, LocalProvider) + assert mcp.local_provider is mcp._local_provider + + async def test_remove_tool_via_local_provider(self): + mcp = FastMCP() + + @mcp.tool + def my_tool() -> str: + return "result" + + assert await mcp.local_provider.get_tool("my_tool") is not None + mcp.local_provider.remove_tool("my_tool") + tools = await mcp.list_tools() + assert not any(t.name == "my_tool" for t in tools) + + async def test_remove_resource_via_local_provider(self): + mcp = FastMCP() + + @mcp.resource("resource://test") + def my_resource() -> str: + return "data" + + mcp.local_provider.remove_resource("resource://test") + resources = await mcp.list_resources() + assert not any(r.uri == "resource://test" for r in resources) + + async def test_remove_prompt_via_local_provider(self): + mcp = FastMCP() + + @mcp.prompt + def my_prompt() -> str: + return "hello" + + mcp.local_provider.remove_prompt("my_prompt") + prompts = await mcp.list_prompts() + 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."""