From a345594b01c88c1c2a7593629f3ddea875b6c274 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:22:01 -0500 Subject: [PATCH 1/4] Parallelize provider operations Add gather() utility using anyio TaskGroup to run provider queries concurrently. Latency is now O(max provider time) instead of O(sum). --- src/fastmcp/server/providers/base.py | 23 +- src/fastmcp/server/server.py | 374 +++++++++++++++------------ src/fastmcp/utilities/async_utils.py | 56 ++++ 3 files changed, 286 insertions(+), 167 deletions(-) create mode 100644 src/fastmcp/utilities/async_utils.py diff --git a/src/fastmcp/server/providers/base.py b/src/fastmcp/server/providers/base.py index 6c35fcf141..1c68b4f0e3 100644 --- a/src/fastmcp/server/providers/base.py +++ b/src/fastmcp/server/providers/base.py @@ -35,6 +35,7 @@ async def get_tool(self, name: str) -> Tool | None: from fastmcp.resources.resource import Resource from fastmcp.resources.template import ResourceTemplate from fastmcp.tools.tool import Tool +from fastmcp.utilities.async_utils import gather from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.visibility import VisibilityFilter @@ -219,17 +220,29 @@ async def get_component( Returns: The component if found, or None to continue searching other providers. """ - # Default implementation: iterate through all components and match by key - for tool in await self.list_tools(): + # Default implementation: fetch all component types in parallel + # Exceptions propagate since return_exceptions=False + results = await gather( + self.list_tools(), + self.list_resources(), + self.list_resource_templates(), + self.list_prompts(), + ) + tools: Sequence[Tool] = results[0] # type: ignore[assignment] + resources: Sequence[Resource] = results[1] # type: ignore[assignment] + templates: Sequence[ResourceTemplate] = results[2] # type: ignore[assignment] + prompts: Sequence[Prompt] = results[3] # type: ignore[assignment] + + for tool in tools: if tool.key == key: return tool - for resource in await self.list_resources(): + for resource in resources: if resource.key == key: return resource - for template in await self.list_resource_templates(): + for template in templates: if template.key == key: return template - for prompt in await self.list_prompts(): + for prompt in prompts: if prompt.key == key: return prompt return None diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 99f030e9ed..e0713b48ba 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -85,6 +85,7 @@ from fastmcp.settings import Settings from fastmcp.tools.tool import FunctionTool, Tool, ToolResult from fastmcp.tools.tool_transform import ToolTransformConfig +from fastmcp.utilities.async_utils import gather from fastmcp.utilities.cli import log_server_banner from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.logging import get_logger, temporary_log_level @@ -473,20 +474,26 @@ async def _docket_lifespan(self) -> AsyncIterator[None]: # Store on server instance for cross-task access (FastMCPTransport) self._docket = docket - # Register task-enabled components from all providers (LocalProvider first) - for provider in self._providers: - try: - for component in await provider.get_tasks(): - component.register_with_docket(docket) - except Exception as e: + # Register task-enabled components from all providers in parallel + task_results = await gather( + *[p.get_tasks() for p in self._providers], + return_exceptions=True, + ) + + for i, result in enumerate(task_results): + if isinstance(result, BaseException): + provider = self._providers[i] provider_name = getattr( provider, "server", provider ).__class__.__name__ logger.warning( - f"Failed to register tasks from provider {provider_name!r}: {e}" + f"Failed to register tasks from provider {provider_name!r}: {result}" ) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result + continue + for component in result: + component.register_with_docket(docket) # Set Docket in ContextVar so CurrentDocket can access it docket_token = _current_docket.set(docket) @@ -789,39 +796,44 @@ def _is_component_enabled(self, component: FastMCPComponent) -> bool: async def get_tools(self) -> dict[str, Tool]: """Get all enabled tools from providers, indexed by name. - Iterates through all providers (LocalProvider first) and collects tools. + Queries all providers in parallel (LocalProvider first) and collects tools. First provider wins for duplicate names. Filters by server blocklist. """ + results = await gather( + *[p.list_tools() for p in self._providers], + return_exceptions=True, + ) + all_tools: dict[str, Tool] = {} - for provider in self._providers: - try: - provider_tools = await provider.list_tools() - for tool in provider_tools: - if tool.name not in all_tools and self._is_component_enabled(tool): - all_tools[tool.name] = tool - except Exception as e: + for i, result in enumerate(results): + if isinstance(result, BaseException): + provider = self._providers[i] provider_name = getattr(provider, "server", provider).__class__.__name__ logger.warning( - f"Failed to get tools from provider {provider_name!r}: {e}" + f"Failed to get tools from provider {provider_name!r}: {result}" ) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result continue + for tool in result: + if tool.name not in all_tools and self._is_component_enabled(tool): + all_tools[tool.name] = tool return all_tools async def get_tool(self, name: str) -> Tool: """Get an enabled tool by name. - Iterates through all providers (LocalProvider first) to find the tool. + Queries all providers in parallel (LocalProvider first) to find the tool. First provider wins. Returns only if enabled. """ - for provider in self._providers: - try: - tool = await provider.get_tool(name) - if tool is not None and self._is_component_enabled(tool): - return tool - except NotFoundError: - continue + results = await gather( + *[p.get_tool(name) for p in self._providers], + return_exceptions=True, + ) + + for result in results: + if isinstance(result, Tool) and self._is_component_enabled(result): + return result raise NotFoundError(f"Unknown tool: {name}") @@ -833,151 +845,171 @@ async def _get_resource_or_template_or_none( Returns the original ResourceTemplate (not a Resource created from it) to preserve the registered function for task execution. - Iterates through all providers (LocalProvider first). + Queries all providers in parallel (LocalProvider first). First provider wins. Checks concrete resources first, then templates. """ - # First pass: check concrete resources from all providers - for provider in self._providers: - try: - resource = await provider.get_resource(uri) - if resource is not None and self._is_component_enabled(resource): - return resource - except NotFoundError: - continue + # Query both resources and templates from all providers in parallel + resource_results, template_results = await gather( + gather( + *[p.get_resource(uri) for p in self._providers], return_exceptions=True + ), + gather( + *[p.get_resource_template(uri) for p in self._providers], + return_exceptions=True, + ), + ) - # Second pass: check templates from all providers - for provider in self._providers: - try: - template = await provider.get_resource_template(uri) - if template is not None and self._is_component_enabled(template): - return template - except NotFoundError: - continue + # First pass: check concrete resources (priority over templates) + for result in resource_results: + if isinstance(result, Resource) and self._is_component_enabled(result): + return result + + # Second pass: check templates + for result in template_results: + if isinstance(result, ResourceTemplate) and self._is_component_enabled( + result + ): + return result return None async def get_resources(self) -> dict[str, Resource]: """Get all enabled resources from providers, indexed by URI. - Iterates through all providers (LocalProvider first) and collects resources. + Queries all providers in parallel (LocalProvider first) and collects resources. First provider wins for duplicate URIs. Filters by server blocklist. """ + results = await gather( + *[p.list_resources() for p in self._providers], + return_exceptions=True, + ) + all_resources: dict[str, Resource] = {} - for provider in self._providers: - try: - provider_resources = await provider.list_resources() - for resource in provider_resources: - uri = str(resource.uri) - if uri not in all_resources and self._is_component_enabled( - resource - ): - all_resources[uri] = resource - except Exception as e: + for i, result in enumerate(results): + if isinstance(result, BaseException): + provider = self._providers[i] provider_name = getattr(provider, "server", provider).__class__.__name__ logger.warning( - f"Failed to get resources from provider {provider_name!r}: {e}" + f"Failed to get resources from provider {provider_name!r}: {result}" ) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result continue + for resource in result: + uri = str(resource.uri) + if uri not in all_resources and self._is_component_enabled(resource): + all_resources[uri] = resource return all_resources async def get_resource(self, uri: str) -> Resource: """Get an enabled resource by URI. - Iterates through all providers (LocalProvider first) to find the resource. + Queries all providers in parallel (LocalProvider first) to find the resource. First provider wins. Returns only if enabled. """ - for provider in self._providers: - try: - resource = await provider.get_resource(uri) - if resource is not None and self._is_component_enabled(resource): - return resource - except NotFoundError: - continue + results = await gather( + *[p.get_resource(uri) for p in self._providers], + return_exceptions=True, + ) + + for result in results: + if isinstance(result, Resource) and self._is_component_enabled(result): + return result raise NotFoundError(f"Unknown resource: {uri}") async def get_resource_templates(self) -> dict[str, ResourceTemplate]: """Get all enabled resource templates from providers, indexed by uri_template. - Iterates through all providers (LocalProvider first) and collects templates. + Queries all providers in parallel (LocalProvider first) and collects templates. First provider wins for duplicate uri_templates. Filters by server blocklist. """ + results = await gather( + *[p.list_resource_templates() for p in self._providers], + return_exceptions=True, + ) + all_templates: dict[str, ResourceTemplate] = {} - for provider in self._providers: - try: - provider_templates = await provider.list_resource_templates() - for template in provider_templates: - if ( - template.uri_template not in all_templates - and self._is_component_enabled(template) - ): - all_templates[template.uri_template] = template - except Exception as e: + for i, result in enumerate(results): + if isinstance(result, BaseException): + provider = self._providers[i] provider_name = getattr(provider, "server", provider).__class__.__name__ logger.warning( - f"Failed to get resource templates from provider {provider_name!r}: {e}" + f"Failed to get resource templates from provider {provider_name!r}: {result}" ) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result continue + for template in result: + if ( + template.uri_template not in all_templates + and self._is_component_enabled(template) + ): + all_templates[template.uri_template] = template return all_templates async def get_resource_template(self, uri: str) -> ResourceTemplate: """Get an enabled resource template that matches the given URI. - Iterates through all providers (LocalProvider first) to find the template. + Queries all providers in parallel (LocalProvider first) to find the template. First provider wins. Returns only if enabled. """ - for provider in self._providers: - try: - template = await provider.get_resource_template(uri) - if template is not None and self._is_component_enabled(template): - return template - except NotFoundError: - continue + results = await gather( + *[p.get_resource_template(uri) for p in self._providers], + return_exceptions=True, + ) + + for result in results: + if isinstance(result, ResourceTemplate) and self._is_component_enabled( + result + ): + return result raise NotFoundError(f"Unknown resource template: {uri}") async def get_prompts(self) -> dict[str, Prompt]: """Get all enabled prompts from providers, indexed by name. - Iterates through all providers (LocalProvider first) and collects prompts. + Queries all providers in parallel (LocalProvider first) and collects prompts. First provider wins for duplicate names. Filters by server blocklist. """ + results = await gather( + *[p.list_prompts() for p in self._providers], + return_exceptions=True, + ) + all_prompts: dict[str, Prompt] = {} - for provider in self._providers: - try: - provider_prompts = await provider.list_prompts() - for prompt in provider_prompts: - if prompt.name not in all_prompts and self._is_component_enabled( - prompt - ): - all_prompts[prompt.name] = prompt - except Exception as e: + for i, result in enumerate(results): + if isinstance(result, BaseException): + provider = self._providers[i] provider_name = getattr(provider, "server", provider).__class__.__name__ logger.warning( - f"Failed to get prompts from provider {provider_name!r}: {e}" + f"Failed to get prompts from provider {provider_name!r}: {result}" ) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result continue + for prompt in result: + if prompt.name not in all_prompts and self._is_component_enabled( + prompt + ): + all_prompts[prompt.name] = prompt return all_prompts async def get_prompt(self, name: str) -> Prompt: """Get an enabled prompt by name. - Iterates through all providers (LocalProvider first) to find the prompt. + Queries all providers in parallel (LocalProvider first) to find the prompt. First provider wins. Returns only if enabled. """ - for provider in self._providers: - try: - prompt = await provider.get_prompt(name) - if prompt is not None and self._is_component_enabled(prompt): - return prompt - except NotFoundError: - continue + results = await gather( + *[p.get_prompt(name) for p in self._providers], + return_exceptions=True, + ) + + for result in results: + if isinstance(result, Prompt) and self._is_component_enabled(result): + return result raise NotFoundError(f"Unknown prompt: {name}") @@ -986,7 +1018,7 @@ async def get_component( ) -> Tool | Resource | ResourceTemplate | Prompt: """Get a component by its prefixed key. - Iterates through all providers (LocalProvider first) to find the component. + Queries all providers in parallel (LocalProvider first) to find the component. First provider wins. Args: @@ -998,13 +1030,14 @@ async def get_component( Raises: NotFoundError: If no component is found with the given key. """ - for provider in self._providers: - try: - component = await provider.get_component(key) - if component is not None: - return component - except NotFoundError: - continue + results = await gather( + *[p.get_component(key) for p in self._providers], + return_exceptions=True, + ) + + for result in results: + if isinstance(result, FastMCPComponent): + return result raise NotFoundError(f"Unknown component: {key}") @@ -1115,20 +1148,24 @@ async def _list_tools( """ List all available tools. - Iterates through all providers (LocalProvider first) and collects tools. + Queries all providers in parallel (LocalProvider first) and collects tools. First provider wins for duplicate keys. """ + results = await gather( + *[p.list_tools() for p in self._providers], + return_exceptions=True, + ) + all_tools: dict[str, Tool] = {} - for provider in self._providers: - try: - provider_tools = await provider.list_tools() - for tool in provider_tools: - if self._is_component_enabled(tool) and tool.key not in all_tools: - all_tools[tool.key] = tool - except Exception: - logger.exception("Error listing tools from provider") + for result in results: + if isinstance(result, BaseException): + logger.exception("Error listing tools from provider", exc_info=result) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result + continue + for tool in result: + if self._is_component_enabled(tool) and tool.key not in all_tools: + all_tools[tool.key] = tool return list(all_tools.values()) async def _list_resources_mcp(self) -> list[SDKResource]: @@ -1177,23 +1214,29 @@ async def _list_resources( """ List all available resources. - Iterates through all providers (LocalProvider first) and collects resources. + Queries all providers in parallel (LocalProvider first) and collects resources. First provider wins for duplicate keys. """ + results = await gather( + *[p.list_resources() for p in self._providers], + return_exceptions=True, + ) + all_resources: dict[str, Resource] = {} - for provider in self._providers: - try: - provider_resources = await provider.list_resources() - for resource in provider_resources: - if ( - self._is_component_enabled(resource) - and resource.key not in all_resources - ): - all_resources[resource.key] = resource - except Exception: - logger.exception("Error listing resources from provider") + for result in results: + if isinstance(result, BaseException): + logger.exception( + "Error listing resources from provider", exc_info=result + ) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result + continue + for resource in result: + if ( + self._is_component_enabled(resource) + and resource.key not in all_resources + ): + all_resources[resource.key] = resource return list(all_resources.values()) async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]: @@ -1243,23 +1286,29 @@ async def _list_resource_templates( """ List all available resource templates. - Iterates through all providers (LocalProvider first) and collects templates. + Queries all providers in parallel (LocalProvider first) and collects templates. First provider wins for duplicate keys. """ + results = await gather( + *[p.list_resource_templates() for p in self._providers], + return_exceptions=True, + ) + all_templates: dict[str, ResourceTemplate] = {} - for provider in self._providers: - try: - provider_templates = await provider.list_resource_templates() - for template in provider_templates: - if ( - self._is_component_enabled(template) - and template.key not in all_templates - ): - all_templates[template.key] = template - except Exception: - logger.exception("Error listing resource templates from provider") + for result in results: + if isinstance(result, BaseException): + logger.exception( + "Error listing resource templates from provider", exc_info=result + ) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result + continue + for template in result: + if ( + self._is_component_enabled(template) + and template.key not in all_templates + ): + all_templates[template.key] = template return list(all_templates.values()) async def _list_prompts_mcp(self) -> list[SDKPrompt]: @@ -1309,23 +1358,24 @@ async def _list_prompts( """ List all available prompts. - Iterates through all providers (LocalProvider first) and collects prompts. + Queries all providers in parallel (LocalProvider first) and collects prompts. First provider wins for duplicate keys. """ + results = await gather( + *[p.list_prompts() for p in self._providers], + return_exceptions=True, + ) + all_prompts: dict[str, Prompt] = {} - for provider in self._providers: - try: - provider_prompts = await provider.list_prompts() - for prompt in provider_prompts: - if ( - self._is_component_enabled(prompt) - and prompt.key not in all_prompts - ): - all_prompts[prompt.key] = prompt - except Exception: - logger.exception("Error listing prompts from provider") + for result in results: + if isinstance(result, BaseException): + logger.exception("Error listing prompts from provider", exc_info=result) if fastmcp.settings.mounted_components_raise_on_load_error: - raise + raise result + continue + for prompt in result: + if self._is_component_enabled(prompt) and prompt.key not in all_prompts: + all_prompts[prompt.key] = prompt return list(all_prompts.values()) async def _call_tool_mcp( diff --git a/src/fastmcp/utilities/async_utils.py b/src/fastmcp/utilities/async_utils.py new file mode 100644 index 0000000000..b69077c9d2 --- /dev/null +++ b/src/fastmcp/utilities/async_utils.py @@ -0,0 +1,56 @@ +"""Async utilities for FastMCP.""" + +from collections.abc import Awaitable +from typing import Literal, TypeVar, overload + +import anyio + +T = TypeVar("T") + + +@overload +async def gather( + *awaitables: Awaitable[T], + return_exceptions: Literal[True], +) -> list[T | BaseException]: ... + + +@overload +async def gather( + *awaitables: Awaitable[T], + return_exceptions: Literal[False] = ..., +) -> list[T]: ... + + +async def gather( + *awaitables: Awaitable[T], + return_exceptions: bool = False, +) -> list[T] | list[T | BaseException]: + """Run awaitables concurrently and return results in order. + + Uses anyio TaskGroup for structured concurrency. + + Args: + *awaitables: Awaitables to run concurrently + return_exceptions: If True, exceptions are returned in results. + If False, first exception cancels all and raises. + + Returns: + List of results in the same order as input awaitables. + """ + results: list[T | BaseException] = [None] * len(awaitables) # type: ignore[assignment] + + async def run_at(i: int, aw: Awaitable[T]) -> None: + try: + results[i] = await aw + except BaseException as e: + if return_exceptions: + results[i] = e + else: + raise + + async with anyio.create_task_group() as tg: + for i, aw in enumerate(awaitables): + tg.start_soon(run_at, i, aw) + + return results From 5e5796aedf4e80bf51e603d4771f3e9836a32339 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:25:01 -0500 Subject: [PATCH 2/4] Add __repr__ to Provider for cleaner logging --- src/fastmcp/server/providers/base.py | 3 +++ src/fastmcp/server/server.py | 18 ++++-------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/fastmcp/server/providers/base.py b/src/fastmcp/server/providers/base.py index 1c68b4f0e3..5817b0dd53 100644 --- a/src/fastmcp/server/providers/base.py +++ b/src/fastmcp/server/providers/base.py @@ -61,6 +61,9 @@ class Provider: def __init__(self) -> None: self._visibility = VisibilityFilter() + def __repr__(self) -> str: + return f"{self.__class__.__name__}()" + def with_transforms( self, *, diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index e0713b48ba..4f9de91277 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -808,10 +808,7 @@ async def get_tools(self) -> dict[str, Tool]: for i, result in enumerate(results): if isinstance(result, BaseException): provider = self._providers[i] - provider_name = getattr(provider, "server", provider).__class__.__name__ - logger.warning( - f"Failed to get tools from provider {provider_name!r}: {result}" - ) + logger.warning(f"Failed to get tools from {provider}: {result}") if fastmcp.settings.mounted_components_raise_on_load_error: raise result continue @@ -888,10 +885,7 @@ async def get_resources(self) -> dict[str, Resource]: for i, result in enumerate(results): if isinstance(result, BaseException): provider = self._providers[i] - provider_name = getattr(provider, "server", provider).__class__.__name__ - logger.warning( - f"Failed to get resources from provider {provider_name!r}: {result}" - ) + logger.warning(f"Failed to get resources from {provider}: {result}") if fastmcp.settings.mounted_components_raise_on_load_error: raise result continue @@ -933,9 +927,8 @@ async def get_resource_templates(self) -> dict[str, ResourceTemplate]: for i, result in enumerate(results): if isinstance(result, BaseException): provider = self._providers[i] - provider_name = getattr(provider, "server", provider).__class__.__name__ logger.warning( - f"Failed to get resource templates from provider {provider_name!r}: {result}" + f"Failed to get resource templates from {provider}: {result}" ) if fastmcp.settings.mounted_components_raise_on_load_error: raise result @@ -982,10 +975,7 @@ async def get_prompts(self) -> dict[str, Prompt]: for i, result in enumerate(results): if isinstance(result, BaseException): provider = self._providers[i] - provider_name = getattr(provider, "server", provider).__class__.__name__ - logger.warning( - f"Failed to get prompts from provider {provider_name!r}: {result}" - ) + logger.warning(f"Failed to get prompts from {provider}: {result}") if fastmcp.settings.mounted_components_raise_on_load_error: raise result continue From dac0e4ebdde247a1453a659f382314dd082790b3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:31:54 -0500 Subject: [PATCH 3/4] Simplify provider patterns and add __repr__ --- src/fastmcp/server/providers/base.py | 21 ++------- src/fastmcp/server/server.py | 67 +++++++++++----------------- 2 files changed, 31 insertions(+), 57 deletions(-) diff --git a/src/fastmcp/server/providers/base.py b/src/fastmcp/server/providers/base.py index 5817b0dd53..c65c266a01 100644 --- a/src/fastmcp/server/providers/base.py +++ b/src/fastmcp/server/providers/base.py @@ -231,23 +231,10 @@ async def get_component( self.list_resource_templates(), self.list_prompts(), ) - tools: Sequence[Tool] = results[0] # type: ignore[assignment] - resources: Sequence[Resource] = results[1] # type: ignore[assignment] - templates: Sequence[ResourceTemplate] = results[2] # type: ignore[assignment] - prompts: Sequence[Prompt] = results[3] # type: ignore[assignment] - - for tool in tools: - if tool.key == key: - return tool - for resource in resources: - if resource.key == key: - return resource - for template in templates: - if template.key == key: - return template - for prompt in prompts: - if prompt.key == key: - return prompt + for components in results: + for component in components: # type: ignore[union-attr] + if component.key == key: + return component return None # ------------------------------------------------------------------------- diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 4f9de91277..103fb0e845 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -483,11 +483,8 @@ async def _docket_lifespan(self) -> AsyncIterator[None]: for i, result in enumerate(task_results): if isinstance(result, BaseException): provider = self._providers[i] - provider_name = getattr( - provider, "server", provider - ).__class__.__name__ logger.warning( - f"Failed to register tasks from provider {provider_name!r}: {result}" + f"Failed to register tasks from {provider}: {result}" ) if fastmcp.settings.mounted_components_raise_on_load_error: raise result @@ -796,7 +793,7 @@ def _is_component_enabled(self, component: FastMCPComponent) -> bool: async def get_tools(self) -> dict[str, Tool]: """Get all enabled tools from providers, indexed by name. - Queries all providers in parallel (LocalProvider first) and collects tools. + Queries all providers in parallel and collects tools. First provider wins for duplicate names. Filters by server blocklist. """ results = await gather( @@ -820,7 +817,7 @@ async def get_tools(self) -> dict[str, Tool]: async def get_tool(self, name: str) -> Tool: """Get an enabled tool by name. - Queries all providers in parallel (LocalProvider first) to find the tool. + Queries all providers in parallel to find the tool. First provider wins. Returns only if enabled. """ results = await gather( @@ -842,30 +839,20 @@ async def _get_resource_or_template_or_none( Returns the original ResourceTemplate (not a Resource created from it) to preserve the registered function for task execution. - Queries all providers in parallel (LocalProvider first). + Queries all providers in parallel. First provider wins. Checks concrete resources first, then templates. """ - # Query both resources and templates from all providers in parallel - resource_results, template_results = await gather( - gather( - *[p.get_resource(uri) for p in self._providers], return_exceptions=True - ), - gather( - *[p.get_resource_template(uri) for p in self._providers], - return_exceptions=True, - ), + # Resources listed first so they have priority over templates + results = await gather( + *[p.get_resource(uri) for p in self._providers], + *[p.get_resource_template(uri) for p in self._providers], + return_exceptions=True, ) - # First pass: check concrete resources (priority over templates) - for result in resource_results: - if isinstance(result, Resource) and self._is_component_enabled(result): - return result - - # Second pass: check templates - for result in template_results: - if isinstance(result, ResourceTemplate) and self._is_component_enabled( - result - ): + for result in results: + if isinstance( + result, (Resource, ResourceTemplate) + ) and self._is_component_enabled(result): return result return None @@ -873,7 +860,7 @@ async def _get_resource_or_template_or_none( async def get_resources(self) -> dict[str, Resource]: """Get all enabled resources from providers, indexed by URI. - Queries all providers in parallel (LocalProvider first) and collects resources. + Queries all providers in parallel and collects resources. First provider wins for duplicate URIs. Filters by server blocklist. """ results = await gather( @@ -898,7 +885,7 @@ async def get_resources(self) -> dict[str, Resource]: async def get_resource(self, uri: str) -> Resource: """Get an enabled resource by URI. - Queries all providers in parallel (LocalProvider first) to find the resource. + Queries all providers in parallel to find the resource. First provider wins. Returns only if enabled. """ results = await gather( @@ -915,7 +902,7 @@ async def get_resource(self, uri: str) -> Resource: async def get_resource_templates(self) -> dict[str, ResourceTemplate]: """Get all enabled resource templates from providers, indexed by uri_template. - Queries all providers in parallel (LocalProvider first) and collects templates. + Queries all providers in parallel and collects templates. First provider wins for duplicate uri_templates. Filters by server blocklist. """ results = await gather( @@ -944,7 +931,7 @@ async def get_resource_templates(self) -> dict[str, ResourceTemplate]: async def get_resource_template(self, uri: str) -> ResourceTemplate: """Get an enabled resource template that matches the given URI. - Queries all providers in parallel (LocalProvider first) to find the template. + Queries all providers in parallel to find the template. First provider wins. Returns only if enabled. """ results = await gather( @@ -963,7 +950,7 @@ async def get_resource_template(self, uri: str) -> ResourceTemplate: async def get_prompts(self) -> dict[str, Prompt]: """Get all enabled prompts from providers, indexed by name. - Queries all providers in parallel (LocalProvider first) and collects prompts. + Queries all providers in parallel and collects prompts. First provider wins for duplicate names. Filters by server blocklist. """ results = await gather( @@ -989,7 +976,7 @@ async def get_prompts(self) -> dict[str, Prompt]: async def get_prompt(self, name: str) -> Prompt: """Get an enabled prompt by name. - Queries all providers in parallel (LocalProvider first) to find the prompt. + Queries all providers in parallel to find the prompt. First provider wins. Returns only if enabled. """ results = await gather( @@ -1008,7 +995,7 @@ async def get_component( ) -> Tool | Resource | ResourceTemplate | Prompt: """Get a component by its prefixed key. - Queries all providers in parallel (LocalProvider first) to find the component. + Queries all providers in parallel to find the component. First provider wins. Args: @@ -1138,7 +1125,7 @@ async def _list_tools( """ List all available tools. - Queries all providers in parallel (LocalProvider first) and collects tools. + Queries all providers in parallel and collects tools. First provider wins for duplicate keys. """ results = await gather( @@ -1204,7 +1191,7 @@ async def _list_resources( """ List all available resources. - Queries all providers in parallel (LocalProvider first) and collects resources. + Queries all providers in parallel and collects resources. First provider wins for duplicate keys. """ results = await gather( @@ -1276,7 +1263,7 @@ async def _list_resource_templates( """ List all available resource templates. - Queries all providers in parallel (LocalProvider first) and collects templates. + Queries all providers in parallel and collects templates. First provider wins for duplicate keys. """ results = await gather( @@ -1348,7 +1335,7 @@ async def _list_prompts( """ List all available prompts. - Queries all providers in parallel (LocalProvider first) and collects prompts. + Queries all providers in parallel and collects prompts. First provider wins for duplicate keys. """ results = await gather( @@ -1555,7 +1542,7 @@ async def _call_tool( """ Call a tool. - Iterates through all providers (LocalProvider first) to find the tool. + Iterates through all providers to find the tool. First provider wins. """ tool_name = context.message.name @@ -1654,7 +1641,7 @@ async def _read_resource( """ Read a resource. - Iterates through all providers (LocalProvider first) to find the resource. + Iterates through all providers to find the resource. First provider wins. Checks concrete resources first, then templates. Returns list[ResourceContent] for synchronous execution, or CreateTaskResult @@ -1798,7 +1785,7 @@ async def _get_prompt( """ Get a prompt. - Iterates through all providers (LocalProvider first) to find the prompt. + Iterates through all providers to find the prompt. First provider wins. Returns PromptResult for synchronous execution, or CreateTaskResult From 6fcd7a4eefe0fb3d99cf1050aa1d7f3b4dd3469b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:48:26 -0500 Subject: [PATCH 4/4] Add debug logging for unexpected errors in get operations --- src/fastmcp/server/server.py | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 103fb0e845..b3a8c40db2 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -825,7 +825,13 @@ async def get_tool(self, name: str) -> Tool: return_exceptions=True, ) - for result in results: + for i, result in enumerate(results): + if isinstance(result, BaseException): + if not isinstance(result, NotFoundError): + logger.debug( + f"Error getting tool from {self._providers[i]}: {result}" + ) + continue if isinstance(result, Tool) and self._is_component_enabled(result): return result @@ -850,6 +856,10 @@ async def _get_resource_or_template_or_none( ) for result in results: + if isinstance(result, BaseException): + if not isinstance(result, NotFoundError): + logger.debug(f"Error getting resource/template: {result}") + continue if isinstance( result, (Resource, ResourceTemplate) ) and self._is_component_enabled(result): @@ -893,7 +903,13 @@ async def get_resource(self, uri: str) -> Resource: return_exceptions=True, ) - for result in results: + for i, result in enumerate(results): + if isinstance(result, BaseException): + if not isinstance(result, NotFoundError): + logger.debug( + f"Error getting resource from {self._providers[i]}: {result}" + ) + continue if isinstance(result, Resource) and self._is_component_enabled(result): return result @@ -939,7 +955,13 @@ async def get_resource_template(self, uri: str) -> ResourceTemplate: return_exceptions=True, ) - for result in results: + for i, result in enumerate(results): + if isinstance(result, BaseException): + if not isinstance(result, NotFoundError): + logger.debug( + f"Error getting template from {self._providers[i]}: {result}" + ) + continue if isinstance(result, ResourceTemplate) and self._is_component_enabled( result ): @@ -984,7 +1006,13 @@ async def get_prompt(self, name: str) -> Prompt: return_exceptions=True, ) - for result in results: + for i, result in enumerate(results): + if isinstance(result, BaseException): + if not isinstance(result, NotFoundError): + logger.debug( + f"Error getting prompt from {self._providers[i]}: {result}" + ) + continue if isinstance(result, Prompt) and self._is_component_enabled(result): return result @@ -1012,7 +1040,13 @@ async def get_component( return_exceptions=True, ) - for result in results: + for i, result in enumerate(results): + if isinstance(result, BaseException): + if not isinstance(result, NotFoundError): + logger.debug( + f"Error getting component from {self._providers[i]}: {result}" + ) + continue if isinstance(result, FastMCPComponent): return result