mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
* Simplify Provider interface and consolidate docket registration - Remove get_http_routes from Provider (unused) - Remove ProviderLifespanConfig, _base_lifespan, _register_tasks - Remove supports_tasks flag from Provider.__init__ - Consolidate all docket registration in server._docket_lifespan() - Simplify lifespan() to take no parameters - Move MountedProvider to separate module * Fix control flow in ComponentService resource methods * Move providers to server/providers * Ensure MountedProvider get_* methods go through middleware * Fix get_resource to only return concrete resources Reverts template-checking in get_resource that broke task execution. Tasks need access to the original template, not instantiated resources. * Move prefix utilities into mounted.py, deprecate import_server - Add resource prefix functions (add/remove/has_resource_prefix) to mounted.py - Deprecate import_server with warning to use mount() instead - Add tool_names uniqueness validation in MountedProvider * Fix provider iteration order and remove dead _is_mounted flag - Remove unused _is_mounted flag (MountedProvider.lifespan() calls _lifespan not _lifespan_manager, so the flag was never checked) - Fix provider iteration: change reversed() to forward order in execution methods (_call_tool, _read_resource_middleware, _get_prompt_content_middleware) to match documented "first non-None wins" semantics - Fix ComponentService to handle prefix-less mounted servers using _strip_tool_prefix()/_strip_resource_prefix() methods - Update conflict resolution tests to expect first-registered provider wins - Add regression tests for Docket behavior and prefix-less ComponentService * Add TaskComponents type and exception handling for provider task registration - Create TaskComponents dataclass with FunctionTool/FunctionResource/etc. types for proper typing of get_tasks() return value - Add try/except wrapper around provider.get_tasks() in _docket_lifespan for consistent error handling (warn + continue or raise based on settings) - Remove type: ignore comments from server.py task registration loop
138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
# /// script
|
|
# dependencies = ["aiosqlite", "fastmcp"]
|
|
# ///
|
|
"""
|
|
MCP server with database-configured tools.
|
|
|
|
Tools are loaded from tools.db on each request, so you can add/modify/disable
|
|
tools in the database without restarting the server.
|
|
|
|
Run with: uv run fastmcp run examples/providers/sqlite/server.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import aiosqlite
|
|
from rich import print
|
|
|
|
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"
|
|
|
|
|
|
class ConfigurableTool(Tool):
|
|
"""A tool that performs a configured arithmetic operation.
|
|
|
|
This demonstrates the pattern: Tool subclass = schema + execution in one place.
|
|
"""
|
|
|
|
operation: str # "add", "multiply", "subtract", "divide"
|
|
default_value: float = 0
|
|
|
|
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
a = arguments.get("a", self.default_value)
|
|
b = arguments.get("b", self.default_value)
|
|
|
|
if self.operation == "add":
|
|
result = a + b
|
|
elif self.operation == "multiply":
|
|
result = a * b
|
|
elif self.operation == "subtract":
|
|
result = a - b
|
|
elif self.operation == "divide":
|
|
if b == 0:
|
|
return ToolResult(
|
|
structured_content={
|
|
"error": "Division by zero",
|
|
"operation": self.operation,
|
|
}
|
|
)
|
|
result = a / b
|
|
else:
|
|
result = a + b
|
|
|
|
return ToolResult(
|
|
structured_content={"result": result, "operation": self.operation}
|
|
)
|
|
|
|
|
|
class SQLiteToolProvider(Provider):
|
|
"""Queries SQLite for tool configurations.
|
|
|
|
Called on every list_tools/get_tool request, so database changes
|
|
are reflected immediately without server restart.
|
|
"""
|
|
|
|
def __init__(self, db_path: str):
|
|
self.db_path = db_path
|
|
|
|
async def list_tools(self, context: Context) -> list[Tool]:
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
async with db.execute("SELECT * FROM tools WHERE enabled = 1") as cursor:
|
|
rows = await cursor.fetchall()
|
|
return [self._make_tool(row) for row in rows]
|
|
|
|
async def get_tool(self, context: Context, name: str) -> Tool | None:
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
async with db.execute(
|
|
"SELECT * FROM tools WHERE name = ? AND enabled = 1", (name,)
|
|
) as cursor:
|
|
row = await cursor.fetchone()
|
|
return self._make_tool(row) if row else None
|
|
|
|
def _make_tool(self, row: aiosqlite.Row) -> ConfigurableTool:
|
|
return ConfigurableTool(
|
|
name=row["name"],
|
|
description=row["description"],
|
|
parameters=json.loads(row["parameters_schema"]),
|
|
operation=row["operation"],
|
|
default_value=row["default_value"] or 0,
|
|
)
|
|
|
|
|
|
mcp = FastMCP("DynamicToolsServer")
|
|
|
|
provider = SQLiteToolProvider(db_path=str(DB_PATH))
|
|
mcp.add_provider(provider)
|
|
|
|
|
|
@mcp.tool
|
|
def server_info() -> dict[str, str]:
|
|
"""Get information about this server (static tool)."""
|
|
return {
|
|
"name": "DynamicToolsServer",
|
|
"description": "A server with database-configured tools",
|
|
"database": str(DB_PATH),
|
|
}
|
|
|
|
|
|
async def main():
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
print(f"[bold]Available tools ({len(tools)}):[/bold]")
|
|
for tool in tools:
|
|
print(f" • {tool.name}: {tool.description}")
|
|
|
|
print()
|
|
print("[bold]Calling add_numbers(10, 5):[/bold]")
|
|
result = await client.call_tool("add_numbers", {"a": 10, "b": 5})
|
|
print(f" Result: {result.structured_content}")
|
|
|
|
print()
|
|
print("[bold]Calling multiply_numbers(7, 6):[/bold]")
|
|
result = await client.call_tool("multiply_numbers", {"a": 7, "b": 6})
|
|
print(f" Result: {result.structured_content}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|