Skip to content

Unify component storage in LocalProvider#2680

Merged
jlowin merged 10 commits intomainfrom
local-provider-refactor
Dec 24, 2025
Merged

Unify component storage in LocalProvider#2680
jlowin merged 10 commits intomainfrom
local-provider-refactor

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 23, 2025

The three manager classes (ToolManager, ResourceManager, PromptManager) duplicated logic that already existed in the provider pattern. This PR consolidates them into a single LocalProvider that stores locally-defined components.

Before: FastMCP had separate managers for each component type, plus a provider chain. The managers stored decorator-registered components, while providers supplied dynamic components. This created two parallel paths for component lookup.

After: LocalProvider is a proper Provider that stores decorator-registered components. FastMCP's _providers list always starts with LocalProvider, so the server just iterates providers for all operations. Server decorators (@tool, @resource, @prompt) and add_* methods are thin passthroughs to LocalProvider—they inject server defaults and delegate. Single code path, simpler architecture.

# LocalProvider is the first provider, stores local components
mcp = FastMCP("App")

@mcp.tool  # Delegates to mcp._local_provider.tool() with server defaults
def greet(name: str) -> str:
    return f"Hello, {name}!"

# Internally: mcp._local_provider stores the tool
# get_tools() iterates _providers (LocalProvider first)

Breaking: ToolManager, ResourceManager, PromptManager are deleted. Users importing these directly will get ImportError. These were internal implementation details, not public API.

Consolidates ToolManager, ResourceManager, and PromptManager into a single
LocalProvider class. This aligns with the existing provider architecture
where FastMCP iterates through providers for all operations.

- LocalProvider stores tools, resources, templates, and prompts
- FastMCP.tool/resource/prompt decorators delegate to LocalProvider
- Delete redundant manager classes and their tests
- Update tests to use public API (get_tools() vs internal dict access)
@marvin-context-protocol marvin-context-protocol Bot added breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. labels Dec 23, 2025
@jlowin jlowin added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. provider Related to the FastMCP Provider class and removed enhancement Improvement to existing functionality. For issues and smaller PR improvements. labels Dec 23, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 23, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Walkthrough

This PR refactors FastMCP to a provider-first architecture: replaces per-type managers (ToolManager, ResourceManager, PromptManager) with a LocalProvider and a providers list, delegates add/get/list/remove operations to LocalProvider or iterates providers (first-wins deduplication), and wires LocalProvider into FastMCP constructor. ComponentService was updated for prompt mounting and to use provider-based lookups. FastMCPProvider's task collection now traverses nested providers. Provider base gained a _notify helper. Several public package all exports were adjusted to remove manager classes.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is comprehensive, explaining the before/after architecture, the breaking change, and includes a code example. However, it does not follow the provided template structure with all required sections and checklists. Add all required sections from the template: explicitly check off the Contributors Checklist items, complete the Review Checklist, and ensure all template sections are present and filled.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main objective of consolidating component storage into LocalProvider, accurately reflecting the primary change in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 86.73% which is sufficient. The required threshold is 80.00%.

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: 0

Caution

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

⚠️ Outside diff range comments (2)
src/fastmcp/contrib/component_manager/component_service.py (2)

196-204: Bug: Using "tool" component_type for prompt lookups.

Lines 198 and 224 pass "tool" to _get_mounted_server_and_key for prompt operations. Since prompts use the same namespace transformation as tools (name-based, not URI-based), this happens to work correctly, but it's semantically misleading and could cause issues if prompt transformations diverge from tool transformations in the future.

🔎 Proposed fix for semantic clarity
     # 2. Check mounted servers via FastMCPProvider/TransformingProvider
     for provider in self._server._providers:
-        result = _get_mounted_server_and_key(provider, key, "tool")
+        result = _get_mounted_server_and_key(provider, key, "prompt")
         if result is not None:
             server, unprefixed = result
             mounted_service = ComponentService(server)
             prompt = await mounted_service._enable_prompt(unprefixed)
             return prompt

Note: This requires updating _get_mounted_server_and_key to handle "prompt" the same as "tool" for now:

 def _get_mounted_server_and_key(
     provider: Provider,
     key: str,
     component_type: str,
 ) -> tuple[FastMCP, str] | None:
     ...
     if isinstance(provider, TransformingProvider):
         # TransformingProvider - reverse the transformation
         if component_type == "resource":
             original = provider._reverse_resource_uri(key)
-        else:
+        else:  # tool or prompt - both use name-based transformation
             original = provider._reverse_tool_name(key)

222-229: Same bug: Using "tool" component_type for disable_prompt.

Same issue as above - Line 224 uses "tool" for prompt lookup.

🔎 Proposed fix
     # 2. Check mounted servers via FastMCPProvider/TransformingProvider
     for provider in self._server._providers:
-        result = _get_mounted_server_and_key(provider, key, "tool")
+        result = _get_mounted_server_and_key(provider, key, "prompt")
🧹 Nitpick comments (3)
examples/mount_example.py (1)

106-106: Prefer public API over private methods in examples.

Line 106 calls _read_resource_mcp(), a private method. Examples should demonstrate only stable, public APIs to avoid coupling examples to implementation details that may change.

If _read_resource_mcp() is internal/experimental, either:

  • Replace it with an equivalent public API call if one exists, or
  • Add a comment clarifying that this demonstrates internal behavior (not recommended for production)

Can you confirm whether a public alternative exists, or if this line should be removed/updated?

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

252-256: Consider efficiency optimization for get_tool.

The current implementation calls list_tools() which applies all transformations, then searches linearly. For providers with many tools, this could be inefficient. However, since transformations need to be applied and the number of tools is typically small, this is acceptable for now.

If performance becomes a concern, consider caching transformed tools or implementing a more direct lookup:

async def get_tool(self, name: str) -> Tool | None:
    """Get a tool by name, with transformations applied."""
    # Check if tool exists before transforming
    if name not in self._tools and name not in self._tool_transformations:
        return None
    tools = await self.list_tools()
    return next((t for t in tools if t.name == name), None)

458-468: Consider using TypeError for classmethod validation.

The static analysis correctly identifies that TypeError is more appropriate than ValueError when the issue is about the type of input (classmethod vs regular function). This is a minor stylistic issue.

🔎 Proposed fix
         if isinstance(name_or_fn, classmethod):
-            raise ValueError(
+            raise TypeError(
                 inspect.cleandoc(
                     """
                     To decorate a classmethod, first define the method and then call
                     tool() directly on the method instead of using it as a
                     decorator. See https://gofastmcp.com/patterns/decorating-methods
                     for examples and more information.
                     """
                 )
             )

Apply similar changes at lines 592-602 and 740-750.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4177d83 and f26d41f.

⛔ Files ignored due to path filters (18)
  • tests/client/test_client.py is excluded by none and included by none
  • tests/contrib/test_component_manager.py is excluded by none and included by none
  • tests/deprecated/test_exclude_args.py is excluded by none and included by none
  • tests/deprecated/test_import_server.py is excluded by none and included by none
  • tests/deprecated/test_settings.py is excluded by none and included by none
  • tests/prompts/test_prompt_manager.py is excluded by none and included by none
  • tests/resources/test_resource_manager.py is excluded by none and included by none
  • tests/server/providers/openapi/test_performance_comparison.py is excluded by none and included by none
  • tests/server/providers/test_local_provider.py is excluded by none and included by none
  • tests/server/proxy/test_proxy_client.py is excluded by none and included by none
  • tests/server/tasks/test_sync_function_task_disabled.py is excluded by none and included by none
  • tests/server/tasks/test_task_config.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/server/test_server.py is excluded by none and included by none
  • tests/server/test_tool_annotations.py is excluded by none and included by none
  • tests/server/test_tool_transformation.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 (12)
  • examples/mount_example.py
  • src/fastmcp/contrib/component_manager/component_service.py
  • src/fastmcp/prompts/__init__.py
  • src/fastmcp/prompts/prompt_manager.py
  • src/fastmcp/resources/__init__.py
  • src/fastmcp/resources/resource_manager.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • src/fastmcp/server/providers/local.py
  • src/fastmcp/server/server.py
  • src/fastmcp/tools/__init__.py
  • src/fastmcp/tools/tool_manager.py
💤 Files with no reviewable changes (4)
  • src/fastmcp/resources/resource_manager.py
  • src/fastmcp/tools/tool_manager.py
  • src/fastmcp/prompts/prompt_manager.py
  • src/fastmcp/resources/init.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥ 3.10 with full type annotations
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/prompts/__init__.py
  • src/fastmcp/contrib/component_manager/component_service.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • examples/mount_example.py
  • src/fastmcp/tools/__init__.py
  • src/fastmcp/server/providers/local.py
  • src/fastmcp/server/server.py
src/fastmcp/**/__init__.py

📄 CodeRabbit inference engine (AGENTS.md)

Core types that define a module's purpose should be exported (e.g., Middleware from fastmcp.server.middleware), while specialized features can live in submodules

Files:

  • src/fastmcp/prompts/__init__.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/tools/__init__.py
🧠 Learnings (4)
📓 Common learnings
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
📚 Learning: 2025-12-21T21:37:55.031Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.031Z
Learning: Applies to src/fastmcp/__init__.py : All module exports should be intentional - only re-export to `fastmcp.*` for fundamental types like `FastMCP` and `Client`, prefer users importing from specific submodules for specialized features

Applied to files:

  • src/fastmcp/prompts/__init__.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/tools/__init__.py
  • src/fastmcp/server/server.py
📚 Learning: 2025-12-21T21:37:55.031Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.031Z
Learning: Applies to src/fastmcp/**/__init__.py : Core types that define a module's purpose should be exported (e.g., `Middleware` from `fastmcp.server.middleware`), while specialized features can live in submodules

Applied to files:

  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/server.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/server/providers/local.py
🧬 Code graph analysis (3)
src/fastmcp/prompts/__init__.py (1)
src/fastmcp/prompts/prompt.py (3)
  • Message (38-46)
  • Prompt (118-322)
  • PromptResult (73-115)
src/fastmcp/server/providers/__init__.py (1)
src/fastmcp/server/providers/local.py (1)
  • LocalProvider (54-804)
src/fastmcp/server/server.py (3)
src/fastmcp/server/providers/local.py (18)
  • add_tool_transformation (209-218)
  • list_tools (244-250)
  • tool (357-373)
  • tool (376-392)
  • tool (394-530)
  • list_resources (266-268)
  • resource (532-655)
  • list_resource_templates (284-286)
  • list_prompts (308-310)
  • prompt (658-670)
  • prompt (673-685)
  • prompt (687-804)
  • add_tool (102-115)
  • remove_tool (117-128)
  • remove_tool_transformation (231-238)
  • add_resource (130-140)
  • add_template (155-165)
  • add_prompt (180-190)
src/fastmcp/server/providers/base.py (5)
  • Provider (56-374)
  • list_tools (142-147)
  • list_resources (161-166)
  • list_resource_templates (180-185)
  • list_prompts (203-208)
src/fastmcp/resources/resource.py (2)
  • key (302-304)
  • Resource (137-331)
🪛 Ruff (0.14.10)
src/fastmcp/server/providers/local.py

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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


459-468: Prefer TypeError exception for invalid type

(TRY004)


502-505: Avoid specifying long messages outside the exception class

(TRY003)


511-513: Avoid specifying long messages outside the exception class

(TRY003)


586-589: Avoid specifying long messages outside the exception class

(TRY003)


593-602: Prefer TypeError exception for invalid type

(TRY004)


650-653: Avoid specifying long messages outside the exception class

(TRY003)


741-750: Prefer TypeError exception for invalid type

(TRY004)


780-783: Avoid specifying long messages outside the exception class

(TRY003)


789-791: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/server.py

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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


2150-2150: 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 windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
🔇 Additional comments (12)
examples/mount_example.py (1)

1-8: ✓ Docstring update aligns with ToolManager removal.

The module docstring has been appropriately updated to remove references to ToolManager and now accurately describes the mounting functionality. This aligns with the PR's consolidation of component management into LocalProvider.

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

32-32: LGTM! LocalProvider properly exported as a core type.

The export aligns with the coding guidelines that core types defining a module's purpose should be exported. LocalProvider is now the central component storage mechanism, making it an appropriate public export.

Also applies to: 41-41

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

1-8: LGTM! PromptManager correctly removed from public API.

The removal of PromptManager aligns with the PR's objective to consolidate component storage in LocalProvider. Core prompt types remain properly exported.

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

196-226: Deprecation handling looks correct, but consider edge case with multiple deprecated params.

If a user provides multiple deprecated parameters (e.g., on_duplicate_tools="error" and on_duplicate_resources="replace"), only the first one encountered will be used. This behavior is acceptable since the new API consolidates these into a single setting, but users might expect a warning if they provide conflicting values.

Consider whether conflicting deprecated parameters should emit an additional warning to help users migrate their code.


236-248: LGTM! LocalProvider correctly initialized as first in provider chain.

The initialization ensures local components registered via decorators take precedence over dynamic providers, maintaining backward compatibility with the previous manager-based approach.


1685-1707: LGTM! Duplicate handling logic is correct.

The implementation properly handles all four DuplicateBehavior values. The direct access to _local_provider._tools is acceptable here since the server owns and manages the local provider.


994-1008: LGTM! Consistent first-provider-wins pattern for component listing.

The iteration pattern is applied consistently across tools, resources, templates, and prompts. The _should_enable_component check combined with duplicate key checking ensures correct filtering behavior.


1501-1523: Verify two-pass resource lookup behavior.

The implementation now prioritizes concrete resources from all providers before checking templates from any provider. Previously, each provider was checked for both resources and templates before moving to the next. This could change behavior if:

  • Provider A has a template matching data://foo/123
  • Provider B has a concrete resource data://foo/123

Previously, Provider A's template might have been used; now Provider B's concrete resource wins.

Confirm this two-pass global lookup (concrete resources first across all providers, then templates) is the intended behavior change.

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

1-4: LGTM! ToolManager correctly removed from public API.

The removal aligns with the PR's objective. Core tool types (Tool, FunctionTool) and transform utilities (forward, forward_raw) remain properly exported.

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

559-584: LGTM! Task aggregation correctly traverses nested providers.

The implementation properly delegates to each provider's get_tasks() method, aggregating components for Docket registration. This aligns with the unified provider architecture and eliminates direct manager access.

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

89-239: LGTM! Clean storage interface with appropriate error handling.

The storage methods provide a consistent interface for all component types. Using KeyError for missing items on removal is the correct choice, and the error messages are appropriately descriptive.


1-52: LGTM! Well-documented module with clear examples.

The module docstring and class documentation provide excellent examples for both standalone usage and server integration. The type hints and overloads ensure good IDE support.

- Use _reverse_prompt_name for prompts instead of _reverse_tool_name
- Remove private method usage from mount example
Server decorators and add_* methods now delegate to LocalProvider.
Notifications moved to base Provider class and called by LocalProvider.
Move behavior tests (type serialization, parameters, context injection, decorators) to LocalProvider test files. Keep user-facing feature tests (tags, enabled) in test_server_interactions.py. Add delegation tests to test_server.py.
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/local_provider.py (1)

470-480: Consider using TypeError for invalid type errors.

When the decorated target is a classmethod, the error is about an invalid type being passed, which conventionally should be a TypeError rather than ValueError. This applies to similar checks in resource (line 605) and prompt (line 753) decorators as well.

🔎 Proposed fix for tool decorator
         if isinstance(name_or_fn, classmethod):
-            raise ValueError(
+            raise TypeError(
                 inspect.cleandoc(
                     """
                     To decorate a classmethod, first define the method and then call
                     tool() directly on the method instead of using it as a
                     decorator. See https://gofastmcp.com/patterns/decorating-methods
                     for examples and more information.
                     """
                 )
             )
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30d14a9 and 01943fd.

⛔ Files ignored due to path filters (7)
  • tests/server/providers/test_local_provider.py is excluded by none and included by none
  • tests/server/providers/test_local_provider_prompts.py is excluded by none and included by none
  • tests/server/providers/test_local_provider_resources.py is excluded by none and included by none
  • tests/server/providers/test_local_provider_tools.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_server.py is excluded by none and included by none
  • tests/server/test_server_interactions.py is excluded by none and included by none
📒 Files selected for processing (2)
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/providers/local_provider.py
🚧 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)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥ 3.10 with full type annotations
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/providers/local_provider.py
🧠 Learnings (1)
📓 Common learnings
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
🧬 Code graph analysis (1)
src/fastmcp/server/providers/local_provider.py (5)
src/fastmcp/resources/resource.py (1)
  • Resource (137-331)
src/fastmcp/resources/template.py (1)
  • ResourceTemplate (97-298)
src/fastmcp/server/providers/base.py (2)
  • Provider (56-394)
  • _notify (74-92)
src/fastmcp/server/tasks/config.py (2)
  • TaskConfig (25-104)
  • supports_tasks (69-75)
src/fastmcp/tools/tool_transform.py (2)
  • ToolTransformConfig (878-921)
  • apply_transformations_to_tools (924-943)
🪛 Ruff (0.14.10)
src/fastmcp/server/providers/local_provider.py

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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


471-480: Prefer TypeError exception for invalid type

(TRY004)


514-517: Avoid specifying long messages outside the exception class

(TRY003)


523-525: Avoid specifying long messages outside the exception class

(TRY003)


598-601: Avoid specifying long messages outside the exception class

(TRY003)


605-614: Prefer TypeError exception for invalid type

(TRY004)


662-665: Avoid specifying long messages outside the exception class

(TRY003)


753-762: Prefer TypeError exception for invalid type

(TRY004)


792-795: Avoid specifying long messages outside the exception class

(TRY003)


801-803: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (6)
src/fastmcp/server/providers/local_provider.py (6)

1-54: LGTM!

Module setup is clean with proper type annotations, conditional imports, and a helpful docstring example.


91-110: LGTM!

Clean initialization with well-documented on_duplicate parameter and properly typed storage dictionaries.


116-151: LGTM!

Consistent duplicate-handling pattern with proper notification via _notify(). The code correctly handles all four DuplicateBehavior modes.


300-338: LGTM!

Provider interface is correctly implemented. list_tools properly applies transformations, and get_tool correctly searches through transformed tools (necessary since transformations can rename tools).


344-362: LGTM!

Task filtering correctly uses task_config.supports_tasks() to identify components eligible for background execution.


699-816: LGTM!

The prompt decorator follows the same well-structured pattern as the tool decorator, correctly handling multiple calling conventions via overloads and partial application.

Comment on lines +265 to +294
def add_tool_transformation(
self, tool_name: str, transformation: ToolTransformConfig
) -> None:
"""Add a tool transformation.

Args:
tool_name: The name of the tool to transform.
transformation: The transformation configuration.
"""
self._tool_transformations[tool_name] = transformation

def get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None:
"""Get a tool transformation.

Args:
tool_name: The name of the tool.

Returns:
The transformation config, or None if not found.
"""
return self._tool_transformations.get(tool_name)

def remove_tool_transformation(self, tool_name: str) -> None:
"""Remove a tool transformation.

Args:
tool_name: The name of the tool.
"""
if tool_name in self._tool_transformations:
del self._tool_transformations[tool_name]
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

Missing _notify("tools") calls in transformation methods.

When a tool transformation is added or removed, the effective tool list changes (tools can be renamed, have descriptions modified, or be disabled). Clients should be notified to refresh their tool lists, similar to how add_tool and remove_tool call self._notify("tools").

🔎 Proposed fix
     def add_tool_transformation(
         self, tool_name: str, transformation: ToolTransformConfig
     ) -> None:
         """Add a tool transformation.

         Args:
             tool_name: The name of the tool to transform.
             transformation: The transformation configuration.
         """
         self._tool_transformations[tool_name] = transformation
+        self._notify("tools")

     def get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None:
         """Get a tool transformation.

         Args:
             tool_name: The name of the tool.

         Returns:
             The transformation config, or None if not found.
         """
         return self._tool_transformations.get(tool_name)

     def remove_tool_transformation(self, tool_name: str) -> None:
         """Remove a tool transformation.

         Args:
             tool_name: The name of the tool.
         """
         if tool_name in self._tool_transformations:
             del self._tool_transformations[tool_name]
+            self._notify("tools")

Comment on lines +661 to +665
else:
raise ValueError(
"Invalid resource or template definition due to a "
"mismatch between URI parameters and function parameters."
)
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 | 🟡 Minor

Unreachable dead code.

This else branch is logically unreachable. The conditions has_uri_params or has_func_params (line 627) and not has_uri_params and not has_func_params (line 644) are mutually exhaustive—every boolean combination is covered by one of these two branches.

🔎 Proposed fix
             if has_uri_params or has_func_params:
                 template = ResourceTemplate.from_function(
                     fn=fn,
                     uri_template=uri,
                     name=name,
                     title=title,
                     description=description,
                     icons=icons,
                     mime_type=mime_type,
                     tags=tags,
                     enabled=enabled,
                     annotations=annotations,
                     meta=meta,
                     task=supports_task,
                 )
                 self.add_template(template)
                 return template
-            elif not has_uri_params and not has_func_params:
+            else:
                 resource_obj = Resource.from_function(
                     fn=fn,
                     uri=uri,
                     name=name,
                     title=title,
                     description=description,
                     icons=icons,
                     mime_type=mime_type,
                     tags=tags,
                     enabled=enabled,
                     annotations=annotations,
                     meta=meta,
                     task=supports_task,
                 )
                 self.add_resource(resource_obj)
                 return resource_obj
-            else:
-                raise ValueError(
-                    "Invalid resource or template definition due to a "
-                    "mismatch between URI parameters and function parameters."
-                )
🧰 Tools
🪛 Ruff (0.14.10)

662-665: Avoid specifying long messages outside the exception class

(TRY003)

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Dec 23, 2025

Test Failure Analysis

Summary: The test test_nested_streamable_http_server_resolves_correctly is timing out during fixture teardown in CI when tests run in parallel.

Root Cause: This is a flaky test that intermittently fails during cleanup. The test itself passes, but the nested_server fixture's teardown hangs when trying to cancel the uvicorn server task. The fixture already has a 3-second timeout (await asyncio.wait_for(server_task, timeout=3.0)), but in CI's parallel test execution environment, the server cleanup doesn't complete within pytest's 5-second timeout limit.

Suggested Solution: This is a test infrastructure issue, not a code bug. The most reliable fix is to mark this specific test to run without parallel execution to avoid timing conflicts:

  1. tests/client/test_streamable_http.py:228 - Add a pytest marker to disable parallel execution for this test:
@pytest.mark.noload  # Prevent xdist from scheduling this test in parallel
async def test_nested_streamable_http_server_resolves_correctly(nested_server: str):
    """Test patch for https://github.com/modelcontextprotocol/python-sdk/pull/659"""
    async with Client(transport=StreamableHttpTransport(nested_server)) as client:
        result = await client.ping()
        assert result is True

Alternatively, increase the fixture's timeout to 5+ seconds and/or increase pytest's global timeout for this test, though this will slow down the test suite.

Detailed Analysis

Test Behavior

  • Passes when run in isolation: pytest tests/client/test_streamable_http.py::test_nested_streamable_http_server_resolves_correctly
  • Passes when run with file-level parallelism: pytest tests/client/test_streamable_http.py -n 2
  • Flaky in CI full test suite: Intermittently times out at teardown ❌

This pattern indicates a resource contention or timing issue that only manifests under heavy parallel load.

Why This Happens

The nested_server fixture:

  1. Starts a uvicorn server with nested Starlette mounts
  2. Yields the URL for the test
  3. Cancels the server task: server_task.cancel()
  4. Waits with timeout: await asyncio.wait_for(server_task, timeout=3.0)
  5. Catches CancelledError and TimeoutError

In CI with high parallel load (4 workers, 3144 tests), the cancellation sometimes doesn't propagate through uvicorn's shutdown sequence within 3 seconds. The wait_for times out, but pytest's own 5-second timeout fires before the exception can be caught, causing an ERROR at teardown.

Test File Changes

The PR shows this test was modified to add the timeout handling:

+    # Cleanup with timeout to prevent hanging
     server_task.cancel()
     try:
-        await server_task
-    except asyncio.CancelledError:
+        await asyncio.wait_for(server_task, timeout=3.0)
+    except (asyncio.CancelledError, asyncio.TimeoutError):
         pass

This was an attempt to fix the flakiness, but the 3-second fixture timeout combined with pytest's 5-second test timeout creates a narrow 2-second window that's not always sufficient in CI.

CI Environment

  • Python 3.10 and 3.13 on ubuntu-latest: Both fail
  • Parallel execution: --numprocesses auto --maxprocesses 4 --dist worksteal
  • Global timeout: 5 seconds (pytest-timeout)
  • Test result: 3142 passed, 1 xfailed, 1 xpassed, 1 warning, 1 error

Not Related to the Refactor

The test works fine locally on the PR branch and only fails intermittently in CI's parallel execution environment. The refactor didn't introduce any server lifecycle changes that would cause this - it's a pre-existing test infrastructure issue.

Related Files
  • tests/client/test_streamable_http.py:101-141 - nested_server fixture definition with the cleanup timeout
  • tests/client/test_streamable_http.py:228-232 - The test that flakes during teardown

Note: This analysis has been updated to reflect the latest test failure (commit 613f913, run #20472384545).

@jlowin
Copy link
Copy Markdown
Member Author

jlowin commented Dec 23, 2025

@coderabbitai ignore

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 23, 2025

✅ Actions performed

Reviews paused.

@jlowin jlowin force-pushed the local-provider-refactor branch from 3910dcf to 613f913 Compare December 23, 2025 21:51
- Port FastMCPError exception handling from managers to server execute methods
- Resolve uvicorn test cleanup conflicts (use graceful shutdown)
- Keep manager deletions (execution moved to server)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. 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