feat: Provider abstraction for dynamic MCP components#2622
Conversation
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()])
```
WalkthroughAdds a Provider abstraction and runtime provider support to FastMCP, exporting a new Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (4 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🧰 Additional context used📓 Path-based instructions (1)**/*.py📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (1)📚 Learning: 2025-12-15T02:52:52.583ZApplied to files:
🪛 Ruff (0.14.8)src/fastmcp/providers.py69-69: Unused method argument: (ARG002) 104-104: Unused method argument: (ARG002) 124-124: Unused method argument: (ARG002) 189-189: Unused method argument: (ARG002) src/fastmcp/server/server.py1778-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)
🔇 Additional comments (13)
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. Comment |
There was a problem hiding this comment.
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/lintersThe updated docstring and exception handling keep the semantics clear:
PromptErroris passed through, and non-PromptErrorexceptions are wrapped with optional masking while still chaining the original exception viafrom 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 eThis 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 inConfigurableTool.run
aandbultimately come from user/DB-configured inputs (default_valuevia SQLite and arguments via JSON). If either ends up as a string (e.g.,"1.5"from the DB), operations likea + bwill raiseTypeErrorat runtime.You might want to defensively coerce to
float(orDecimalif 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 lintTwo small polish items here:
__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)
contextis intentionally unused in this example but is required by theProviderAPI. To satisfy linters like Ruff’sARG002while 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 laterasync 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 laterasync 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: Normalizedefault_valuefrom SQLite to match the annotatedfloat
default_valueis annotated asfloat, but the SQLite row may return a string,None, or another type depending on schema and seeding. Passingrow["default_value"] or 0through directly can lead to type mismatches and surprising arithmetic inConfigurableTool.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.0from the DB instead of relying onor 0.
118-137: Annotatemain’s return type for consistencyTo match the “full type annotations” guideline, it’s worth annotating
mainexplicitly:-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
⛔ Files ignored due to path filters (2)
AGENTS.mdis excluded by none and included by nonetests/server/test_providers.pyis 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 bareexceptstatements - 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.pysrc/fastmcp/providers.pysrc/fastmcp/tools/tool_manager.pysrc/fastmcp/resources/resource_manager.pysrc/fastmcp/__init__.pyexamples/providers/sqlite/setup_db.pysrc/fastmcp/server/server.pyexamples/providers/sqlite/server.pysrc/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
CallNextwhich 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:
- Re-raises
ValidationErrorandToolErrorwithout additional wrapping (they're already domain-specific)- Wraps generic exceptions in
ToolErrorwith appropriate masking- 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:
ResourceErrorexceptions are re-raised without additional logging (centralized at server level)- Generic exceptions are wrapped in
ResourceErrorwith conditional detail masking- Both concrete resource and template paths follow the same pattern
src/fastmcp/__init__.py (1)
17-17: LGTM!Clean addition of
Providerto 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 iteratinglist_*, with explicit encouragement to override for efficiencyThe static analysis warnings about unused
contextparameters (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 REPLACEensures 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
providersparameter 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_providermethod follows the established pattern ofadd_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 Exceptionblock logs but swallows errors fromlist_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_toolcorrectly re-raises exceptions after logging, which aligns with the stated PR semantics that providers "signal real errors by raising exceptions." This differs appropriately fromlist_toolswhere graceful degradation makes sense.
1758-1775: Well-structured unified error handling.The
_run_toolhelper correctly handles all error categories:
- Validation errors (both fastmcp and pydantic) are logged and re-raised unmodified
ToolErroris 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_contentand_render_promptfollow 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_contenthelper 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.
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
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/fastmcp/providers.py (1)
174-181: Consider removing redundant null check.Lines 177-179 check if
paramsisNoneafterget_resource_templatealready filtered templates bymatches(uri). Sinceget_resource_templateonly returns templates wherematches()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
⛔ Files ignored due to path filters (1)
tests/server/test_providers.pyis 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 bareexceptstatements - 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.pysrc/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_providermethod 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_toolhelper (and its counterparts_execute_resourceand_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.
- 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
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.The Provider interface has three methods per component type - list, get, and execute:
list_toolsget_toolcall_toollist_resourcesget_resourceread_resourcelist_resource_templatesget_resource_templateread_resource_templatelist_promptsget_promptrender_promptThis separation enables future work where
MountedProvidercan invoke a wrapped server's middleware chain incall_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.