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
35 changes: 24 additions & 11 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,11 +136,16 @@ async def on_call_tool(
f"Authorization failed for tool '{tool_name}': missing context"
)

tool = await fastmcp.fastmcp.get_tool(tool_name)
# Get tool (component auth is checked in get_tool, raises if unauthorized)
tool = await fastmcp.fastmcp._get_tool(tool_name)
if tool is None:
raise AuthorizationError(
f"Authorization failed for tool '{tool_name}': tool not found"
)

# Global auth check
token = get_access_token()
ctx = AuthContext(token=token, component=tool)

if not run_auth_checks(self.auth, ctx):
raise AuthorizationError(
f"Authorization failed for tool '{tool_name}': insufficient permissions"
Expand Down Expand Up @@ -196,15 +201,18 @@ 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:
component = await fastmcp.fastmcp.get_resource_template(str(uri))
# Get resource/template (component auth is checked in get_*, raises if unauthorized)
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"
)

# Global auth check
token = get_access_token()
ctx = AuthContext(token=token, component=component)

if not run_auth_checks(self.auth, ctx):
raise AuthorizationError(
f"Authorization failed for resource '{uri}': insufficient permissions"
Expand Down Expand Up @@ -286,11 +294,16 @@ async def on_get_prompt(
f"Authorization failed for prompt '{prompt_name}': missing context"
)

prompt = await fastmcp.fastmcp.get_prompt(prompt_name)
# Get prompt (component auth is checked in get_prompt, raises if unauthorized)
prompt = await fastmcp.fastmcp._get_prompt(prompt_name)
if prompt is None:
raise AuthorizationError(
f"Authorization failed for prompt '{prompt_name}': prompt not found"
)

# Global auth check
token = get_access_token()
ctx = AuthContext(token=token, component=prompt)

if not run_auth_checks(self.auth, ctx):
raise AuthorizationError(
f"Authorization failed for prompt '{prompt_name}': insufficient permissions"
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
Loading