Skip to content

feat: Provider abstraction for dynamic MCP components#2622

Merged
jlowin merged 3 commits intomainfrom
dynamic-components
Dec 17, 2025
Merged

feat: Provider abstraction for dynamic MCP components#2622
jlowin merged 3 commits intomainfrom
dynamic-components

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 15, 2025

MCP servers often need to provide components from external sources - databases, APIs, configuration systems. This PR introduces Provider, a first-class abstraction for dynamic component sources that separates listing, lookup, and execution.

from fastmcp import FastMCP, Provider

class DatabaseProvider(Provider):
    async def list_tools(self, context):
        rows = await self.db.fetch("SELECT * FROM tools")
        return [self._make_tool(row) for row in rows]

    async def get_tool(self, context, name):
        row = await self.db.fetchone("SELECT * FROM tools WHERE name = ?", name)
        return self._make_tool(row) if row else None

    async def call_tool(self, context, name, arguments):
        # Custom execution: add middleware, logging, etc.
        tool = await self.get_tool(context, name)
        return await tool.run(arguments)

mcp = FastMCP("Server", providers=[DatabaseProvider(db_url)])

The Provider interface has three methods per component type - list, get, and execute:

Component List Get Execute
Tools list_tools get_tool call_tool
Resources list_resources get_resource read_resource
Templates list_resource_templates get_resource_template read_resource_template
Prompts list_prompts get_prompt render_prompt

This separation enables future work where MountedProvider can invoke a wrapped server's middleware chain in call_tool, rather than just returning Tool objects that bypass middleware. Static components (registered via decorators) take precedence over providers for execution, while provider components appear first in list results for visibility.

Introduces a `Provider` base class that allows dynamic provision of
tools, resources, and prompts at runtime. Providers are queried after
static components, enabling database-backed tools, external integrations,
and other dynamic component sources.

```python
from fastmcp import FastMCP, Provider

class DatabaseProvider(Provider):
    async def list_tools(self, context):
        return await db.fetch_tools()

mcp = FastMCP("Server", providers=[DatabaseProvider()])
```
@marvin-context-protocol marvin-context-protocol Bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality. labels Dec 15, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 15, 2025

Walkthrough

Adds a Provider abstraction and runtime provider support to FastMCP, exporting a new Provider class. FastMCP gains a providers constructor parameter and an add_provider() method; listing and execution paths for tools, resources, templates, and prompts now consult registered providers while preserving static component execution precedence. Error-handling paths were consolidated (reducing internal logging and adjusting masking). Includes an example SQLite-based dynamic tool provider with setup and server scripts.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (4 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks required checklist items from the template. No checkboxes are marked for issue closure, development workflow, testing, documentation, self-review, or readiness confirmation. Complete the Contributors and Review Checklists by marking checkboxes, especially confirming issue #2597 closure and confirming self-review and readiness for review.
Linked Issues check ⚠️ Warning The PR implements a Provider abstraction for dynamic MCP components but the linked issue #2597 requests concurrent_startup parameter for Client to parallelize MCP server initialization. These are unrelated objectives. Verify the correct issue is linked or implement the concurrent_startup functionality described in issue #2597, or link the correct issue that this PR addresses.
Out of Scope Changes check ⚠️ Warning The PR introduces a Provider abstraction and dynamic tool loading from SQLite, but issue #2597 requires concurrent server startup in Client. The implemented functionality does not match the linked issue's requirements. Either implement issue #2597's concurrent_startup requirements or link the correct issue describing Provider abstraction and dynamic component support.
Docstring Coverage ⚠️ Warning Docstring coverage is 79.55% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Provider abstraction for dynamic MCP components' clearly and concisely summarizes the main change: introducing a Provider abstraction for dynamic components in FastMCP.
✨ 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

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88c5450 and 28a4307.

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

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥3.10 with full type annotations in all code
Avoid bare except statements - be specific with exception types in error handling
Follow existing patterns and maintain consistency; prioritize readable, understandable code over cleverness
Avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/providers.py
  • src/fastmcp/server/server.py
🧠 Learnings (1)
📚 Learning: 2025-12-15T02:52:52.583Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T02:52:52.583Z
Learning: Applies to **/*.py : Avoid bare `except` statements - be specific with exception types in error handling

Applied to files:

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

69-69: Unused method argument: context

(ARG002)


104-104: Unused method argument: context

(ARG002)


124-124: Unused method argument: context

(ARG002)


189-189: Unused method argument: context

(ARG002)

src/fastmcp/server/server.py

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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


1917-1919: Avoid specifying long messages outside the exception class

(TRY003)


1920-1922: Avoid specifying long messages outside the exception class

(TRY003)


1941-1943: Avoid specifying long messages outside the exception class

(TRY003)


1944-1946: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


2092-2092: 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 with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
🔇 Additional comments (13)
src/fastmcp/providers.py (2)

46-67: Well-designed provider abstraction with clear semantics.

The docstring clearly documents:

  • Provider semantics (None = continue searching, static components take precedence)
  • Error handling behavior (graceful degradation for list, propagation for execution)
  • Registration order precedence

This design enables flexible multi-provider composition.


76-86: The get_tool implementation is correct. Tools are looked up by name, which is the external identifier used throughout the protocol. The key property exists separately for internal bookkeeping (e.g., prefixing in nested server hierarchies) and defaults to name anyway, so no mismatch occurs.

Likely an incorrect or invalid review comment.

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

248-253: Good: Unified error masking configuration.

Storing _mask_error_details at initialization ensures consistent error handling behavior across all execution paths (local tools, provider tools, resources, prompts).


918-928: Clean provider registration API.

The method correctly appends to the providers list, preserving registration order for precedence. The docstring accurately documents that static components take precedence over providers.


1247-1264: List method exception handling aligns with documented Provider semantics.

The except Exception: with logger.exception() and continue is intentional graceful degradation, as documented in the Provider class docstring (lines 62-63): "list_* methods: Errors are logged and the provider returns empty."

This allows other providers and static components to still contribute when one provider fails.


1353-1370: Consistent provider integration pattern across list methods.

Resources, templates, and prompts follow the same pattern as tools: graceful degradation on errors, deduplication against static components, provider items listed first for visibility.


1756-1780: Unified error handling for provider tool execution.

The error handling now matches local tool execution:

  • ValidationError/PydanticValidationError: Never masked (client input issues)
  • ToolError: Pass through
  • Other exceptions: Wrapped in ToolError with masking based on _mask_error_details

This addresses the previous review concern about inconsistent error semantics between local and provider tools.


1783-1800: Good: Extracted unified error handling helper.

The _execute_tool helper encapsulates the common error handling pattern, ensuring local tools have the same error semantics as provider tools. This reduces code duplication and ensures consistency.


1899-1947: Consistent provider resource execution with unified error handling.

Both concrete resources and templates from providers have proper error handling:

  • ResourceError passes through
  • Other exceptions wrapped with optional detail masking

This matches the local resource execution path via _execute_resource.


1950-1963: LGTM!

The _execute_resource helper follows the same error handling pattern as _execute_tool, maintaining consistency across component types.


2056-2076: Consistent provider prompt execution with unified error handling.

Follows the same pattern as tools and resources: PromptError passes through, other exceptions wrapped with optional masking.


2079-2092: LGTM!

The _execute_prompt helper completes the unified error handling pattern across all component types.


1756-1766: No action needed. The assumption that middleware context always provides a valid fastmcp_context is confirmed—get_context() either returns a Context or raises RuntimeError, never None. The defensive check if ctx is not None is appropriate practice and will never encounter None in normal operation.


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

🧹 Nitpick comments (5)
src/fastmcp/prompts/prompt_manager.py (1)

110-123: Error wrapping behavior looks good; you can DRY the message for style/linters

The updated docstring and exception handling keep the semantics clear: PromptError is passed through, and non-PromptError exceptions are wrapped with optional masking while still chaining the original exception via from e. That’s a solid pattern.

To reduce duplication and satisfy Ruff’s TRY003 hint, you could factor out the base message:

-        try:
-            return await prompt._render(arguments)
-        except PromptError:
-            raise
-        except Exception as e:
-            if self.mask_error_details:
-                raise PromptError(f"Error rendering prompt {name!r}") from e
-            raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
+        try:
+            return await prompt._render(arguments)
+        except PromptError:
+            raise
+        except Exception as e:
+            base_message = f"Error rendering prompt {name!r}"
+            if self.mask_error_details:
+                raise PromptError(base_message) from e
+            raise PromptError(f"{base_message}: {e}") from e

This keeps behavior identical while cleaning up the string handling and addressing the linter suggestion.

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

36-63: Consider explicit numeric coercion in ConfigurableTool.run

a and b ultimately come from user/DB-configured inputs (default_value via SQLite and arguments via JSON). If either ends up as a string (e.g., "1.5" from the DB), operations like a + b will raise TypeError at runtime.

You might want to defensively coerce to float (or Decimal if you ever care about precision) at the boundary:

-        a = arguments.get("a", self.default_value)
-        b = arguments.get("b", self.default_value)
+        a = float(arguments.get("a", self.default_value))
+        b = float(arguments.get("b", self.default_value))

This keeps the example resilient even if the DB column type or seeding script changes later.


73-90: Tighten provider method signatures and silence unused-argument lint

Two small polish items here:

  1. __init__ is missing an explicit return annotation, and the path could be slightly more flexible:
  • def init(self, db_path: str):
  •    self.db_path = db_path
    
  • def init(self, db_path: str | Path) -> None:
  •    self.db_path = str(db_path)
    
    
    
  1. context is intentionally unused in this example but is required by the Provider API. To satisfy linters like Ruff’s ARG002 while making intent clear, you can mark it as intentionally unused:

  • async def list_tools(self, context: Context) -> list[Tool]:
  •    async with aiosqlite.connect(self.db_path) as db:
    
  • async def list_tools(self, context: Context) -> list[Tool]:
  •    _ = context  # context available if you need logging/session data later
    
  •    async with aiosqlite.connect(self.db_path) as db:
       ...
    
  • async def get_tool(self, context: Context, name: str) -> Tool | None:
  •    async with aiosqlite.connect(self.db_path) as db:
    
  • async def get_tool(self, context: Context, name: str) -> Tool | None:
  •    _ = context  # context available if you need logging/session data later
    
  •    async with aiosqlite.connect(self.db_path) as db:
       ...
    
    
    

This keeps the example aligned with the Provider contract and the “fully typed / lint-clean” guideline.


92-99: Normalize default_value from SQLite to match the annotated float

default_value is annotated as float, but the SQLite row may return a string, None, or another type depending on schema and seeding. Passing row["default_value"] or 0 through directly can lead to type mismatches and surprising arithmetic in ConfigurableTool.run.

Normalizing here makes the tool behavior predictable:

-        return ConfigurableTool(
-            name=row["name"],
-            description=row["description"],
-            parameters=json.loads(row["parameters_schema"]),
-            operation=row["operation"],
-            default_value=row["default_value"] or 0,
-        )
+        raw_default = row["default_value"]
+        default_value = float(raw_default) if raw_default is not None else 0.0
+        return ConfigurableTool(
+            name=row["name"],
+            description=row["description"],
+            parameters=json.loads(row["parameters_schema"]),
+            operation=row["operation"],
+            default_value=default_value,
+        )

This also preserves explicit 0/0.0 from the DB instead of relying on or 0.


118-137: Annotate main’s return type for consistency

To match the “full type annotations” guideline, it’s worth annotating main explicitly:

-async def main():
+async def main() -> None:

Everything else in this example is nicely typed; this keeps the entrypoint consistent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8abd84b and 3fcb01f.

⛔ Files ignored due to path filters (2)
  • AGENTS.md 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 (10)
  • examples/providers/sqlite/README.md (1 hunks)
  • examples/providers/sqlite/server.py (1 hunks)
  • examples/providers/sqlite/setup_db.py (1 hunks)
  • src/fastmcp/__init__.py (2 hunks)
  • src/fastmcp/prompts/prompt_manager.py (1 hunks)
  • src/fastmcp/providers.py (1 hunks)
  • src/fastmcp/resources/resource_manager.py (1 hunks)
  • src/fastmcp/server/middleware/__init__.py (1 hunks)
  • src/fastmcp/server/server.py (11 hunks)
  • src/fastmcp/tools/tool_manager.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥3.10 with full type annotations in all code
Avoid bare except statements - be specific with exception types in error handling
Follow existing patterns and maintain consistency; prioritize readable, understandable code over cleverness
Avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/prompts/prompt_manager.py
  • src/fastmcp/providers.py
  • src/fastmcp/tools/tool_manager.py
  • src/fastmcp/resources/resource_manager.py
  • src/fastmcp/__init__.py
  • examples/providers/sqlite/setup_db.py
  • src/fastmcp/server/server.py
  • examples/providers/sqlite/server.py
  • src/fastmcp/server/middleware/__init__.py
🧠 Learnings (2)
📓 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-15T02:52:52.572Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T02:52:52.572Z
Learning: Applies to tests/**/*.py : Pass FastMCP servers directly to clients for testing; only use HTTP transport when explicitly testing network features

Applied to files:

  • src/fastmcp/__init__.py
🧬 Code graph analysis (7)
src/fastmcp/prompts/prompt_manager.py (2)
src/fastmcp/prompts/prompt.py (1)
  • _render (207-230)
src/fastmcp/exceptions.py (1)
  • PromptError (22-23)
src/fastmcp/tools/tool_manager.py (1)
src/fastmcp/exceptions.py (2)
  • ValidationError (10-11)
  • ToolError (18-19)
src/fastmcp/resources/resource_manager.py (2)
src/fastmcp/resources/resource.py (2)
  • _read (230-249)
  • key (276-283)
src/fastmcp/exceptions.py (1)
  • ResourceError (14-15)
src/fastmcp/__init__.py (1)
src/fastmcp/providers.py (1)
  • Provider (45-117)
examples/providers/sqlite/setup_db.py (1)
examples/providers/sqlite/server.py (1)
  • run (39-63)
examples/providers/sqlite/server.py (2)
src/fastmcp/providers.py (3)
  • Provider (45-117)
  • list_tools (62-67)
  • get_tool (69-79)
src/fastmcp/tools/tool.py (2)
  • Tool (123-272)
  • ToolResult (74-120)
src/fastmcp/server/middleware/__init__.py (1)
src/fastmcp/server/middleware/middleware.py (1)
  • CallNext (42-43)
🪛 Ruff (0.14.8)
src/fastmcp/prompts/prompt_manager.py

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

(TRY003)


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

(TRY003)

src/fastmcp/providers.py

62-62: Unused method argument: context

(ARG002)


81-81: Unused method argument: context

(ARG002)


100-100: Unused method argument: context

(ARG002)

src/fastmcp/tools/tool_manager.py

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

(TRY003)


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

(TRY003)

src/fastmcp/resources/resource_manager.py

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

(TRY003)


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

(TRY003)


323-325: Avoid specifying long messages outside the exception class

(TRY003)


326-328: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/server.py

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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)

examples/providers/sqlite/server.py

76-76: Unused method argument: context

(ARG002)


83-83: Unused method argument: context

(ARG002)

⏰ 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 (16)
src/fastmcp/server/middleware/__init__.py (1)

1-11: LGTM!

The import now correctly includes CallNext which was already declared in __all__. This ensures the public API is properly exported.

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

153-170: LGTM!

The error handling refactoring correctly:

  1. Re-raises ValidationError and ToolError without additional wrapping (they're already domain-specific)
  2. Wraps generic exceptions in ToolError with appropriate masking
  3. Removes internal logging since it's now centralized at the server level

The docstring update accurately reflects the new responsibility split.

src/fastmcp/resources/resource_manager.py (1)

289-330: LGTM!

The error handling changes are consistent with the tool_manager.py refactoring:

  • ResourceError exceptions are re-raised without additional logging (centralized at server level)
  • Generic exceptions are wrapped in ResourceError with conditional detail masking
  • Both concrete resource and template paths follow the same pattern
src/fastmcp/__init__.py (1)

17-17: LGTM!

Clean addition of Provider to the public API, following the established import pattern in this module.

Also applies to: 35-35

src/fastmcp/providers.py (1)

45-117: Well-designed base class with clear extension points.

The Provider abstraction is well-structured:

  • Default implementations allow selective overriding
  • Semantics are clearly documented (None vs exception)
  • get_* methods default to iterating list_*, with explicit encouragement to override for efficiency

The static analysis warnings about unused context parameters (ARG002) are false positives - these parameters are intentionally provided for subclass use.

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

19-102: LGTM!

The database setup is well-implemented:

  • Uses async/await properly with aiosqlite
  • INSERT OR REPLACE ensures idempotent re-runs
  • Schema matches the ConfigurableTool fields expected by server.py
  • Proper commit and resource cleanup via context manager
src/fastmcp/server/server.py (10)

192-192: LGTM!

The providers parameter follows the established pattern of other sequence parameters (e.g., middleware, tools), and proper initialization to an empty list when None is provided.

Also applies to: 231-231


918-928: LGTM!

The add_provider method follows the established pattern of add_middleware, with clear documentation of the registration order semantics.


1247-1264: Provider integration looks good, but consider error handling strategy.

The integration correctly:

  • Queries providers after static components
  • Deduplicates by checking existing keys
  • Places provider tools first for visibility while noting static tools win for execution

One consideration: the except Exception block logs but swallows errors from list_tools. This is reasonable for listing (graceful degradation), but verify this aligns with the PR's stated semantics that "providers signal real errors by raising exceptions."


1742-1755: LGTM!

Provider lookup in _call_tool correctly re-raises exceptions after logging, which aligns with the stated PR semantics that providers "signal real errors by raising exceptions." This differs appropriately from list_tools where graceful degradation makes sense.


1758-1775: Well-structured unified error handling.

The _run_tool helper correctly handles all error categories:

  • Validation errors (both fastmcp and pydantic) are logged and re-raised unmodified
  • ToolError is logged and re-raised
  • Generic exceptions are wrapped with conditional detail masking

This centralizes the error handling that was previously duplicated in ToolManager.call_tool.


1777-1805: Consistent error handling helpers.

Both _read_resource_content and _render_prompt follow the same pattern as _run_tool, ensuring consistent error handling across all component types.


1904-1918: LGTM!

Provider resource lookup follows the same pattern as tool lookup - errors are logged and re-raised. The integration uses the new _read_resource_content helper for consistent error handling.


2012-2025: LGTM!

Provider prompt lookup completes the consistent pattern across all three component types. Error handling matches tools and resources.


1353-1370: Consistent with _list_tools pattern.

Resource provider listing follows the same graceful degradation pattern as tool listing - errors are logged but don't prevent other providers from being queried.


1557-1574: Consistent with _list_tools and _list_resources patterns.

Prompt provider listing completes the consistent pattern across all three component types for list operations.

Comment thread examples/providers/sqlite/README.md
Comment thread examples/providers/sqlite/README.md
Comment thread examples/providers/sqlite/setup_db.py
@jlowin jlowin added the DON'T MERGE PR is not ready for merging. Used by authors to prevent premature merging. label Dec 15, 2025
Enable providers to customize how components are executed, not just listed.
This unlocks MountedProvider (future) invoking wrapped server middleware.

- call_tool, read_resource, read_resource_template, render_prompt
- Server uses provider execution methods after filter checks
- Default implementations delegate to component methods
@jlowin jlowin changed the title feat: add Provider abstraction for dynamic components feat: Provider abstraction for dynamic MCP components Dec 16, 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: 3

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

174-181: Consider removing redundant null check.

Lines 177-179 check if params is None after get_resource_template already filtered templates by matches(uri). Since get_resource_template only returns templates where matches() returns non-None (line 140), this additional check appears redundant.

Apply this diff to simplify:

     async def read_resource_template(
         self, context: Context, uri: str
     ) -> ResourceContent | None:
         """Read a resource via a matching template.
 
         Default implementation finds a matching template, creates a resource
         from it, and reads the content.
         Override for custom read logic (e.g., middleware, caching).
 
         Returns:
             The ResourceContent if a matching template is found and read,
             or None if no template matches.
         """
         template = await self.get_resource_template(context, uri)
         if template is None:
             return None
         params = template.matches(uri)
-        if params is None:
-            return None
         resource = await template.create_resource(uri, params)
         return await resource._read()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fcb01f and 88c5450.

⛔ Files ignored due to path filters (1)
  • tests/server/test_providers.py is excluded by none and included by none
📒 Files selected for processing (2)
  • src/fastmcp/providers.py (1 hunks)
  • src/fastmcp/server/server.py (12 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥3.10 with full type annotations in all code
Avoid bare except statements - be specific with exception types in error handling
Follow existing patterns and maintain consistency; prioritize readable, understandable code over cleverness
Avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/providers.py
  • src/fastmcp/server/server.py
🧬 Code graph analysis (1)
src/fastmcp/server/server.py (2)
src/fastmcp/providers.py (12)
  • Provider (46-216)
  • list_tools (63-68)
  • list_resources (98-103)
  • list_resource_templates (117-124)
  • list_prompts (183-188)
  • get_tool (70-80)
  • call_tool (82-96)
  • get_resource (105-115)
  • read_resource (144-159)
  • get_resource_template (126-142)
  • get_prompt (190-200)
  • render_prompt (202-216)
src/fastmcp/resources/resource_manager.py (2)
  • get_resource (244-287)
  • read_resource (289-330)
🪛 Ruff (0.14.8)
src/fastmcp/providers.py

63-63: Unused method argument: context

(ARG002)


98-98: Unused method argument: context

(ARG002)


118-118: Unused method argument: context

(ARG002)


183-183: Unused method argument: context

(ARG002)

src/fastmcp/server/server.py

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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


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

(TRY003)


2060-2060: 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.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (2)
src/fastmcp/server/server.py (2)

918-928: LGTM!

The add_provider method is well-documented with clear semantics about precedence and ordering. The implementation correctly appends providers to maintain registration order.


1774-1792: Well-designed unified error handling for local components.

The _execute_tool helper (and its counterparts _execute_resource and _execute_prompt) provide consistent error handling with appropriate semantics:

  • ValidationError is never masked (indicates client input issues)
  • Specific error types (ToolError) are propagated as-is
  • Generic exceptions are wrapped and conditionally masked based on settings

This provides good separation of concerns and consistent error semantics for locally-registered components.

Comment thread src/fastmcp/server/server.py
Comment thread src/fastmcp/server/server.py
Comment thread src/fastmcp/server/server.py
- Document error semantics: list_* gracefully degrades, execution propagates
- Wrap provider call_tool/read_resource/render_prompt with masking logic
- ToolError/ResourceError/PromptError pass through, others wrapped
@jlowin jlowin merged commit 0e5a8fe into main Dec 17, 2025
10 of 11 checks passed
@jlowin jlowin deleted the dynamic-components branch December 17, 2025 03:05
@jlowin jlowin removed the DON'T MERGE PR is not ready for merging. Used by authors to prevent premature merging. label Dec 18, 2025
@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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