mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 04:24:17 +02:00
* Add test_custom_subclass_tasks.py * Refactor provider execution: delegate to middleware via wrapper components - Remove execution methods (call_tool, read_resource, etc.) from Provider base - Add FastMCPProvider* wrapper classes that delegate to child server middleware - Move task routing to Tool._run() using contextvars (_task_metadata, _tool_call_key) - Add convert_to_tool_result(result, output_schema) utility for Docket results - Add convert_to_prompt_result() utility for prompt task results - Pass namespaced key via add_to_docket(name=) for mounted tool lookup * Standardize add_to_docket() with fn_key/task_key parameters All components now use explicit fn_key (function lookup) and task_key (result storage) parameters instead of relying on implicit key handling. This fixes mounted component task execution where the MCP-visible key differs from the Docket-registered function name. * Add middleware chain tests for three-level mount hierarchy Tests verify middleware runs at parent, child, and grandchild levels for tools, resources, prompts, and resource templates. * WIP: Provider refactor - unified submit_to_docket, template _read() in progress Work in progress on refactoring execution to use component _read()/_run()/_render() methods. Template background tasks not yet working - needs fix for Docket key lookup. * Fix conversion functions to take full component for attribute access Pass Tool/Prompt/Resource/Template to conversion functions instead of individual attributes, ensuring access to serializer, output_schema, mime_type, etc. Also fixes mixed-content output schema validation. * Refactor: unified convert_result() methods and check_background_task helper - Add convert_result() instance methods to all component types (Tool, Prompt, Resource, ResourceTemplate) - Extract duplicated task routing logic into check_background_task() helper - Fix type annotations on FastMCPProviderResource.read() and FastMCPProviderPrompt.render() - Update protocol.py to use component.convert_result() uniformly * Update tests to use namespace= instead of deprecated prefix= parameter
139 lines
4.2 KiB
Python
139 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 collections.abc import Sequence
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import aiosqlite
|
|
from rich import print
|
|
|
|
from fastmcp import Client, FastMCP
|
|
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):
|
|
super().__init__()
|
|
self.db_path = db_path
|
|
|
|
async def list_tools(self) -> Sequence[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, 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())
|