Skip to content
Merged
8 changes: 4 additions & 4 deletions examples/providers/sqlite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ This example demonstrates serving MCP tools from a database. Tools can be added,

```bash
# Reset the database (optional - tools.db is pre-seeded)
uv run examples/dynamic_tools_sqlite/setup_db.py
uv run examples/providers/sqlite/setup_db.py

# Run the server
uv run fastmcp run examples/dynamic_tools_sqlite/server.py
uv run fastmcp run examples/providers/sqlite/server.py
```

## How It Works
Expand Down Expand Up @@ -50,10 +50,10 @@ While the server is running, you can modify tools in the database:

```bash
# Add a new tool
sqlite3 examples/dynamic_tools_sqlite/tools.db "INSERT INTO tools VALUES ('subtract_numbers', 'Subtract two numbers', '{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"number\"},\"b\":{\"type\":\"number\"}},\"required\":[\"a\",\"b\"]}', 'subtract', 0, 1)"
sqlite3 examples/providers/sqlite/tools.db "INSERT INTO tools VALUES ('subtract_numbers', 'Subtract two numbers', '{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"number\"},\"b\":{\"type\":\"number\"}},\"required\":[\"a\",\"b\"]}', 'subtract', 0, 1)"

# Disable a tool
sqlite3 examples/dynamic_tools_sqlite/tools.db "UPDATE tools SET enabled = 0 WHERE name = 'divide_numbers'"
sqlite3 examples/providers/sqlite/tools.db "UPDATE tools SET enabled = 0 WHERE name = 'divide_numbers'"
```

The next `list_tools` or `call_tool` request will reflect these changes.
3 changes: 2 additions & 1 deletion examples/providers/sqlite/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
import aiosqlite
from rich import print

from fastmcp import Client, FastMCP, Provider
from fastmcp import Client, FastMCP
from fastmcp.server.context import Context
from fastmcp.server.providers import Provider
from fastmcp.tools.tool import Tool, ToolResult

DB_PATH = Path(__file__).parent / "tools.db"
Expand Down
2 changes: 1 addition & 1 deletion examples/providers/sqlite/setup_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""
Creates and seeds the tools database.

Run with: uv run examples/dynamic_tools_sqlite/setup_db.py
Run with: uv run examples/providers/sqlite/setup_db.py
"""

import asyncio
Expand Down
2 changes: 0 additions & 2 deletions src/fastmcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

from fastmcp.server.server import FastMCP
from fastmcp.server.context import Context
from fastmcp.providers import Provider
import fastmcp.server

from fastmcp.client import Client
Expand All @@ -32,6 +31,5 @@
"Client",
"Context",
"FastMCP",
"Provider",
"settings",
]
99 changes: 44 additions & 55 deletions src/fastmcp/contrib/component_manager/component_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.server import FastMCP, has_resource_prefix, remove_resource_prefix
from fastmcp.server.providers import MountedProvider
from fastmcp.server.server import FastMCP
from fastmcp.tools.tool import Tool
from fastmcp.utilities.logging import get_logger

Expand Down Expand Up @@ -40,16 +41,14 @@ async def _enable_tool(self, key: str) -> Tool:
tool.enable()
return tool

# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._server._mounted_servers):
if mounted.prefix:
if key.startswith(f"{mounted.prefix}_"):
tool_key = key.removeprefix(f"{mounted.prefix}_")
mounted_service = ComponentService(mounted.server)
tool = await mounted_service._enable_tool(tool_key)
# 2. Check mounted servers via MountedProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
tool = await mounted_service._enable_tool(unprefixed)
return tool
else:
continue
raise NotFoundError(f"Unknown tool: {key}")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async def _disable_tool(self, key: str) -> Tool:
Expand All @@ -69,16 +68,14 @@ async def _disable_tool(self, key: str) -> Tool:
tool.disable()
return tool

# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._server._mounted_servers):
if mounted.prefix:
if key.startswith(f"{mounted.prefix}_"):
tool_key = key.removeprefix(f"{mounted.prefix}_")
mounted_service = ComponentService(mounted.server)
tool = await mounted_service._disable_tool(tool_key)
# 2. Check mounted servers via MountedProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
tool = await mounted_service._disable_tool(unprefixed)
return tool
else:
continue
raise NotFoundError(f"Unknown tool: {key}")

async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
Expand All @@ -102,18 +99,16 @@ async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
template.enable()
return template

# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._server._mounted_servers):
if mounted.prefix:
if has_resource_prefix(key, mounted.prefix):
key = remove_resource_prefix(key, mounted.prefix)
mounted_service = ComponentService(mounted.server)
# 2. Check mounted servers via MountedProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_resource_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._enable_resource(key)
) = await mounted_service._enable_resource(unprefixed)
return mounted_resource
else:
continue
raise NotFoundError(f"Unknown resource: {key}")

async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
Expand All @@ -137,18 +132,16 @@ async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
template.disable()
return template

# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._server._mounted_servers):
if mounted.prefix:
if has_resource_prefix(key, mounted.prefix):
key = remove_resource_prefix(key, mounted.prefix)
mounted_service = ComponentService(mounted.server)
# 2. Check mounted servers via MountedProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_resource_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._disable_resource(key)
) = await mounted_service._disable_resource(unprefixed)
return mounted_resource
else:
continue
raise NotFoundError(f"Unknown resource: {key}")

async def _enable_prompt(self, key: str) -> Prompt:
Expand All @@ -168,16 +161,14 @@ async def _enable_prompt(self, key: str) -> Prompt:
prompt.enable()
return prompt

# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._server._mounted_servers):
if mounted.prefix:
if key.startswith(f"{mounted.prefix}_"):
prompt_key = key.removeprefix(f"{mounted.prefix}_")
mounted_service = ComponentService(mounted.server)
prompt = await mounted_service._enable_prompt(prompt_key)
# 2. Check mounted servers via MountedProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
prompt = await mounted_service._enable_prompt(unprefixed)
return prompt
else:
continue
raise NotFoundError(f"Unknown prompt: {key}")

async def _disable_prompt(self, key: str) -> Prompt:
Expand All @@ -196,14 +187,12 @@ async def _disable_prompt(self, key: str) -> Prompt:
prompt.disable()
return prompt

# 2. Check mounted servers using the filtered protocol path.
for mounted in reversed(self._server._mounted_servers):
if mounted.prefix:
if key.startswith(f"{mounted.prefix}_"):
prompt_key = key.removeprefix(f"{mounted.prefix}_")
mounted_service = ComponentService(mounted.server)
prompt = await mounted_service._disable_prompt(prompt_key)
# 2. Check mounted servers via MountedProvider
for provider in self._server._providers:
if isinstance(provider, MountedProvider):
unprefixed = provider._strip_tool_prefix(key)
if unprefixed is not None:
mounted_service = ComponentService(provider.server)
prompt = await mounted_service._disable_prompt(unprefixed)
return prompt
else:
continue
raise NotFoundError(f"Unknown prompt: {key}")
36 changes: 36 additions & 0 deletions src/fastmcp/server/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Providers for dynamic MCP components.

This module provides the `Provider` abstraction for providing tools,
resources, and prompts dynamically at runtime.

Example:
```python
from fastmcp import FastMCP
from fastmcp.server.providers import Provider
from fastmcp.tools import Tool

class DatabaseProvider(Provider):
def __init__(self, db_url: str):
self.db = Database(db_url)

async def list_tools(self) -> list[Tool]:
rows = await self.db.fetch("SELECT * FROM tools")
return [self._make_tool(row) for row in rows]

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

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

from fastmcp.server.providers.base import Components, Provider, TaskComponents
from fastmcp.server.providers.mounted import MountedProvider

__all__ = [
"Components",
"MountedProvider",
"Provider",
"TaskComponents",
]
Loading