Skip to content

Convert mounted servers to MountedProvider#2635

Merged
jlowin merged 8 commits intomainfrom
dynamic-components
Dec 18, 2025
Merged

Convert mounted servers to MountedProvider#2635
jlowin merged 8 commits intomainfrom
dynamic-components

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 17, 2025

Converts mounted servers to use the Provider abstraction, enabling them to participate fully in the provider system.

Key change: FastMCP.mount() now creates a MountedProvider that wraps the mounted server. This means mounted servers:

  • Are queried through the same provider chain as custom providers
  • Have their tools/resources/prompts discovered via list_* methods
  • Execute through the mounted server's middleware chain
  • Register tasks through the central docket registration
# Mounting now creates a MountedProvider internally
main = FastMCP("Main")
sub = FastMCP("Sub")

@sub.tool
def greet(name: str) -> str:
    return f"Hello, {name}!"

main.mount(sub, prefix="sub")  # Creates MountedProvider(sub, prefix="sub")

# Tool accessible as "sub_greet" through the provider chain

- Remove get_http_routes from Provider (unused)
- Remove ProviderLifespanConfig, _base_lifespan, _register_tasks
- Remove supports_tasks flag from Provider.__init__
- Consolidate all docket registration in server._docket_lifespan()
- Simplify lifespan() to take no parameters
- Move MountedProvider to separate module
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. labels Dec 17, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 17, 2025

Walkthrough

The PR replaces the mounted-servers model with a provider-centric design: it adds a Provider interface and Components/TaskComponents containers, introduces MountedProvider to expose a mounted FastMCP server with optional prefixing and name mappings, and updates server and ComponentService logic to iterate over and delegate to providers instead of MountedServer instances. Provider lifespans drive lifecycle management, provider-provided tools/resources/templates/prompts/tasks are registered into the docket with prefixed keys, public provider exports moved under fastmcp.server.providers, and legacy MountedServer and resource-prefix utilities were removed.

Possibly related PRs

  • fastmcp#2622 — introduces and wires the Provider abstraction and MountedProvider into server/provider integration (direct overlap in provider API and wiring).
  • fastmcp#2582 — changes mount/import APIs and server mounting logic (related to deprecating import_server and updating mount behavior).
  • fastmcp#2619 — modifies mounted-server name-override and tool name propagation (related to MountedProvider's tool_names mapping and name-prefixing).

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides a clear explanation of the key change with a code example, but lacks the required checklist items from the template (Contributors/Review checklists). Complete the Contributors and Review checklists from the template to confirm testing, documentation updates, and readiness for review.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: converting mounted servers to use the MountedProvider abstraction.
Docstring Coverage ✅ Passed Docstring coverage is 95.69% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dynamic-components

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/fastmcp/server/server.py (1)

884-901: Redundant exception handling for provider lookups.

The get_tool method catches NotFoundError from providers, but per the Provider interface contract, get_tool() returns None when not found (not raising an exception). The same pattern appears in get_resource, get_resource_template, and get_prompt.

While this defensive coding won't cause issues, it's inconsistent with the documented provider semantics: "Return None from get_* methods to indicate 'I don't have it'".

If you want to keep defensive handling, consider catching a broader Exception to handle provider implementation bugs, or simplify to only check for None:

-        except NotFoundError:
-            continue
+        except Exception:
+            continue  # Provider failed, try next

Or trust the provider contract:

         for provider in self._providers:
-            try:
-                tool = await provider.get_tool(key)
-                if tool is not None:
-                    return tool
-            except NotFoundError:
-                continue
+            tool = await provider.get_tool(key)
+            if tool is not None:
+                return tool
src/fastmcp/providers/mounted.py (1)

218-236: Potential performance concern with get_resources() for single lookup.

The get_resource() method calls self.server.get_resources() which retrieves all resources just to check if one exists. For servers with many resources, this could be inefficient.

However, I understand this is intentional to preserve the distinction between concrete resources and template-generated resources for task execution purposes.

If performance becomes a concern, consider using the resource manager directly:

# More efficient: check resource manager directly
if unprefixed not in self.server._resource_manager._resources:
    return None
resource = self.server._resource_manager._resources[unprefixed]

This mirrors what get_tasks() does for direct manager access.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e5a8fe and 115ec5c.

⛔ Files ignored due to path filters (4)
  • tests/client/test_client.py is excluded by none and included by none
  • tests/server/test_mount.py is excluded by none and included by none
  • tests/server/test_providers.py is excluded by none and included by none
  • tests/tools/test_tool_manager.py is excluded by none and included by none
📒 Files selected for processing (7)
  • examples/providers/sqlite/README.md (2 hunks)
  • examples/providers/sqlite/setup_db.py (1 hunks)
  • src/fastmcp/contrib/component_manager/component_service.py (7 hunks)
  • src/fastmcp/providers/__init__.py (1 hunks)
  • src/fastmcp/providers/base.py (12 hunks)
  • src/fastmcp/providers/mounted.py (1 hunks)
  • src/fastmcp/server/server.py (21 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/providers/__init__.py
  • src/fastmcp/contrib/component_manager/component_service.py
  • src/fastmcp/providers/mounted.py
  • src/fastmcp/providers/base.py
  • src/fastmcp/server/server.py
src/fastmcp/**/__init__.py

📄 CodeRabbit inference engine (AGENTS.md)

Be intentional about re-exports; core types that define a module's purpose should be exported; specialized features can live in submodules; only re-export to fastmcp.* for fundamental types

Files:

  • src/fastmcp/providers/__init__.py
🧠 Learnings (2)
📚 Learning: 2025-12-17T03:06:14.522Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Applies to src/fastmcp/**/__init__.py : Be intentional about re-exports; core types that define a module's purpose should be exported; specialized features can live in submodules; only re-export to fastmcp.* for fundamental types

Applied to files:

  • src/fastmcp/providers/__init__.py
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions

Applied to files:

  • src/fastmcp/providers/base.py
🪛 Ruff (0.14.8)
src/fastmcp/providers/mounted.py

195-195: Consider moving this statement to an else block

(TRY300)


299-299: Consider moving this statement to an else block

(TRY300)

src/fastmcp/server/server.py

901-901: Avoid specifying long messages outside the exception class

(TRY003)


991-991: Avoid specifying long messages outside the exception class

(TRY003)


1031-1031: Avoid specifying long messages outside the exception class

(TRY003)


1071-1071: Avoid specifying long messages outside the exception class

(TRY003)


1592-1592: Avoid specifying long messages outside the exception class

(TRY003)


1593-1593: Avoid specifying long messages outside the exception class

(TRY003)


1704-1704: Avoid specifying long messages outside the exception class

(TRY003)


1705-1705: Avoid specifying long messages outside the exception class

(TRY003)


1723-1723: Avoid specifying long messages outside the exception class

(TRY003)


1724-1724: Avoid specifying long messages outside the exception class

(TRY003)


1828-1828: Avoid specifying long messages outside the exception class

(TRY003)


1829-1829: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (16)
examples/providers/sqlite/setup_db.py (1)

7-7: Update aligns with new provider path structure.

The docstring update correctly reflects the new file location and is consistent with the provider-centric restructuring introduced in this PR.

examples/providers/sqlite/README.md (1)

15-15: Consistent path updates reflect new provider directory structure.

All path references have been correctly updated from examples/dynamic_tools_sqlite/ to examples/providers/sqlite/, maintaining consistency across usage examples, setup instructions, and runtime modification examples.

Also applies to: 18-18, 53-53, 56-56

src/fastmcp/providers/__init__.py (1)

1-34: Well-structured module initialization.

The re-exports are appropriate for this module's purpose - Components, Provider, and MountedProvider are core types that define the providers abstraction. The docstring example demonstrates the intended usage pattern clearly.

src/fastmcp/server/server.py (5)

478-492: Type safety concern with type: ignore comments on .fn access.

The code accesses .fn attribute on tools, resources, templates, and prompts returned from provider.get_tasks(), but these are typed as base Tool, Resource, etc. The # type: ignore[attr-defined] comments suppress the error, but if a provider returns non-function-based components from get_tasks(), this will fail at runtime.

The base Provider.get_tasks() already filters for FunctionTool, FunctionResource, etc., but custom provider implementations could return non-function types.

Consider adding runtime checks or narrowing the type annotations in Components:

# Option 1: Add runtime check before access
for tool in tasks.tools:
    if hasattr(tool, 'fn'):
        named_fn = _create_named_fn_wrapper(tool.fn, tool.key)
        docket.register(named_fn)

Alternatively, the Components dataclass could use more specific types like Sequence[FunctionTool] if only function-based components are expected.


546-548: LGTM!

The provider lifespan management using AsyncExitStack is correct. Provider lifespans are entered in registration order and will be properly exited in reverse order when the stack unwinds.


1182-1208: LGTM!

The deduplication logic correctly implements the documented semantics:

  1. Local (static) components take precedence for execution
  2. Later providers override earlier ones when there are duplicates
  3. Provider components appear first in the list for visibility

The comments clearly explain the intentional ordering behavior.


1572-1593: LGTM!

The provider tool execution logic correctly:

  1. Uses reversed() to give later providers precedence
  2. Validates tool existence before calling
  3. Allows validation errors to propagate unmasked
  4. Wraps unexpected exceptions with optional detail masking

2699-2720: Clean deprecation handling for as_proxy parameter.

The implementation properly:

  1. Emits a DeprecationWarning when as_proxy is used
  2. Still honors the flag for backward compatibility
  3. Documents the migration path in the warning message
src/fastmcp/providers/mounted.py (4)

57-77: LGTM!

The initialization is clean with proper super() call. The reverse mapping for tool names enables efficient lookup when stripping prefixes.


89-108: LGTM!

The _strip_tool_prefix method correctly handles:

  1. Tool name overrides via _reverse_tool_names
  2. Prefix pattern matching
  3. No-prefix case (always matches)

Returning None to signal "not this provider's responsibility" aligns with the Provider interface semantics.


360-370: LGTM!

The recursive task collection correctly handles nested providers. Each level applies its own prefix, so deeply nested mounts get properly prefixed at each level (e.g., parent_child_grandchild_tool).


376-387: LGTM!

The lifespan correctly invokes only the wrapped server's user-defined lifespan, not its full lifecycle manager. This avoids duplicate Docket/Worker creation since the parent server handles all background task infrastructure.

src/fastmcp/providers/base.py (4)

41-51: LGTM!

The Components dataclass provides a clean container for task-eligible components. Using Sequence with empty tuple defaults is appropriate for immutability and flexibility.


54-72: Well-documented Provider interface.

The docstring clearly explains:

  1. The None return convention for "not found"
  2. Precedence rules (static components win)
  3. Error handling semantics (list_* degrades gracefully, execution methods propagate)

This makes the contract clear for custom provider implementations.


227-268: LGTM!

The default get_tasks() implementation correctly:

  1. Filters for function-based component types that can be registered with Docket
  2. Excludes components with task_config.mode == "forbidden"
  3. Uses list_* methods which is appropriate for generic providers

Note: MountedProvider overrides this to bypass middleware, which is the correct design for mounted servers.


274-298: LGTM!

The default lifespan implementation is a no-op that can be overridden. The docstring provides a clear example of the expected pattern with proper try/finally for cleanup.

Comment thread src/fastmcp/contrib/component_manager/component_service.py Outdated
@jlowin jlowin changed the title Simplify Provider interface Convert mounted servers to MountedProvider Dec 17, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/providers/sqlite/server.py (1)

77-91: Method signatures don't match the updated Provider base class.

The list_tools and get_tool methods include a context: Context parameter, but the base Provider class (in src/fastmcp/server/providers/base.py) has updated these methods to be context-free:

  • async def list_tools(self) -> Sequence[Tool]
  • async def get_tool(self, name: str) -> Tool | None

This signature mismatch means these methods won't be called correctly by the provider infrastructure.

Apply this diff to fix the method signatures:

-    async def list_tools(self, context: Context) -> list[Tool]:
+    async def list_tools(self) -> list[Tool]:
         async with aiosqlite.connect(self.db_path) as db:
             db.row_factory = aiosqlite.Row
             async with db.execute("SELECT * FROM tools WHERE enabled = 1") as cursor:
                 rows = await cursor.fetchall()
                 return [self._make_tool(row) for row in rows]

-    async def get_tool(self, context: Context, name: str) -> Tool | None:
+    async def get_tool(self, name: str) -> Tool | None:
         async with aiosqlite.connect(self.db_path) as db:
             db.row_factory = aiosqlite.Row
             async with db.execute(

Also remove the unused Context import from line 24 if no longer needed elsewhere.

🧹 Nitpick comments (4)
src/fastmcp/server/server.py (2)

1573-1594: Consider consolidating error handling patterns.

The provider call_tool error handling duplicates the pattern from _execute_tool. While this is functional, you might consider extracting a shared error handling utility in a future refactor. The current implementation is correct and the static analysis hints about TRY003 (long exception messages) can be safely ignored as the messages provide useful debugging context.


1690-1725: Provider resource/template reads iterate providers twice - potential optimization.

The _read_resource method iterates through providers twice: once for concrete resources (lines 1691-1705) and once for templates (lines 1708-1724). If a provider is known to not have the resource, it will be queried again for templates.

This is functionally correct but could be optimized in a future iteration by combining the lookups. For now, this is acceptable given the clarity of the current implementation.

src/fastmcp/server/providers/mounted.py (2)

73-77: Potential silent collision if tool_names contains duplicate values.

If two original tool names map to the same custom name (e.g., {"a": "x", "b": "x"}), only one entry survives in _reverse_tool_names. This could cause one tool to become unreachable when stripping prefixes.

Consider validating uniqueness of values:

         self.tool_names = tool_names or {}
+        if len(self.tool_names) != len(set(self.tool_names.values())):
+            raise ValueError("tool_names values must be unique")
         self._reverse_tool_names = {v: k for k, v in self.tool_names.items()}

183-207: Tool retrieval and execution logic is correct.

The prefix-stripping before delegation and re-prefixing after retrieval ensures proper routing.

The static analysis hint at line 195 (TRY300) suggests moving return tool into an else block, which can marginally improve clarity but is stylistic:

         try:
             tool = await self.server.get_tool(unprefixed)
             # Return with prefixed key for parent's filter checking
             prefixed_key = name  # The name we received is already the prefixed form
             if tool.key != prefixed_key:
                 tool = tool.model_copy(key=prefixed_key)
-            return tool
         except NotFoundError:
             return None
+        else:
+            return tool
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba70dc5 and 5466bb7.

⛔ Files ignored due to path filters (2)
  • tests/server/test_mount.py is excluded by none and included by none
  • tests/server/test_providers.py is excluded by none and included by none
📒 Files selected for processing (7)
  • examples/providers/sqlite/server.py (1 hunks)
  • src/fastmcp/__init__.py (0 hunks)
  • src/fastmcp/contrib/component_manager/component_service.py (7 hunks)
  • src/fastmcp/server/providers/__init__.py (1 hunks)
  • src/fastmcp/server/providers/base.py (12 hunks)
  • src/fastmcp/server/providers/mounted.py (1 hunks)
  • src/fastmcp/server/server.py (22 hunks)
💤 Files with no reviewable changes (1)
  • src/fastmcp/init.py
🧰 Additional context used
📓 Path-based instructions (2)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/contrib/component_manager/component_service.py
  • src/fastmcp/server/server.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/providers/mounted.py
  • src/fastmcp/server/providers/base.py
src/fastmcp/**/__init__.py

📄 CodeRabbit inference engine (AGENTS.md)

Be intentional about re-exports; core types that define a module's purpose should be exported; specialized features can live in submodules; only re-export to fastmcp.* for fundamental types

Files:

  • src/fastmcp/server/providers/__init__.py
🧠 Learnings (1)
📚 Learning: 2025-12-17T03:06:14.522Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Applies to src/fastmcp/**/__init__.py : Be intentional about re-exports; core types that define a module's purpose should be exported; specialized features can live in submodules; only re-export to fastmcp.* for fundamental types

Applied to files:

  • src/fastmcp/server/providers/__init__.py
🧬 Code graph analysis (3)
src/fastmcp/contrib/component_manager/component_service.py (2)
src/fastmcp/server/providers/mounted.py (1)
  • MountedProvider (25-387)
src/fastmcp/server/server.py (8)
  • tool (1904-1919)
  • tool (1922-1937)
  • tool (1939-2081)
  • has_resource_prefix (3056-3092)
  • remove_resource_prefix (3012-3053)
  • prompt (2295-2307)
  • prompt (2310-2322)
  • prompt (2324-2474)
examples/providers/sqlite/server.py (2)
src/fastmcp/server/context.py (1)
  • Context (117-1003)
src/fastmcp/server/providers/base.py (1)
  • Provider (55-299)
src/fastmcp/server/providers/__init__.py (2)
src/fastmcp/server/providers/base.py (2)
  • Components (43-52)
  • Provider (55-299)
src/fastmcp/server/providers/mounted.py (1)
  • MountedProvider (25-387)
🪛 Ruff (0.14.8)
src/fastmcp/server/server.py

901-901: Avoid specifying long messages outside the exception class

(TRY003)


991-991: Avoid specifying long messages outside the exception class

(TRY003)


1031-1031: Avoid specifying long messages outside the exception class

(TRY003)


1071-1071: Avoid specifying long messages outside the exception class

(TRY003)


1592-1592: Avoid specifying long messages outside the exception class

(TRY003)


1593-1593: Avoid specifying long messages outside the exception class

(TRY003)


1704-1704: Avoid specifying long messages outside the exception class

(TRY003)


1705-1705: Avoid specifying long messages outside the exception class

(TRY003)


1723-1723: Avoid specifying long messages outside the exception class

(TRY003)


1724-1724: Avoid specifying long messages outside the exception class

(TRY003)


1828-1828: Avoid specifying long messages outside the exception class

(TRY003)


1829-1829: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/providers/mounted.py

195-195: Consider moving this statement to an else block

(TRY300)


299-299: Consider moving this statement to an else block

(TRY300)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (22)
src/fastmcp/server/providers/base.py (4)

1-53: LGTM! Clean module structure and Components dataclass.

The module docstring with example is helpful, and the Components dataclass with Sequence types and empty tuple defaults is well-designed for immutability and type safety.


55-93: LGTM! Provider base class with sensible defaults.

The Provider class documentation clearly explains the semantics (return None to indicate "I don't have it"), error handling behavior, and precedence rules. Default implementations returning empty lists/None allow subclasses to implement only what they need.


228-269: Consider whether filtering for mode != "forbidden" is sufficient.

The get_tasks() method filters components where task_config.mode != "forbidden". This correctly excludes forbidden components from Docket registration. The logic is sound, though the imports inside the method suggest circular import avoidance - this is acceptable.


275-299: LGTM! Clean lifespan implementation.

The lifespan() async context manager follows the standard pattern with a simple yield. The docstring clearly explains the expected usage for subclasses.

examples/providers/sqlite/server.py (1)

23-25: LGTM! Import path updated correctly.

The import path for Provider has been correctly updated to use fastmcp.server.providers instead of the old location.

src/fastmcp/contrib/component_manager/component_service.py (3)

10-11: LGTM! Import updated for provider-centric architecture.

The import of MountedProvider from the new providers module is correct.


44-55: LGTM! Provider-based tool routing with correct control flow.

The logic correctly:

  1. Iterates providers in reverse order (later providers win)
  2. Filters for MountedProvider instances
  3. Checks for prefix match before delegating
  4. Uses else: continue to skip non-matching prefixes

108-121: LGTM! Resource routing control flow is now correct.

The else: continue at lines 119-120 is correctly aligned with the inner if has_resource_prefix(...) check, ensuring the loop continues to the next provider when the prefix doesn't match. This addresses the previously reported control flow issue.

src/fastmcp/server/providers/__init__.py (1)

1-35: LGTM! Intentional public API surface for providers module.

The module correctly exports the core types (Components, Provider, MountedProvider) that define the provider abstraction. The __all__ declaration makes the public API explicit. Based on learnings, this follows the guideline to be intentional about re-exports and export core types that define a module's purpose.

src/fastmcp/server/server.py (5)

84-84: LGTM! Provider import updated to new module path.

The import of Provider from fastmcp.server.providers aligns with the new provider-centric architecture.


478-492: LGTM! Provider task registration with proper name wrapping.

The Docket registration for provider components correctly uses _create_named_fn_wrapper to create unique function names, avoiding collisions between mounted servers with identically-named functions. The type: ignore[attr-defined] comments are justified since get_tasks() filters for function-based variants.


546-548: LGTM! Provider lifespans integrated into server lifecycle.

Provider lifespans are correctly entered via the AsyncExitStack, ensuring proper startup/shutdown sequencing with the server's lifespan.


1182-1208: LGTM! Correct deduplication logic - local components take precedence.

The implementation correctly:

  1. Collects local tools first
  2. Collects provider tools (later providers override earlier)
  3. Removes provider tools that conflict with local tools
  4. Returns provider tools first in list for visibility, but local tools win for execution

2699-2720: LGTM! Clean deprecation path for as_proxy parameter.

The deprecation warning is clear and provides migration guidance. The backward compatibility handling that creates a proxy when as_proxy=True is a thoughtful transition path.

src/fastmcp/server/providers/mounted.py (8)

1-23: Clean import structure with proper type checking guard.

The use of TYPE_CHECKING to guard the FastMCP import avoids circular dependencies at runtime while maintaining type safety.


83-133: Prefix handling logic is sound.

The helper methods correctly handle the bidirectional prefix transformations. The local imports at lines 115 and 129 appropriately avoid circular dependencies.


139-172: Efficient prefix application with conditional copying.

The model_copy approach avoids unnecessary object creation when no changes are needed. The pattern is consistent across all component types.


213-248: Resource methods correctly distinguish concrete resources from templates.

The get_resource method explicitly documents that it only returns concrete resources (line 221-222), preserving templates for proper task execution. The middleware delegation in read_resource is consistent with the tool pattern.


254-276: Template handling properly delegates to the server's middleware.

The read_resource_template correctly delegates to read_resource since the server's middleware handles template resolution internally.


287-311: Prompt methods follow the same pattern as tools.

The same TRY300 suggestion applies at line 299 as noted for the tool methods. The implementation is consistent and correct.


317-370: Direct manager access is intentional and well-documented.

The docstring clearly explains the rationale for bypassing middleware during task registration. The recursive collection from nested providers (lines 361-366) properly handles hierarchical provider structures with correct prefix propagation.


376-387: Lifecycle correctly starts only the user-defined lifespan.

The distinction between the user's lifespan and the full _lifespan_manager() (which includes Docket) is important—the parent server handles background tasks centrally.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/fastmcp/server/providers/mounted.py (2)

57-77: Consider validating uniqueness of tool_names values.

If tool_names contains duplicate values (e.g., {"foo": "bar", "baz": "bar"}), the reverse mapping at line 77 will silently overwrite, causing only one original name to be recoverable. This could lead to unexpected behavior when resolving custom names back to originals.

If this is a concern, consider adding validation:

         self.tool_names = tool_names or {}
+        if len(self.tool_names) != len(set(self.tool_names.values())):
+            raise ValueError("tool_names values must be unique")
         self._reverse_tool_names = {v: k for k, v in self.tool_names.items()}

305-358: Tight coupling via private attribute access is documented but worth noting.

The direct access to internal manager dictionaries (_tool_manager._tools, _resource_manager._resources, etc.) tightly couples this provider to the server's internal structure. The docstring explains this is intentional to bypass middleware during registration.

Consider adding a dedicated non-middleware method on FastMCP (e.g., _get_raw_tasks()) to encapsulate this access pattern, reducing the surface area of private attribute dependencies:

# In FastMCP class:
def _get_raw_tasks(self) -> Components:
    """Return raw task-eligible components without middleware."""
    # Centralized access to internal managers
    ...

This would make future refactoring of internal structures easier.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5466bb7 and 58d7484.

📒 Files selected for processing (1)
  • src/fastmcp/server/providers/mounted.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/providers/mounted.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Applies to tests/**/*.py : Pass FastMCP servers directly to clients for in-memory testing instead of using network transport
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (8)
src/fastmcp/server/providers/mounted.py (8)

1-23: LGTM!

Clean module structure with proper TYPE_CHECKING guard for the FastMCP import to avoid circular dependencies. Import organization is appropriate.


83-108: LGTM!

The prefix handling logic correctly prioritizes tool_names overrides before checking the prefix pattern, and the fallback for unprefixed providers (line 107-108) is appropriate.


183-199: LGTM!

The early-exit optimization at line 186 prevents unnecessary middleware calls when the prefix doesn't match. The get_tool implementation correctly goes through list_tools() to ensure middleware participation as stated in the PR objectives.


234-244: LGTM!

Good use of specific exception handling with NotFoundError rather than a bare except. The fallback to None when resource is not found allows the provider chain to continue searching.


255-272: LGTM!

The template matching logic correctly uses the unprefixed URI against unprefixed templates from middleware, then returns the prefixed version. Delegation to read_resource for reading is appropriate since the server's middleware handles template resolution internally.


278-299: LGTM!

Prompt methods follow the same pattern as tools, consistently using underscore-separated prefixes. The middleware call ensures proper execution through the mounted server's chain.


364-375: LGTM!

The lifespan context manager correctly delegates to the mounted server's user lifespan while allowing the parent server's Docket to handle background tasks. The docstring clearly explains this distinction.


139-172: The model_copy calls in these methods are valid. Pydantic v2 model_copy accepts an update dictionary for field changes, but FastMCPComponent overrides model_copy with a custom implementation that explicitly accepts a key parameter. All four model classes (Tool, Resource, ResourceTemplate, Prompt) inherit from FastMCPComponent and therefore support this API. The code follows the codebase's design pattern correctly.

Likely an incorrect or invalid review comment.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The test test_mounted_resource_template_task times out (>5.0s) due to premature resource template execution in MountedProvider.get_resource().

Root Cause: Commit 58d7484 modified MountedProvider.get_resource() to call template.create_resource() when a template matches the URI. This causes the template's function to execute immediately during resource lookup, before the task infrastructure is properly set up. The previous version explicitly avoided this by only returning concrete resources and preserving templates for later execution.

Suggested Solution:

Revert the get_resource() behavior to match the previous implementation that explicitly avoided creating resources from templates. The key changes:

  1. File: src/fastmcp/server/providers/mounted.py:210-233

  2. Remove the template-checking logic from get_resource():

    # Remove these lines (223-233):
    # Also check templates
    template = await self.get_resource_template(uri)
    if template is None:
        return None
    params = template.matches(uri)
    if params is None:
        return None
    return await template.create_resource(uri, params)  # <-- This executes the template function!
  3. Restore the original behavior that only returns concrete resources:

    async def get_resource(self, uri: str) -> Resource | None:
        # Early exit if URI doesn't match our prefix pattern
        if self._strip_resource_prefix(uri) is None:
            return None
            
        # Only check concrete resources (not templates)
        # This preserves the original template for task execution
        resources = await self.list_resources()
        return next((r for r in resources if r.key == uri), None)

Why this fixes it: Resource templates need special handling during task execution. When get_resource() calls template.create_resource(), it triggers template.read(arguments=params) which executes the template function. This premature execution happens before the task context is ready, causing the timeout. By keeping get_resource() separate from template handling, the system can properly set up task infrastructure via get_resource_template()read_resource_template() path.

Detailed Analysis

The test failure occurs in: tests/server/tasks/test_task_mount.py::TestMountedResourceTasks::test_mounted_resource_template_task

Test code (line 280-289):

async def test_mounted_resource_template_task(self, parent_server):
    async with Client(parent_server) as client:
        task = await client.read_resource("child://child/item/99.json", task=True)
        assert not task.returned_immediately
        result = await task.result()
        assert '"itemId": "99"' in result[0].text

Execution flow that causes the hang:

  1. Client calls read_resource() with task=True on parent server
  2. Parent's _read_resource() iterates through providers
  3. Finds MountedProvider, calls provider.get_resource_template(uri) → returns matching template
  4. Calls provider.read_resource_template(uri) (line 1712 in server.py)
  5. read_resource_template() delegates to read_resource() (line 272 in mounted.py)
  6. BUT, earlier in step 3, get_resource() is also called (line 1693 in server.py)
  7. The NEW get_resource() finds the template and calls create_resource()
  8. This executes the template function at the wrong time in the execution path
  9. The task infrastructure isn't ready, causing a timeout

Key insight from previous code (5466bb7):

# Only check concrete resources (not templates that match the URI)
# This preserves the original template for task execution

This comment was removed in 58d7484, but the reasoning still applies.

Related Files
  • src/fastmcp/server/providers/mounted.py:210-233 - get_resource() method that needs modification
  • src/fastmcp/server/server.py:1690-1726 - _read_resource() that calls provider methods
  • src/fastmcp/resources/template.py:240-257 - create_resource() that executes template function
  • tests/server/tasks/test_task_mount.py:280-289 - Failing test

Reverts template-checking in get_resource that broke task execution.
Tasks need access to the original template, not instantiated resources.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/fastmcp/server/providers/mounted.py (1)

110-133: Inline imports resolve circular dependencies—consider extracting utilities to a separate module for improved maintainability.

The imports at lines 115 and 129 work around a circular dependency between mounted.py and server.py. While functional and well-documented, this pattern can make code harder to maintain and breaks static analysis tools. Consider moving add_resource_prefix, has_resource_prefix, and remove_resource_prefix to a dedicated utility module (e.g., fastmcp.server.resource_prefix_utils) to eliminate the circular dependency.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58d7484 and cacce67.

📒 Files selected for processing (1)
  • src/fastmcp/server/providers/mounted.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/providers/mounted.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Applies to tests/**/*.py : Pass FastMCP servers directly to clients for in-memory testing instead of using network transport
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Analyze (python)
🔇 Additional comments (7)
src/fastmcp/server/providers/mounted.py (7)

1-23: LGTM!

The imports and module documentation are well-structured and clear.


83-108: LGTM!

The prefix handling logic correctly supports both custom name mappings and standard prefixing, with proper early-exit routing for non-matching names.


178-199: LGTM!

The tool methods correctly route through the mounted server's middleware chain while properly handling prefix matching and early exits for non-matching names.


224-234: LGTM!

The resource reading correctly routes through middleware and handles errors gracefully. The code assumes _read_resource_middleware returns a list, with line 232 extracting the first content item.


268-289: LGTM!

The prompt methods follow the same consistent pattern as tools, properly routing through middleware with prefix handling.


354-365: LGTM!

The lifespan method correctly starts only the user-defined lifespan, avoiding duplicate Docket initialization. The comment clearly explains the design decision.


139-145: Clarify prefixing behavior when tool_names mapping is applied.

The tool_names mapping allows direct customization of tool keys, but this bypasses the standard prefix logic. When a tool name is found in tool_names, the mapped value is used directly as new_key without applying _add_tool_prefix(). This differs from the fallback case and from _prefix_resource(), which always applies a prefix. Either apply the prefix to the mapped value for consistency, or update the docstring to clarify that the mapping takes precedence over prefixing.

Comment thread src/fastmcp/server/providers/mounted.py
Comment thread src/fastmcp/server/providers/mounted.py Outdated
- Add resource prefix functions (add/remove/has_resource_prefix) to mounted.py
- Deprecate import_server with warning to use mount() instead
- Add tool_names uniqueness validation in MountedProvider
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/fastmcp/server/server.py (2)

478-492: Inconsistent key selection for Docket registration may cause collisions.

The code uses different attributes for different component types:

  • Tools: tool.key (line 482)
  • Resources: resource.name (line 485)
  • Templates: template.name (line 488)
  • Prompts: prompt.key (line 491)

For resources and templates, using .name instead of .key (the URI) could cause collisions if multiple resources from different providers happen to have the same display name. Consider using .key consistently for all component types to ensure uniqueness.

🔎 Suggested fix:
                     for resource in tasks.resources:
-                        named_fn = _create_named_fn_wrapper(resource.fn, resource.name)  # type: ignore[attr-defined]
+                        named_fn = _create_named_fn_wrapper(resource.fn, resource.key)  # type: ignore[attr-defined]
                         docket.register(named_fn)
                     for template in tasks.templates:
-                        named_fn = _create_named_fn_wrapper(template.fn, template.name)  # type: ignore[attr-defined]
+                        named_fn = _create_named_fn_wrapper(template.fn, template.key)  # type: ignore[attr-defined]
                         docket.register(named_fn)

1690-1725: Consider combining the two provider loops for resources and templates.

The code has two separate reverse iterations over providers: one for concrete resources (lines 1691-1705) and one for templates (lines 1708-1724). While functionally correct, this could be slightly more efficient by checking both in a single loop.

However, the current separation is clear and maintainable, so this is optional.

src/fastmcp/server/providers/mounted.py (1)

221-227: Consider efficiency of get_tool implementation.

The current implementation calls list_tools() (which invokes middleware and prefixes all tools) just to find a single tool. While functionally correct, this could be inefficient for servers with many tools.

A potential optimization would be to directly look up the unprefixed name in the mounted server's tools and apply prefixing only to the result. However, this would require ensuring middleware is properly invoked for the single-tool case, which may not be worth the added complexity.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cacce67 and e6b16ff.

⛔ Files ignored due to path filters (1)
  • tests/server/test_server.py is excluded by none and included by none
📒 Files selected for processing (3)
  • src/fastmcp/contrib/component_manager/component_service.py (7 hunks)
  • src/fastmcp/server/providers/mounted.py (1 hunks)
  • src/fastmcp/server/server.py (24 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/server.py
  • src/fastmcp/server/providers/mounted.py
  • src/fastmcp/contrib/component_manager/component_service.py
🧬 Code graph analysis (2)
src/fastmcp/server/server.py (2)
src/fastmcp/server/providers/base.py (14)
  • Provider (55-299)
  • get_tasks (228-269)
  • lifespan (276-299)
  • get_prompt (196-206)
  • list_tools (75-80)
  • get_tool (82-92)
  • get_resource (117-127)
  • get_resource_template (136-150)
  • list_resources (110-115)
  • list_resource_templates (129-134)
  • list_prompts (189-194)
  • call_tool (94-108)
  • read_resource (152-167)
  • render_prompt (208-222)
src/fastmcp/server/providers/mounted.py (15)
  • get_tasks (333-386)
  • lifespan (393-403)
  • get_prompt (311-317)
  • list_tools (216-219)
  • get_tool (221-227)
  • get_resource (248-260)
  • get_resource_template (283-295)
  • list_resources (243-246)
  • list_resource_templates (278-281)
  • list_prompts (306-309)
  • call_tool (229-237)
  • read_resource (262-272)
  • render_prompt (319-327)
  • MountedProvider (67-403)
  • add_resource_prefix (29-37)
src/fastmcp/contrib/component_manager/component_service.py (2)
src/fastmcp/server/context.py (1)
  • fastmcp (169-174)
src/fastmcp/server/providers/mounted.py (3)
  • MountedProvider (67-403)
  • has_resource_prefix (55-64)
  • remove_resource_prefix (40-52)
🪛 Ruff (0.14.8)
src/fastmcp/server/server.py

901-901: Avoid specifying long messages outside the exception class

(TRY003)


991-991: Avoid specifying long messages outside the exception class

(TRY003)


1031-1031: Avoid specifying long messages outside the exception class

(TRY003)


1071-1071: Avoid specifying long messages outside the exception class

(TRY003)


1592-1592: Avoid specifying long messages outside the exception class

(TRY003)


1593-1593: Avoid specifying long messages outside the exception class

(TRY003)


1704-1704: Avoid specifying long messages outside the exception class

(TRY003)


1705-1705: Avoid specifying long messages outside the exception class

(TRY003)


1723-1723: Avoid specifying long messages outside the exception class

(TRY003)


1724-1724: Avoid specifying long messages outside the exception class

(TRY003)


1828-1828: Avoid specifying long messages outside the exception class

(TRY003)


1829-1829: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/providers/mounted.py

35-35: Avoid specifying long messages outside the exception class

(TRY003)


46-46: Avoid specifying long messages outside the exception class

(TRY003)


61-61: Avoid specifying long messages outside the exception class

(TRY003)


120-120: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (14)
src/fastmcp/contrib/component_manager/component_service.py (3)

10-12: LGTM on imports.

The imports correctly bring in MountedProvider from the providers module and the prefix utility functions from mounted.py.


109-122: Control flow is now correct.

The else: continue block at lines 120-121 is properly aligned with the inner if has_resource_prefix(...) check, ensuring that when a provider has a prefix but the key doesn't match, the loop continues to the next provider. This is consistent with the tool and prompt methods.


177-188: LGTM on prompt enable/disable logic.

The control flow is consistent with the tool methods, properly continuing to the next provider when the prefix doesn't match.

src/fastmcp/server/server.py (5)

546-548: LGTM on provider lifespan management.

Using AsyncExitStack ensures proper startup in registration order and cleanup in reverse order when the server shuts down.


740-743: LGTM on defensive prompt lookup.

Wrapping get_prompt in a try/except allows the handler to gracefully continue when the prompt doesn't exist, matching the check pattern used elsewhere.


1182-1208: Clear and well-documented aggregation logic.

The implementation correctly:

  1. Gives local tools precedence over provider tools
  2. Allows later providers to override earlier ones (forward iteration)
  3. Returns provider tools first in the list for visibility

The code is well-commented and the behavior is clear.


1572-1594: LGTM on provider tool execution.

The reverse iteration ensures later providers take precedence (matching the "last wins" semantics). Error handling properly distinguishes validation errors (never masked) from execution errors (optionally masked).


2762-2770: LGTM on deprecation handling.

The deprecation warning correctly guides users to use mount() instead while maintaining backward compatibility for existing code.

src/fastmcp/server/providers/mounted.py (6)

29-64: LGTM on URI prefix utility functions.

The implementations correctly:

  1. Validate URI format before processing
  2. Use re.escape() to prevent regex injection
  3. Handle edge cases (empty prefix returns original URI)
  4. Return the original URI if prefix doesn't match in remove_resource_prefix

115-121: LGTM on initialization with uniqueness validation.

The validation at lines 119-120 correctly ensures tool_names values are unique before building the reverse mapping, preventing silent overwrites that would cause unpredictable tool resolution. This addresses the previous review feedback.


177-183: LGTM on tool prefixing logic.

The method correctly:

  1. Checks for explicit name overrides first via tool_names
  2. Falls back to standard prefixing using _add_tool_prefix
  3. Avoids unnecessary copies when the key doesn't change

333-386: Direct manager access is intentional for task registration.

The comment at lines 336-338 explains that direct manager access is intentional to avoid triggering middleware during Docket registration. While this creates coupling to internal structures (as noted in past reviews), it's a pragmatic trade-off.

The implementation correctly:

  1. Filters for function-based components only
  2. Respects task_config.mode != "forbidden"
  3. Recursively collects from nested providers with proper prefixing

392-403: LGTM on lifespan management.

The implementation correctly starts only the mounted server's user-defined lifespan, leaving Docket management to the parent server. This prevents duplicate task registration and worker conflicts.


268-272: Swallowing NotFoundError is intentional but consider logging.

The try/except catches NotFoundError and returns None, allowing the parent server to try other providers. This is correct for the provider chain pattern.

However, other exceptions from _read_resource_middleware would propagate up, which is appropriate for actual errors vs. "not found" situations.

Comment thread src/fastmcp/contrib/component_manager/component_service.py
Comment thread src/fastmcp/server/server.py
Comment thread src/fastmcp/server/server.py
- Remove unused _is_mounted flag (MountedProvider.lifespan() calls _lifespan
  not _lifespan_manager, so the flag was never checked)
- Fix provider iteration: change reversed() to forward order in execution
  methods (_call_tool, _read_resource_middleware, _get_prompt_content_middleware)
  to match documented "first non-None wins" semantics
- Fix ComponentService to handle prefix-less mounted servers using
  _strip_tool_prefix()/_strip_resource_prefix() methods
- Update conflict resolution tests to expect first-registered provider wins
- Add regression tests for Docket behavior and prefix-less ComponentService
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/fastmcp/server/server.py (2)

874-891: Provider iteration order is now consistent with _call_tool.

The previous review flagged an inconsistency where get_tool used forward order while _call_tool used reversed(). Looking at the current code, both methods now iterate self._providers in forward order (lines 883 and 1563), which resolves the previously flagged issue. The "first registered provider wins" semantics are now consistent.


2708-2710: Missing _is_mounted flag on mounted server.

The mount() method creates a MountedProvider but doesn't set server._is_mounted = True. While the new provider-based architecture may handle this differently, setting this flag would:

  1. Allow code to detect whether a server is mounted
  2. Prevent issues if the mounted server is accidentally run independently later
#!/bin/bash
# Check if _is_mounted is used elsewhere in the codebase
rg -n '_is_mounted' --type=py
🧹 Nitpick comments (2)
src/fastmcp/server/server.py (2)

468-482: Missing exception handling for provider task registration.

The provider iteration for Docket registration lacks exception handling, unlike the similar patterns in get_tools, get_resources, etc. which catch exceptions and optionally continue based on mounted_components_raise_on_load_error. If provider.get_tasks() fails, the entire server startup will fail.

Consider adding consistent error handling:

🔎 Suggested fix:
                 # Register provider components
                 for provider in self._providers:
-                    tasks = await provider.get_tasks()
-                    for tool in tasks.tools:
-                        named_fn = _create_named_fn_wrapper(tool.fn, tool.key)  # type: ignore[attr-defined]
-                        docket.register(named_fn)
-                    for resource in tasks.resources:
-                        named_fn = _create_named_fn_wrapper(resource.fn, resource.name)  # type: ignore[attr-defined]
-                        docket.register(named_fn)
-                    for template in tasks.templates:
-                        named_fn = _create_named_fn_wrapper(template.fn, template.name)  # type: ignore[attr-defined]
-                        docket.register(named_fn)
-                    for prompt in tasks.prompts:
-                        named_fn = _create_named_fn_wrapper(prompt.fn, prompt.key)  # type: ignore[attr-defined]
-                        docket.register(named_fn)
+                    try:
+                        tasks = await provider.get_tasks()
+                        for tool in tasks.tools:
+                            named_fn = _create_named_fn_wrapper(tool.fn, tool.key)  # type: ignore[attr-defined]
+                            docket.register(named_fn)
+                        for resource in tasks.resources:
+                            named_fn = _create_named_fn_wrapper(resource.fn, resource.name)  # type: ignore[attr-defined]
+                            docket.register(named_fn)
+                        for template in tasks.templates:
+                            named_fn = _create_named_fn_wrapper(template.fn, template.name)  # type: ignore[attr-defined]
+                            docket.register(named_fn)
+                        for prompt in tasks.prompts:
+                            named_fn = _create_named_fn_wrapper(prompt.fn, prompt.key)  # type: ignore[attr-defined]
+                            docket.register(named_fn)
+                    except Exception as e:
+                        provider_name = getattr(provider, "server", provider).__class__.__name__
+                        logger.warning(f"Failed to register tasks from provider {provider_name!r}: {e}")
+                        if fastmcp.settings.mounted_components_raise_on_load_error:
+                            raise

1680-1715: Consider consolidating provider resource and template lookup.

The two separate loops (lines 1681-1695 for resources, 1698-1714 for templates) have nearly identical exception handling. While functionally correct, this could be simplified. However, the current implementation is clear and the separation makes the priority chain explicit.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6b16ff and 5931612.

⛔ Files ignored due to path filters (1)
  • tests/server/test_mount.py is excluded by none and included by none
📒 Files selected for processing (2)
  • src/fastmcp/contrib/component_manager/component_service.py (7 hunks)
  • src/fastmcp/server/server.py (24 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/fastmcp/contrib/component_manager/component_service.py
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/server.py
🪛 Ruff (0.14.8)
src/fastmcp/server/server.py

891-891: Avoid specifying long messages outside the exception class

(TRY003)


981-981: Avoid specifying long messages outside the exception class

(TRY003)


1021-1021: Avoid specifying long messages outside the exception class

(TRY003)


1061-1061: Avoid specifying long messages outside the exception class

(TRY003)


1582-1582: Avoid specifying long messages outside the exception class

(TRY003)


1583-1583: Avoid specifying long messages outside the exception class

(TRY003)


1694-1694: Avoid specifying long messages outside the exception class

(TRY003)


1695-1695: Avoid specifying long messages outside the exception class

(TRY003)


1713-1713: Avoid specifying long messages outside the exception class

(TRY003)


1714-1714: Avoid specifying long messages outside the exception class

(TRY003)


1818-1818: Avoid specifying long messages outside the exception class

(TRY003)


1819-1819: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (12)
src/fastmcp/server/server.py (12)

115-129: LGTM! Clean helper for Docket name mapping.

The wrapper correctly preserves the original function's metadata via @functools.wraps while allowing a custom __name__ for Docket registration. The local import of functools is acceptable given this is a rarely-called utility.


536-538: LGTM! Provider lifespans correctly managed.

Using AsyncExitStack ensures proper cleanup of provider lifespans in reverse order on exit, which is the expected behavior for nested context managers.


730-733: LGTM! Specific exception handling.

Catching NotFoundError specifically (rather than a bare except) correctly follows the coding guidelines and allows the handler to fall through to subsequent logic.


904-941: LGTM! Clear fallback chain for resource/template lookup.

The method correctly implements a priority chain: local resources → local templates → provider resources → provider templates. Catching NotFoundError specifically and continuing is the right approach.


1172-1198: LGTM! Correct conflict resolution semantics.

The implementation correctly implements:

  1. Local tools always win over provider tools (line 1193-1194 removes conflicts)
  2. Among providers, later providers override earlier ones (line 1186)
  3. Provider tools appear first in the list for visibility, but local tools take execution precedence

The exception handling with except Exception plus logger.exception is appropriate for this aggregation pattern.


1552-1584: LGTM! Robust tool execution with proper error handling.

The execution flow correctly:

  1. Prioritizes local tools (first-registered wins)
  2. Uses forward provider iteration (consistent with get_tool)
  3. Handles errors appropriately: validation errors are never masked, while other exceptions respect _mask_error_details

The ToolError wrapping pattern with conditional detail masking is a good security practice.


1792-1819: LGTM! Consistent prompt execution pattern.

The prompt execution follows the same well-structured pattern as tool execution: local-first lookup, forward provider iteration, and proper exception handling with _mask_error_details support.


2712-2760: LGTM! Proper deprecation pattern.

The deprecation warning with stacklevel=2 correctly points to the caller. The method remains functional for backward compatibility while guiding users toward mount().


2693-2706: LGTM! Clean deprecation with backward compatibility.

The as_proxy deprecation correctly:

  1. Emits a warning with guidance on the new approach
  2. Still honors the flag for backward compatibility
  3. Provides a clear migration path

1004-1021: LGTM! Consistent resource template lookup.

The method correctly implements local-first lookup with provider fallback, maintaining consistency with other getter methods.


84-84: LGTM! Clean import for new provider module.

The import correctly brings in Provider from the new providers package.


1113-1122: Remove misleading docstring reference to provider HTTP routes.

The docstring states this method returns routes "including from providers," but providers don't expose HTTP routes and the implementation only returns locally registered routes. Update the docstring to reflect the actual behavior.

…stration

- Create TaskComponents dataclass with FunctionTool/FunctionResource/etc. types
  for proper typing of get_tasks() return value
- Add try/except wrapper around provider.get_tasks() in _docket_lifespan for
  consistent error handling (warn + continue or raise based on settings)
- Remove type: ignore comments from server.py task registration loop
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/fastmcp/server/providers/mounted.py (1)

337-390: Direct access to private manager internals breaks encapsulation.

Lines 355, 359, 366, and 373 directly access private attributes like self.server._tool_manager._tools, bypassing encapsulation. While the comment on line 354 explains this is intentional to avoid middleware, this creates tight coupling and fragility.

Based on the past review comment, consider adding a public method on FastMCP or the managers to retrieve task-eligible components:

🔎 Suggested approach
# On FastMCP
async def get_task_eligible_components(self) -> TaskComponents:
    """Return components eligible for task registration (bypassing middleware)."""
    from fastmcp.prompts.prompt import FunctionPrompt
    from fastmcp.resources.resource import FunctionResource
    from fastmcp.resources.template import FunctionResourceTemplate
    from fastmcp.tools.tool import FunctionTool
    
    tools = [
        t for t in self._tool_manager._tools.values()
        if isinstance(t, FunctionTool) and t.task_config.mode != "forbidden"
    ]
    resources = [
        r for r in self._resource_manager._resources.values()
        if isinstance(r, FunctionResource) and r.task_config.mode != "forbidden"
    ]
    templates = [
        t for t in self._resource_manager._templates.values()
        if isinstance(t, FunctionResourceTemplate) and t.task_config.mode != "forbidden"
    ]
    prompts = [
        p for p in self._prompt_manager._prompts.values()
        if isinstance(p, FunctionPrompt) and p.task_config.mode != "forbidden"
    ]
    return TaskComponents(tools=tools, resources=resources, templates=templates, prompts=prompts)

Then in MountedProvider.get_tasks():

async def get_tasks(self) -> TaskComponents:
    """Return task-eligible components with prefixes applied."""
    components = await self.server.get_task_eligible_components()
    
    tools = [self._prefix_tool(t) for t in components.tools]
    resources = [self._prefix_resource(r) for r in components.resources]
    templates = [self._prefix_template(t) for t in components.templates]
    prompts = [self._prefix_prompt(p) for p in components.prompts]
    
    # Recursively get tasks from nested providers
    for provider in self.server._providers:
        nested = await provider.get_tasks()
        tools.extend(self._prefix_tool(t) for t in nested.tools)
        resources.extend(self._prefix_resource(r) for r in nested.resources)
        templates.extend(self._prefix_template(t) for t in nested.templates)
        prompts.extend(self._prefix_prompt(p) for p in nested.prompts)
    
    return TaskComponents(tools=tools, resources=resources, templates=templates, prompts=prompts)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5931612 and 7edb1ba.

📒 Files selected for processing (4)
  • src/fastmcp/server/providers/__init__.py (1 hunks)
  • src/fastmcp/server/providers/base.py (13 hunks)
  • src/fastmcp/server/providers/mounted.py (1 hunks)
  • src/fastmcp/server/server.py (24 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/fastmcp/server/providers/init.py
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/server.py
  • src/fastmcp/server/providers/mounted.py
  • src/fastmcp/server/providers/base.py
🪛 Ruff (0.14.8)
src/fastmcp/server/server.py

905-905: Avoid specifying long messages outside the exception class

(TRY003)


995-995: Avoid specifying long messages outside the exception class

(TRY003)


1035-1035: Avoid specifying long messages outside the exception class

(TRY003)


1075-1075: Avoid specifying long messages outside the exception class

(TRY003)


1596-1596: Avoid specifying long messages outside the exception class

(TRY003)


1597-1597: Avoid specifying long messages outside the exception class

(TRY003)


1708-1708: Avoid specifying long messages outside the exception class

(TRY003)


1709-1709: Avoid specifying long messages outside the exception class

(TRY003)


1727-1727: Avoid specifying long messages outside the exception class

(TRY003)


1728-1728: Avoid specifying long messages outside the exception class

(TRY003)


1832-1832: Avoid specifying long messages outside the exception class

(TRY003)


1833-1833: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/providers/mounted.py

39-39: Avoid specifying long messages outside the exception class

(TRY003)


50-50: Avoid specifying long messages outside the exception class

(TRY003)


65-65: Avoid specifying long messages outside the exception class

(TRY003)


124-124: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (6)
src/fastmcp/server/providers/mounted.py (2)

103-125: LGTM!

The initialization logic is correct. The uniqueness validation for tool_names values (lines 123-124) properly addresses the past review concern, ensuring the reverse mapping won't have collisions.


396-407: LGTM!

The lifespan implementation correctly starts only the mounted server's user-defined lifespan (line 406), avoiding double initialization of Docket and allowing the parent server to manage background tasks.

src/fastmcp/server/server.py (3)

867-905: LGTM!

The tool lookup methods correctly prioritize local tools over provider tools, and use consistent forward iteration order for provider queries. The error handling allows graceful degradation when providers fail to load.


2726-2819: Deprecation properly implemented.

The deprecation warning (lines 2770-2774) and documentation clearly guide users toward using mount() instead. The import from fastmcp.server.providers.mounted (line 2768) maintains functionality during the deprecation period.


2657-2724: Remove the outdated _is_mounted suggestion.

The _is_mounted flag suggested in the past review comment does not exist in the codebase and is unnecessary with the current provider-centric architecture. Mounted servers are now tracked via the MountedProvider and self._providers list. Setting server._is_mounted = True would add dead code that is never referenced or checked. The mount() implementation is already complete and correct as-is.

Likely an incorrect or invalid review comment.

src/fastmcp/server/providers/base.py (1)

1-316: LGTM! Well-designed provider abstraction.

The Provider base class provides a clean, context-free interface with sensible defaults:

  • Components and TaskComponents containers separate runtime components from task-eligible ones
  • Method signatures are simplified (no Context parameter required)
  • get_tasks() provides filtering logic that subclasses can override
  • lifespan() context manager enables provider-specific setup/teardown

This design enables flexible provider implementations while maintaining consistency across the codebase.

Comment on lines +468 to +496
# Register provider components
for provider in self._providers:
try:
tasks = await provider.get_tasks()
for tool in tasks.tools:
named_fn = _create_named_fn_wrapper(tool.fn, tool.key)
docket.register(named_fn)
for resource in tasks.resources:
named_fn = _create_named_fn_wrapper(
resource.fn, resource.name
)
docket.register(named_fn)
for template in tasks.templates:
named_fn = _create_named_fn_wrapper(
template.fn, template.name
)
docket.register(named_fn)
for prompt in tasks.prompts:
named_fn = _create_named_fn_wrapper(prompt.fn, prompt.key)
docket.register(named_fn)
except Exception as e:
provider_name = getattr(
provider, "server", provider
).__class__.__name__
logger.warning(
f"Failed to register tasks from provider {provider_name!r}: {e}"
)
if fastmcp.settings.mounted_components_raise_on_load_error:
raise
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find where Resource, ResourceTemplate, Tool, and Prompt are defined
find . -type f -name "*.py" | head -20
git ls-files "*.py" | grep -E "(model|task|provider|resource|tool|prompt)" | head -30

Repository: jlowin/fastmcp

Length of output: 2152


🏁 Script executed:

# Search for Resource and ResourceTemplate class definitions
rg -n "class Resource" --type=py -A 10 | head -50
rg -n "class ResourceTemplate" --type=py -A 10 | head -50
rg -n "class Tool" --type=py -A 10 | head -50
rg -n "class Prompt" --type=py -A 10 | head -50

Repository: jlowin/fastmcp

Length of output: 10591


🏁 Script executed:

# Check how docket.register is called with resources and templates in the code
rg -n "\.name|\.key" src/fastmcp/server/server.py -B 2 -A 2 | head -60

Repository: jlowin/fastmcp

Length of output: 2793


🏁 Script executed:

# Look at the actual code snippet to understand the context better
sed -n '468,496p' src/fastmcp/server/server.py

Repository: jlowin/fastmcp

Length of output: 1618


🏁 Script executed:

# Get the Resource class definition
cat -n src/fastmcp/resources/resource.py | head -80

Repository: jlowin/fastmcp

Length of output: 2864


🏁 Script executed:

# Get the ResourceTemplate class full definition
cat -n src/fastmcp/resources/template.py | head -150

Repository: jlowin/fastmcp

Length of output: 6123


🏁 Script executed:

# Get the Tool class definition
cat -n src/fastmcp/tools/tool.py | grep -A 30 "^   123"

Repository: jlowin/fastmcp

Length of output: 1306


🏁 Script executed:

# Get the Prompt class definition
rg -n "class Prompt\(" --type=py -A 30 src/fastmcp/prompts/

Repository: jlowin/fastmcp

Length of output: 2073


🏁 Script executed:

# Find FastMCPComponent definition
rg -n "class FastMCPComponent" --type=py -A 50

Repository: jlowin/fastmcp

Length of output: 3800


🏁 Script executed:

# Also check if Resource class has explicit .key or .name properties
cat -n src/fastmcp/resources/resource.py | sed -n '80,250p'

Repository: jlowin/fastmcp

Length of output: 7403


🏁 Script executed:

# Check what exceptions _create_named_fn_wrapper might raise
rg -n "_create_named_fn_wrapper" --type=py -B 5 -A 20 src/fastmcp/server/

Repository: jlowin/fastmcp

Length of output: 5170


Inconsistent Docket registration keys and bare exception handling.

Lines 477 and 482 register resources and templates using .name, while lines 473 and 486 use .key for tools and prompts. Since resources and templates are indexed by .key elsewhere in the codebase (lines 966, 1006), this inconsistency could cause lookup failures. Use .key consistently across all component types.

Additionally, line 488 uses a bare except Exception which violates the coding guideline requiring specific exception types. Replace with appropriate exception type(s).

🤖 Prompt for AI Agents
In src/fastmcp/server/server.py around lines 468 to 496, fix two issues:
register resources and templates using .key (not .name) so all component types
consistently use .key for docket registration, and replace the bare "except
Exception" with a narrow exception handler that targets likely failure modes
from provider.get_tasks and registration (for example AttributeError, TypeError,
ValueError, RuntimeError) — catch those explicitly (e.g., except
(AttributeError, TypeError, ValueError, RuntimeError) as e) and keep the
existing logging/raise behavior.

@jlowin jlowin merged commit ede8ff6 into main Dec 18, 2025
13 checks passed
@jlowin jlowin deleted the dynamic-components branch December 18, 2025 03:21
@jlowin jlowin added this to the 2.15 milestone Dec 18, 2025
@jlowin jlowin added the provider Related to the FastMCP Provider class label Dec 19, 2025
@jlowin jlowin added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. and removed enhancement Improvement to existing functionality. For issues and smaller PR improvements. labels Dec 26, 2025
@jlowin jlowin added enhancement Improvement to existing functionality. For issues and smaller PR improvements. and removed feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. labels Jan 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing functionality. For issues and smaller PR improvements. provider Related to the FastMCP Provider class server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant