Skip to content
8 changes: 8 additions & 0 deletions src/fastmcp/contrib/component_manager/component_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,14 @@ async def _enable_resource(self, uri: str) -> Resource | ResourceTemplate:
if resource_keys:
self._server.enable(keys=resource_keys)
resource = await self._server.get_resource(uri)
if resource is None:
raise NotFoundError(f"Resource {uri!r} not found after enabling")
return resource
if template_keys:
self._server.enable(keys=template_keys)
template = await self._server.get_resource_template(uri)
if template is None:
raise NotFoundError(f"Template {uri!r} not found after enabling")
return template

# 2. Check mounted servers via FastMCPProvider
Expand Down Expand Up @@ -234,11 +238,15 @@ async def _disable_resource(self, uri: str) -> Resource | ResourceTemplate:
if resource_keys:
# Get the highest version to return before disabling
resource = await self._server.get_resource(uri)
if resource is None:
raise NotFoundError(f"Resource {uri!r} not found")
self._server.disable(keys=resource_keys)
return resource
if template_keys:
# Get the highest version to return before disabling
template = await self._server.get_resource_template(uri)
if template is None:
raise NotFoundError(f"Template {uri!r} not found")
self._server.disable(keys=template_keys)
return template

Expand Down
23 changes: 18 additions & 5 deletions src/fastmcp/server/middleware/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

import mcp.types as mt

from fastmcp.exceptions import AuthorizationError, NotFoundError
from fastmcp.exceptions import AuthorizationError
from fastmcp.prompts.prompt import Prompt, PromptResult
from fastmcp.resources.resource import Resource, ResourceResult
from fastmcp.resources.template import ResourceTemplate
Expand Down Expand Up @@ -136,7 +136,12 @@ async def on_call_tool(
f"Authorization failed for tool '{tool_name}': missing context"
)

# Resolve tool (includes component-level auth check)
tool = await fastmcp.fastmcp.get_tool(tool_name)
if tool is None:
raise AuthorizationError(
f"Authorization failed for tool '{tool_name}': tool not found"
)

token = get_access_token()
ctx = AuthContext(token=token, component=tool)
Expand Down Expand Up @@ -196,11 +201,14 @@ async def on_read_resource(
f"Authorization failed for resource '{uri}': missing context"
)

# Try concrete resource first, then template (for template-backed URIs)
try:
component = await fastmcp.fastmcp.get_resource(str(uri))
except NotFoundError:
# Try concrete resource first, then template (includes component-level auth check)
component = await fastmcp.fastmcp.get_resource(str(uri))
if component is None:
component = await fastmcp.fastmcp.get_resource_template(str(uri))
if component is None:
raise AuthorizationError(
f"Authorization failed for resource '{uri}': resource not found"
)

token = get_access_token()
ctx = AuthContext(token=token, component=component)
Expand Down Expand Up @@ -286,7 +294,12 @@ async def on_get_prompt(
f"Authorization failed for prompt '{prompt_name}': missing context"
)

# Resolve prompt (includes component-level auth check)
prompt = await fastmcp.fastmcp.get_prompt(prompt_name)
if prompt is None:
raise AuthorizationError(
f"Authorization failed for prompt '{prompt_name}': prompt not found"
)

token = get_access_token()
ctx = AuthContext(token=token, component=prompt)
Expand Down
2 changes: 2 additions & 0 deletions src/fastmcp/server/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ async def get_tool(self, name: str) -> Tool | None:

from typing import TYPE_CHECKING

from fastmcp.server.providers.aggregate import AggregateProvider
from fastmcp.server.providers.base import Provider
from fastmcp.server.providers.fastmcp_provider import FastMCPProvider
from fastmcp.server.providers.filesystem import FileSystemProvider
Expand All @@ -37,6 +38,7 @@ async def get_tool(self, name: str) -> Tool | None:
from fastmcp.server.providers.proxy import ProxyProvider as ProxyProvider

__all__ = [
"AggregateProvider",
"FastMCPProvider",
"FileSystemProvider",
"LocalProvider",
Expand Down
22 changes: 18 additions & 4 deletions src/fastmcp/server/providers/aggregate.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
"""AggregateProvider for combining multiple providers into one.

This module provides `AggregateProvider` which presents multiple providers
as a single unified provider. Used internally by FastMCP for aggregating
components from all providers.
This module provides `AggregateProvider`, a utility class that presents
multiple providers as a single unified provider. Useful when you want to
combine custom providers without creating a full FastMCP server.

Example:
```python
from fastmcp.server.providers import AggregateProvider

# Combine multiple providers into one
combined = AggregateProvider([provider1, provider2, provider3])

# Use like any other provider
tools = await combined.list_tools()
```
"""

from __future__ import annotations
Expand All @@ -28,12 +39,15 @@


class AggregateProvider(Provider):
"""Presents multiple providers as a single provider.
"""Utility provider that combines multiple providers into one.

Components are aggregated from all providers. For get_* operations,
providers are queried in parallel and the highest version is returned.

Errors from individual providers are logged and skipped (graceful degradation).

This is useful when you want to combine custom providers without creating
a full FastMCP server.
"""

def __init__(self, providers: Sequence[Provider]) -> None:
Expand Down
28 changes: 16 additions & 12 deletions src/fastmcp/server/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,17 @@ class Provider:
"""

def __init__(self) -> None:
# Visibility is the first (innermost) transform - closest to the base provider
self._visibility = Visibility()
self._transforms: list[Transform] = [self._visibility]
self._transforms: list[Transform] = []

Comment on lines 67 to 70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply visibility when FastMCPProvider registers tasks

Now that _transforms no longer includes Visibility, any code that still iterates self._transforms directly will skip visibility filtering. FastMCPProvider.get_tasks() still wraps only self._transforms, so provider-level enable/disable (or allowlist) won’t be applied to task registration for mounted servers, and disabled components can still be registered with Docket. This regression is introduced by separating visibility from _transforms; update FastMCPProvider.get_tasks() to iterate self.transforms instead (see src/fastmcp/server/providers/fastmcp_provider.py).

Useful? React with 👍 / 👎.

def __repr__(self) -> str:
return f"{self.__class__.__name__}()"

@property
def transforms(self) -> list[Transform]:
"""All transforms including visibility (applied last/outermost)."""
return [*self._transforms, self._visibility]

def add_transform(self, transform: Transform) -> None:
"""Add a transform to this provider.

Expand Down Expand Up @@ -110,7 +114,7 @@ async def base() -> Sequence[Tool]:
return await self.list_tools()

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.list_tools, call_next=chain)

return await chain()
Expand All @@ -132,7 +136,7 @@ async def base(n: str, version: VersionSpec | None = None) -> Tool | None:
return await self.get_tool(n, version)

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.get_tool, call_next=chain)

return await chain(name, version=version)
Expand All @@ -144,7 +148,7 @@ async def base() -> Sequence[Resource]:
return await self.list_resources()

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.list_resources, call_next=chain)

return await chain()
Expand All @@ -163,7 +167,7 @@ async def base(u: str, version: VersionSpec | None = None) -> Resource | None:
return await self.get_resource(u, version)

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.get_resource, call_next=chain)

return await chain(uri, version=version)
Expand All @@ -175,7 +179,7 @@ async def base() -> Sequence[ResourceTemplate]:
return await self.list_resource_templates()

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.list_resource_templates, call_next=chain)

return await chain()
Expand All @@ -196,7 +200,7 @@ async def base(
return await self.get_resource_template(u, version)

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.get_resource_template, call_next=chain)

return await chain(uri, version=version)
Expand All @@ -208,7 +212,7 @@ async def base() -> Sequence[Prompt]:
return await self.list_prompts()

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.list_prompts, call_next=chain)

return await chain()
Expand All @@ -227,7 +231,7 @@ async def base(n: str, version: VersionSpec | None = None) -> Prompt | None:
return await self.get_prompt(n, version)

chain = base
for transform in self._transforms:
for transform in self.transforms:
chain = partial(transform.get_prompt, call_next=chain)

return await chain(name, version=version)
Expand Down Expand Up @@ -402,13 +406,13 @@ async def templates_base() -> Sequence[ResourceTemplate]:
async def prompts_base() -> Sequence[Prompt]:
return prompts

# Apply transforms in order (first is innermost)
# Apply transforms in order (visibility last/outermost)
tools_chain = tools_base
resources_chain = resources_base
templates_chain = templates_base
prompts_chain = prompts_base

for transform in self._transforms:
for transform in self.transforms:
tools_chain = partial(transform.list_tools, call_next=tools_chain)
resources_chain = partial(
transform.list_resources, call_next=resources_chain
Expand Down
43 changes: 20 additions & 23 deletions src/fastmcp/server/providers/fastmcp_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import mcp.types
from mcp.types import AnyUrl

from fastmcp.exceptions import NotFoundError
from fastmcp.prompts.prompt import Prompt, PromptResult
from fastmcp.resources.resource import Resource, ResourceResult
from fastmcp.resources.template import ResourceTemplate
Expand Down Expand Up @@ -486,9 +485,9 @@ def __init__(self, server: FastMCP[Any]):
async def list_tools(self) -> Sequence[Tool]:
"""List all tools from the mounted server as FastMCPProviderTools.

Calls the nested server's middleware to list tools, then wraps
each tool as a FastMCPProviderTool that delegates execution to the
nested server's middleware.
Runs the mounted server's middleware so filtering/transformation applies.
Wraps each tool as a FastMCPProviderTool that delegates execution to
the nested server's middleware.
"""
raw_tools = await self.server.get_tools(run_middleware=True)
return [FastMCPProviderTool.wrap(self.server, t) for t in raw_tools]
Expand All @@ -501,9 +500,8 @@ async def get_tool(
Passes the full VersionSpec to the nested server, which handles both
exact version matching and range filtering.
"""
try:
raw_tool = await self.server.get_tool(name, version)
except NotFoundError:
raw_tool = await self.server.get_tool(name, version)
if raw_tool is None:
return None
return FastMCPProviderTool.wrap(self.server, raw_tool)

Expand All @@ -514,9 +512,9 @@ async def get_tool(
async def list_resources(self) -> Sequence[Resource]:
"""List all resources from the mounted server as FastMCPProviderResources.

Calls the nested server's middleware to list resources, then wraps
each resource as a FastMCPProviderResource that delegates reading to the
nested server's middleware.
Runs the mounted server's middleware so filtering/transformation applies.
Wraps each resource as a FastMCPProviderResource that delegates reading
to the nested server's middleware.
"""
raw_resources = await self.server.get_resources(run_middleware=True)
return [FastMCPProviderResource.wrap(self.server, r) for r in raw_resources]
Expand All @@ -529,9 +527,8 @@ async def get_resource(
Passes the full VersionSpec to the nested server, which handles both
exact version matching and range filtering.
"""
try:
raw_resource = await self.server.get_resource(uri, version)
except NotFoundError:
raw_resource = await self.server.get_resource(uri, version)
if raw_resource is None:
return None
return FastMCPProviderResource.wrap(self.server, raw_resource)

Expand All @@ -542,6 +539,7 @@ async def get_resource(
async def list_resource_templates(self) -> Sequence[ResourceTemplate]:
"""List all resource templates from the mounted server.

Runs the mounted server's middleware so filtering/transformation applies.
Returns FastMCPProviderResourceTemplate instances that create
FastMCPProviderResources when materialized.
"""
Expand All @@ -558,9 +556,8 @@ async def get_resource_template(
Passes the full VersionSpec to the nested server, which handles both
exact version matching and range filtering.
"""
try:
raw_template = await self.server.get_resource_template(uri, version)
except NotFoundError:
raw_template = await self.server.get_resource_template(uri, version)
if raw_template is None:
return None
return FastMCPProviderResourceTemplate.wrap(self.server, raw_template)

Expand All @@ -571,6 +568,7 @@ async def get_resource_template(
async def list_prompts(self) -> Sequence[Prompt]:
"""List all prompts from the mounted server as FastMCPProviderPrompts.

Runs the mounted server's middleware so filtering/transformation applies.
Returns FastMCPProviderPrompt instances that delegate rendering to the
wrapped server's middleware.
"""
Expand All @@ -585,9 +583,8 @@ async def get_prompt(
Passes the full VersionSpec to the nested server, which handles both
exact version matching and range filtering.
"""
try:
raw_prompt = await self.server.get_prompt(name, version)
except NotFoundError:
raw_prompt = await self.server.get_prompt(name, version)
if raw_prompt is None:
return None
return FastMCPProviderPrompt.wrap(self.server, raw_prompt)

Expand All @@ -599,12 +596,12 @@ async def get_tasks(self) -> Sequence[FastMCPComponent]:
"""Return task-eligible components from the mounted server.

Returns the child's ACTUAL components (not wrapped) so their actual
functions get registered with Docket. Uses _source_get_tasks() to get
components with child server's transforms applied, then applies this
provider's transforms for correct registration keys.
functions get registered with Docket. Gets components with child
server's transforms applied, then applies this provider's transforms
for correct registration keys.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"""
# Get tasks with child server's transforms already applied
components = list(await self.server._source_get_tasks())
components = list(await self.server.get_tasks())

# Separate by type for this provider's transform application
tools = [c for c in components if isinstance(c, Tool)]
Expand Down
Loading
Loading