Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 82 additions & 47 deletions src/fastmcp/contrib/component_manager/component_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,49 @@
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.providers import MountedProvider
from fastmcp.server.providers import FastMCPProvider, Provider, TransformingProvider
from fastmcp.server.server import FastMCP
from fastmcp.tools.tool import Tool
from fastmcp.utilities.logging import get_logger

logger = get_logger(__name__)


def _get_mounted_server_and_key(
provider: Provider,
key: str,
component_type: str,
) -> tuple[FastMCP, str] | None:
"""Get the mounted server and unprefixed key for a component.

Args:
provider: The provider to check.
key: The transformed component key.
component_type: Either "tool" (for tools/prompts) or "resource".

Returns:
Tuple of (server, original_key) if the key matches this provider,
or None if it doesn't.
"""
if isinstance(provider, TransformingProvider):
# TransformingProvider - reverse the transformation
if component_type == "resource":
original = provider._reverse_resource_uri(key)
else:
original = provider._reverse_tool_name(key)

if original is not None:
# Recursively check the wrapped provider
return _get_mounted_server_and_key(
provider._wrapped, original, component_type
)
elif isinstance(provider, FastMCPProvider):
# Direct FastMCPProvider - no transformation, key is used directly
return provider.server, key

return None


class ComponentService:
"""Service for managing components like tools, resources, and prompts."""

Expand All @@ -41,14 +76,14 @@ async def _enable_tool(self, key: str) -> Tool:
tool.enable()
return tool

# 2. Check mounted servers via MountedProvider
# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
tool = await mounted_service._enable_tool(unprefixed)
return tool
result = _get_mounted_server_and_key(provider, key, "tool")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
tool = await mounted_service._enable_tool(unprefixed)
return tool
raise NotFoundError(f"Unknown tool: {key}")

async def _disable_tool(self, key: str) -> Tool:
Expand All @@ -68,14 +103,14 @@ async def _disable_tool(self, key: str) -> Tool:
tool.disable()
return tool

# 2. Check mounted servers via MountedProvider
# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
tool = await mounted_service._disable_tool(unprefixed)
return tool
result = _get_mounted_server_and_key(provider, key, "tool")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
tool = await mounted_service._disable_tool(unprefixed)
return tool
raise NotFoundError(f"Unknown tool: {key}")

async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
Expand All @@ -99,16 +134,16 @@ async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
template.enable()
return template

# 2. Check mounted servers via MountedProvider
# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_resource_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._enable_resource(unprefixed)
return mounted_resource
result = _get_mounted_server_and_key(provider, key, "resource")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._enable_resource(unprefixed)
return mounted_resource
raise NotFoundError(f"Unknown resource: {key}")

async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
Expand All @@ -132,16 +167,16 @@ async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
template.disable()
return template

# 2. Check mounted servers via MountedProvider
# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_resource_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._disable_resource(unprefixed)
return mounted_resource
result = _get_mounted_server_and_key(provider, key, "resource")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._disable_resource(unprefixed)
return mounted_resource
raise NotFoundError(f"Unknown resource: {key}")

async def _enable_prompt(self, key: str) -> Prompt:
Expand All @@ -161,14 +196,14 @@ async def _enable_prompt(self, key: str) -> Prompt:
prompt.enable()
return prompt

# 2. Check mounted servers via MountedProvider
# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
prompt = await mounted_service._enable_prompt(unprefixed)
return prompt
result = _get_mounted_server_and_key(provider, key, "tool")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
prompt = await mounted_service._enable_prompt(unprefixed)
return prompt
raise NotFoundError(f"Unknown prompt: {key}")

async def _disable_prompt(self, key: str) -> Prompt:
Expand All @@ -187,12 +222,12 @@ async def _disable_prompt(self, key: str) -> Prompt:
prompt.disable()
return prompt

# 2. Check mounted servers via MountedProvider
# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
prompt = await mounted_service._disable_prompt(unprefixed)
return prompt
result = _get_mounted_server_and_key(provider, key, "tool")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
prompt = await mounted_service._disable_prompt(unprefixed)
return prompt
raise NotFoundError(f"Unknown prompt: {key}")
10 changes: 5 additions & 5 deletions src/fastmcp/server/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ async def get_tool(self, name: str) -> Tool | None:
```
"""

from fastmcp.server.providers.base import Components, Provider, TaskComponents
from fastmcp.server.providers.mounted import MountedProvider
from fastmcp.server.providers.base import Provider
from fastmcp.server.providers.fastmcp_provider import FastMCPProvider
from fastmcp.server.providers.transforming import TransformingProvider

__all__ = [
"Components",
"MountedProvider",
"FastMCPProvider",
"Provider",
"TaskComponents",
"TransformingProvider",
]
68 changes: 68 additions & 0 deletions src/fastmcp/server/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,74 @@ class Provider:
exceptions are wrapped with optional detail masking.
"""

def with_transforms(
self,
*,
namespace: str | None = None,
tool_renames: dict[str, str] | None = None,
) -> Provider:
"""Apply transformations to this provider's components.

Returns a TransformingProvider that wraps this provider and applies
the specified transformations. Can be chained - each call creates a
new wrapper that composes with the previous.

Args:
namespace: Prefix for tools/prompts ("namespace_name"), path segment
for resources ("protocol://namespace/path").
tool_renames: Map of original_name → final_name. Tools in this map
use the specified name instead of namespace prefixing.

Returns:
A TransformingProvider wrapping this provider.

Example:
```python
# Apply namespace to all components
provider = MyProvider().with_transforms(namespace="db")
# Tool "greet" becomes "db_greet"
# Resource "resource://data" becomes "resource://db/data"

# Rename specific tools (bypasses namespace for those tools)
provider = MyProvider().with_transforms(
namespace="api",
tool_renames={"verbose_tool_name": "short"}
)
# "verbose_tool_name" → "short" (explicit rename)
# "other_tool" → "api_other_tool" (namespace applied)

# Stacking composes transformations
provider = (
MyProvider()
.with_transforms(namespace="api")
.with_transforms(tool_renames={"api_foo": "bar"})
)
# "foo" → "api_foo" (inner) → "bar" (outer)
```
"""
from fastmcp.server.providers.transforming import TransformingProvider

return TransformingProvider(
self, namespace=namespace, tool_renames=tool_renames
)

def with_namespace(self, namespace: str) -> Provider:
"""Shorthand for with_transforms(namespace=...).

Args:
namespace: The namespace to apply.

Returns:
A TransformingProvider wrapping this provider.

Example:
```python
provider = MyProvider().with_namespace("db")
# Equivalent to: MyProvider().with_transforms(namespace="db")
```
"""
return self.with_transforms(namespace=namespace)

async def list_tools(self) -> Sequence[Tool]:
"""Return all available tools.

Expand Down
Loading
Loading