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."""