├── .gitattributes ├── .gitignore ├── README.md ├── fastmcp-documentation.txt ├── mcp-documentation.txt ├── requirements.txt └── sqlite_explorer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/hannesrudolph-sqlite-explorer-fastmcp-mcp-server-badge.png)](https://mseep.ai/app/hannesrudolph-sqlite-explorer-fastmcp-mcp-server) 2 | 3 | # SQLite Explorer MCP Server 4 | 5 | An MCP server that provides safe, read-only access to SQLite databases through Model Context Protocol (MCP). This server is built with the FastMCP framework, which enables LLMs to explore and query SQLite databases with built-in safety features and query validation. 6 | 7 | ## 📋 System Requirements 8 | 9 | - Python 3.6+ 10 | - SQLite database file (path specified via environment variable) 11 | 12 | ## 📦 Dependencies 13 | 14 | Install all required dependencies: 15 | 16 | ```bash 17 | # Using pip 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | ### Required Packages 22 | - **fastmcp**: Framework for building Model Context Protocol servers 23 | 24 | All dependencies are specified in `requirements.txt` for easy installation. 25 | 26 | ## 📑 Table of Contents 27 | - [System Requirements](#-system-requirements) 28 | - [Dependencies](#-dependencies) 29 | - [MCP Tools](#%EF%B8%8F-mcp-tools) 30 | - [Getting Started](#-getting-started) 31 | - [Installation Options](#-installation-options) 32 | - [Claude Desktop](#option-1-install-for-claude-desktop) 33 | - [Cline VSCode Plugin](#option-2-install-for-cline-vscode-plugin) 34 | - [Safety Features](#-safety-features) 35 | - [Development Documentation](#-development-documentation) 36 | - [Environment Variables](#%EF%B8%8F-environment-variables) 37 | 38 | ## 🛠️ MCP Tools 39 | 40 | The server exposes the following tools to LLMs: 41 | 42 | ### read_query 43 | Execute a SELECT query on the database with built-in safety validations. Features: 44 | - Query validation and sanitization 45 | - Parameter binding support 46 | - Row limit enforcement 47 | - Results formatted as dictionaries 48 | 49 | ### list_tables 50 | List all available tables in the database with their names. 51 | 52 | ### describe_table 53 | Get detailed schema information for a specific table, including: 54 | - Column names and types 55 | - NULL constraints 56 | - Default values 57 | - Primary key information 58 | 59 | ## 🚀 Getting Started 60 | 61 | Clone the repository: 62 | 63 | ```bash 64 | git clone https://github.com/hannesrudolph/sqlite-explorer-fastmcp-mcp-server.git 65 | cd sqlite-explorer-fastmcp-mcp-server 66 | ``` 67 | 68 | ## 📦 Installation Options 69 | 70 | You can install this MCP server in either Claude Desktop or the Cline VSCode plugin. Choose the option that best suits your needs. 71 | 72 | ### Option 1: Install for Claude Desktop 73 | 74 | Install using FastMCP: 75 | 76 | ```bash 77 | fastmcp install sqlite_explorer.py --name "SQLite Explorer" -e SQLITE_DB_PATH=/path/to/db 78 | ``` 79 | 80 | Replace `/path/to/db` with the path to your SQLite database file. 81 | 82 | ### Option 2: Install for Cline VSCode Plugin 83 | 84 | To use this server with the [Cline VSCode plugin](http://cline.bot): 85 | 86 | 1. In VSCode, click the server icon (☰) in the Cline plugin sidebar 87 | 2. Click the "Edit MCP Settings" button (✎) 88 | 3. Add the following configuration to the settings file: 89 | 90 | ```json 91 | { 92 | "sqlite-explorer": { 93 | "command": "uv", 94 | "args": [ 95 | "run", 96 | "--with", 97 | "fastmcp", 98 | "--with", 99 | "uvicorn", 100 | "fastmcp", 101 | "run", 102 | "/path/to/repo/sqlite_explorer.py" 103 | ], 104 | "env": { 105 | "SQLITE_DB_PATH": "/path/to/your/database.db" 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | Replace: 112 | - `/path/to/repo` with the full path to where you cloned this repository (e.g., `/Users/username/Projects/sqlite-explorer-fastmcp-mcp-server`) 113 | - `/path/to/your/database.db` with the full path to your SQLite database file 114 | 115 | ## 🔒 Safety Features 116 | 117 | - Read-only access to SQLite databases 118 | - Query validation and sanitization 119 | - Parameter binding for safe query execution 120 | - Row limit enforcement 121 | - Progress output suppression for clean JSON responses 122 | 123 | ## 📚 Development Documentation 124 | 125 | The repository includes documentation files for development: 126 | 127 | - `mcp-documentation.txt`: Contains comprehensive documentation about the MCP server implementation and FastMCP framework usage. 128 | 129 | This documentation serves as context when developing features and can be used with LLMs to assist in development. 130 | 131 | ## ⚙️ Environment Variables 132 | 133 | The following environment variables must be set: 134 | 135 | - `SQLITE_DB_PATH`: Full path to the SQLite database file you want to explore 136 | -------------------------------------------------------------------------------- /fastmcp-documentation.txt: -------------------------------------------------------------------------------- 1 | Directory structure: 2 | └── jlowin-fastmcp 3 | ├── pyproject.toml 4 | ├── docs 5 | │ └── assets 6 | ├── README.md 7 | ├── examples 8 | │ ├── simple_echo.py 9 | │ ├── memory.py 10 | │ ├── readme-quickstart.py 11 | │ ├── text_me.py 12 | │ ├── screenshot.py 13 | │ ├── echo.py 14 | │ ├── desktop.py 15 | │ └── complex_inputs.py 16 | ├── Windows_Notes.md 17 | └── src 18 | └── fastmcp 19 | ├── server.py 20 | ├── tools 21 | │ ├── tool_manager.py 22 | │ ├── __init__.py 23 | │ └── base.py 24 | ├── resources 25 | │ ├── resource_manager.py 26 | │ ├── __init__.py 27 | │ ├── types.py 28 | │ ├── templates.py 29 | │ └── base.py 30 | ├── __init__.py 31 | ├── cli 32 | │ ├── claude.py 33 | │ ├── __init__.py 34 | │ └── cli.py 35 | ├── utilities 36 | │ ├── logging.py 37 | │ ├── func_metadata.py 38 | │ ├── __init__.py 39 | │ └── types.py 40 | ├── prompts 41 | │ ├── prompt_manager.py 42 | │ ├── __init__.py 43 | │ ├── manager.py 44 | │ └── base.py 45 | ├── py.typed 46 | └── exceptions.py 47 | 48 | ================================================ 49 | File: /pyproject.toml 50 | ================================================ 51 | [project] 52 | name = "fastmcp" 53 | dynamic = ["version"] 54 | description = "A more ergonomic interface for MCP servers" 55 | authors = [{ name = "Jeremiah Lowin" }] 56 | dependencies = [ 57 | "httpx>=0.26.0", 58 | "mcp>=1.0.0,<2.0.0", 59 | "pydantic-settings>=2.6.1", 60 | "pydantic>=2.5.3,<3.0.0", 61 | "typer>=0.9.0", 62 | "python-dotenv>=1.0.1", 63 | ] 64 | requires-python = ">=3.10" 65 | readme = "README.md" 66 | license = { text = "MIT" } 67 | 68 | [project.scripts] 69 | fastmcp = "fastmcp.cli:app" 70 | 71 | [build-system] 72 | requires = ["hatchling>=1.21.0", "hatch-vcs>=0.4.0"] 73 | build-backend = "hatchling.build" 74 | 75 | [project.optional-dependencies] 76 | tests = [ 77 | "pre-commit", 78 | "pyright>=1.1.389", 79 | "pytest>=8.3.3", 80 | "pytest-asyncio>=0.23.5", 81 | "pytest-flakefinder", 82 | "pytest-xdist>=3.6.1", 83 | "ruff", 84 | ] 85 | dev = ["fastmcp[tests]", "copychat>=0.5.2", "ipython>=8.12.3", "pdbpp>=0.10.3"] 86 | 87 | [tool.pytest.ini_options] 88 | asyncio_mode = "auto" 89 | asyncio_default_fixture_loop_scope = "session" 90 | 91 | [tool.hatch.version] 92 | source = "vcs" 93 | 94 | [tool.pyright] 95 | include = ["src", "tests"] 96 | exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"] 97 | pythonVersion = "3.10" 98 | pythonPlatform = "Darwin" 99 | typeCheckingMode = "basic" 100 | reportMissingImports = true 101 | reportMissingTypeStubs = false 102 | useLibraryCodeForTypes = true 103 | venvPath = "." 104 | venv = ".venv" 105 | 106 | 107 | ================================================ 108 | File: /README.md 109 | ================================================ 110 | 111 | # FastMCP 🚀 112 | 113 |
114 | 115 | The fast, Pythonic way to build MCP servers. 116 | 117 | [![PyPI - Version](https://img.shields.io/pypi/v/fastmcp.svg)](https://pypi.org/project/fastmcp) 118 | [![Tests](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml) 119 | [![License](https://img.shields.io/github/license/jlowin/fastmcp.svg)](https://github.com/jlowin/fastmcp/blob/main/LICENSE) 120 | 121 | 122 |
123 | 124 | [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers are a new, standardized way to provide context and tools to your LLMs, and FastMCP makes building MCP servers simple and intuitive. Create tools, expose resources, and define prompts with clean, Pythonic code: 125 | 126 | ```python 127 | # demo.py 128 | 129 | from fastmcp import FastMCP 130 | 131 | 132 | mcp = FastMCP("Demo 🚀") 133 | 134 | 135 | @mcp.tool() 136 | def add(a: int, b: int) -> int: 137 | """Add two numbers""" 138 | return a + b 139 | ``` 140 | 141 | That's it! Give Claude access to the server by running: 142 | 143 | ```bash 144 | fastmcp install demo.py 145 | ``` 146 | 147 | FastMCP handles all the complex protocol details and server management, so you can focus on building great tools. It's designed to be high-level and Pythonic - in most cases, decorating a function is all you need. 148 | 149 | 150 | ### Key features: 151 | * **Fast**: High-level interface means less code and faster development 152 | * **Simple**: Build MCP servers with minimal boilerplate 153 | * **Pythonic**: Feels natural to Python developers 154 | * **Complete***: FastMCP aims to provide a full implementation of the core MCP specification 155 | 156 | (\*emphasis on *aims*) 157 | 158 | 🚨 🚧 🏗️ *FastMCP is under active development, as is the MCP specification itself. Core features are working but some advanced capabilities are still in progress.* 159 | 160 | 161 | 162 | ## Table of Contents 163 | 164 | - [Installation](#installation) 165 | - [Quickstart](#quickstart) 166 | - [What is MCP?](#what-is-mcp) 167 | - [Core Concepts](#core-concepts) 168 | - [Server](#server) 169 | - [Resources](#resources) 170 | - [Tools](#tools) 171 | - [Prompts](#prompts) 172 | - [Images](#images) 173 | - [Context](#context) 174 | - [Running Your Server](#running-your-server) 175 | - [Development Mode (Recommended for Building \& Testing)](#development-mode-recommended-for-building--testing) 176 | - [Claude Desktop Integration (For Regular Use)](#claude-desktop-integration-for-regular-use) 177 | - [Direct Execution (For Advanced Use Cases)](#direct-execution-for-advanced-use-cases) 178 | - [Server Object Names](#server-object-names) 179 | - [Examples](#examples) 180 | - [Echo Server](#echo-server) 181 | - [SQLite Explorer](#sqlite-explorer) 182 | - [Contributing](#contributing) 183 | - [Prerequisites](#prerequisites) 184 | - [Installation](#installation-1) 185 | - [Testing](#testing) 186 | - [Formatting](#formatting) 187 | - [Opening a Pull Request](#opening-a-pull-request) 188 | 189 | ## Installation 190 | 191 | We strongly recommend installing FastMCP with [uv](https://docs.astral.sh/uv/), as it is required for deploying servers: 192 | 193 | ```bash 194 | uv pip install fastmcp 195 | ``` 196 | 197 | Note: on macOS, uv may need to be installed with Homebrew (`brew install uv`) in order to make it available to the Claude Desktop app. 198 | 199 | Alternatively, to use the SDK without deploying, you may use pip: 200 | 201 | ```bash 202 | pip install fastmcp 203 | ``` 204 | 205 | ## Quickstart 206 | 207 | Let's create a simple MCP server that exposes a calculator tool and some data: 208 | 209 | ```python 210 | # server.py 211 | 212 | from fastmcp import FastMCP 213 | 214 | 215 | # Create an MCP server 216 | mcp = FastMCP("Demo") 217 | 218 | 219 | # Add an addition tool 220 | @mcp.tool() 221 | def add(a: int, b: int) -> int: 222 | """Add two numbers""" 223 | return a + b 224 | 225 | 226 | # Add a dynamic greeting resource 227 | @mcp.resource("greeting://{name}") 228 | def get_greeting(name: str) -> str: 229 | """Get a personalized greeting""" 230 | return f"Hello, {name}!" 231 | ``` 232 | 233 | You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running: 234 | ```bash 235 | fastmcp install server.py 236 | ``` 237 | 238 | Alternatively, you can test it with the MCP Inspector: 239 | ```bash 240 | fastmcp dev server.py 241 | ``` 242 | 243 | ![MCP Inspector](/docs/assets/demo-inspector.png) 244 | 245 | ## What is MCP? 246 | 247 | The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: 248 | 249 | - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) 250 | - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) 251 | - Define interaction patterns through **Prompts** (reusable templates for LLM interactions) 252 | - And more! 253 | 254 | There is a low-level [Python SDK](https://github.com/modelcontextprotocol/python-sdk) available for implementing the protocol directly, but FastMCP aims to make that easier by providing a high-level, Pythonic interface. 255 | 256 | ## Core Concepts 257 | 258 | 259 | ### Server 260 | 261 | The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: 262 | 263 | ```python 264 | from fastmcp import FastMCP 265 | 266 | # Create a named server 267 | mcp = FastMCP("My App") 268 | 269 | # Specify dependencies for deployment and development 270 | mcp = FastMCP("My App", dependencies=["pandas", "numpy"]) 271 | ``` 272 | 273 | ### Resources 274 | 275 | Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects. Some examples: 276 | 277 | - File contents 278 | - Database schemas 279 | - API responses 280 | - System information 281 | 282 | Resources can be static: 283 | ```python 284 | @mcp.resource("config://app") 285 | def get_config() -> str: 286 | """Static configuration data""" 287 | return "App configuration here" 288 | ``` 289 | 290 | Or dynamic with parameters (FastMCP automatically handles these as MCP templates): 291 | ```python 292 | @mcp.resource("users://{user_id}/profile") 293 | def get_user_profile(user_id: str) -> str: 294 | """Dynamic user data""" 295 | return f"Profile data for user {user_id}" 296 | ``` 297 | 298 | ### Tools 299 | 300 | Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects. They're similar to POST endpoints in a REST API. 301 | 302 | Simple calculation example: 303 | ```python 304 | @mcp.tool() 305 | def calculate_bmi(weight_kg: float, height_m: float) -> float: 306 | """Calculate BMI given weight in kg and height in meters""" 307 | return weight_kg / (height_m ** 2) 308 | ``` 309 | 310 | HTTP request example: 311 | ```python 312 | import httpx 313 | 314 | @mcp.tool() 315 | async def fetch_weather(city: str) -> str: 316 | """Fetch current weather for a city""" 317 | async with httpx.AsyncClient() as client: 318 | response = await client.get( 319 | f"https://api.weather.com/{city}" 320 | ) 321 | return response.text 322 | ``` 323 | 324 | Complex input handling example: 325 | ```python 326 | from pydantic import BaseModel, Field 327 | from typing import Annotated 328 | 329 | class ShrimpTank(BaseModel): 330 | class Shrimp(BaseModel): 331 | name: Annotated[str, Field(max_length=10)] 332 | 333 | shrimp: list[Shrimp] 334 | 335 | @mcp.tool() 336 | def name_shrimp( 337 | tank: ShrimpTank, 338 | # You can use pydantic Field in function signatures for validation. 339 | extra_names: Annotated[list[str], Field(max_length=10)], 340 | ) -> list[str]: 341 | """List all shrimp names in the tank""" 342 | return [shrimp.name for shrimp in tank.shrimp] + extra_names 343 | ``` 344 | 345 | ### Prompts 346 | 347 | Prompts are reusable templates that help LLMs interact with your server effectively. They're like "best practices" encoded into your server. A prompt can be as simple as a string: 348 | 349 | ```python 350 | @mcp.prompt() 351 | def review_code(code: str) -> str: 352 | return f"Please review this code:\n\n{code}" 353 | ``` 354 | 355 | Or a more structured sequence of messages: 356 | ```python 357 | from fastmcp.prompts.base import UserMessage, AssistantMessage 358 | 359 | @mcp.prompt() 360 | def debug_error(error: str) -> list[Message]: 361 | return [ 362 | UserMessage("I'm seeing this error:"), 363 | UserMessage(error), 364 | AssistantMessage("I'll help debug that. What have you tried so far?") 365 | ] 366 | ``` 367 | 368 | 369 | ### Images 370 | 371 | FastMCP provides an `Image` class that automatically handles image data in your server: 372 | 373 | ```python 374 | from fastmcp import FastMCP, Image 375 | from PIL import Image as PILImage 376 | 377 | @mcp.tool() 378 | def create_thumbnail(image_path: str) -> Image: 379 | """Create a thumbnail from an image""" 380 | img = PILImage.open(image_path) 381 | img.thumbnail((100, 100)) 382 | 383 | # FastMCP automatically handles conversion and MIME types 384 | return Image(data=img.tobytes(), format="png") 385 | 386 | @mcp.tool() 387 | def load_image(path: str) -> Image: 388 | """Load an image from disk""" 389 | # FastMCP handles reading and format detection 390 | return Image(path=path) 391 | ``` 392 | 393 | Images can be used as the result of both tools and resources. 394 | 395 | ### Context 396 | 397 | The Context object gives your tools and resources access to MCP capabilities. To use it, add a parameter annotated with `fastmcp.Context`: 398 | 399 | ```python 400 | from fastmcp import FastMCP, Context 401 | 402 | @mcp.tool() 403 | async def long_task(files: list[str], ctx: Context) -> str: 404 | """Process multiple files with progress tracking""" 405 | for i, file in enumerate(files): 406 | ctx.info(f"Processing {file}") 407 | await ctx.report_progress(i, len(files)) 408 | 409 | # Read another resource if needed 410 | data = await ctx.read_resource(f"file://{file}") 411 | 412 | return "Processing complete" 413 | ``` 414 | 415 | The Context object provides: 416 | - Progress reporting through `report_progress()` 417 | - Logging via `debug()`, `info()`, `warning()`, and `error()` 418 | - Resource access through `read_resource()` 419 | - Request metadata via `request_id` and `client_id` 420 | 421 | ## Running Your Server 422 | 423 | There are three main ways to use your FastMCP server, each suited for different stages of development: 424 | 425 | ### Development Mode (Recommended for Building & Testing) 426 | 427 | The fastest way to test and debug your server is with the MCP Inspector: 428 | 429 | ```bash 430 | fastmcp dev server.py 431 | ``` 432 | 433 | This launches a web interface where you can: 434 | - Test your tools and resources interactively 435 | - See detailed logs and error messages 436 | - Monitor server performance 437 | - Set environment variables for testing 438 | 439 | During development, you can: 440 | - Add dependencies with `--with`: 441 | ```bash 442 | fastmcp dev server.py --with pandas --with numpy 443 | ``` 444 | - Mount your local code for live updates: 445 | ```bash 446 | fastmcp dev server.py --with-editable . 447 | ``` 448 | 449 | ### Claude Desktop Integration (For Regular Use) 450 | 451 | Once your server is ready, install it in Claude Desktop to use it with Claude: 452 | 453 | ```bash 454 | fastmcp install server.py 455 | ``` 456 | 457 | Your server will run in an isolated environment with: 458 | - Automatic installation of dependencies specified in your FastMCP instance: 459 | ```python 460 | mcp = FastMCP("My App", dependencies=["pandas", "numpy"]) 461 | ``` 462 | - Custom naming via `--name`: 463 | ```bash 464 | fastmcp install server.py --name "My Analytics Server" 465 | ``` 466 | - Environment variable management: 467 | ```bash 468 | # Set variables individually 469 | fastmcp install server.py -e API_KEY=abc123 -e DB_URL=postgres://... 470 | 471 | # Or load from a .env file 472 | fastmcp install server.py -f .env 473 | ``` 474 | 475 | ### Direct Execution (For Advanced Use Cases) 476 | 477 | For advanced scenarios like custom deployments or running without Claude, you can execute your server directly: 478 | 479 | ```python 480 | from fastmcp import FastMCP 481 | 482 | mcp = FastMCP("My App") 483 | 484 | if __name__ == "__main__": 485 | mcp.run() 486 | ``` 487 | 488 | Run it with: 489 | ```bash 490 | # Using the FastMCP CLI 491 | fastmcp run server.py 492 | 493 | # Or with Python/uv directly 494 | python server.py 495 | uv run python server.py 496 | ``` 497 | 498 | 499 | Note: When running directly, you are responsible for ensuring all dependencies are available in your environment. Any dependencies specified on the FastMCP instance are ignored. 500 | 501 | Choose this method when you need: 502 | - Custom deployment configurations 503 | - Integration with other services 504 | - Direct control over the server lifecycle 505 | 506 | ### Server Object Names 507 | 508 | All FastMCP commands will look for a server object called `mcp`, `app`, or `server` in your file. If you have a different object name or multiple servers in one file, use the syntax `server.py:my_server`: 509 | 510 | ```bash 511 | # Using a standard name 512 | fastmcp run server.py 513 | 514 | # Using a custom name 515 | fastmcp run server.py:my_custom_server 516 | ``` 517 | 518 | ## Examples 519 | 520 | Here are a few examples of FastMCP servers. For more, see the `examples/` directory. 521 | 522 | ### Echo Server 523 | A simple server demonstrating resources, tools, and prompts: 524 | 525 | ```python 526 | from fastmcp import FastMCP 527 | 528 | mcp = FastMCP("Echo") 529 | 530 | @mcp.resource("echo://{message}") 531 | def echo_resource(message: str) -> str: 532 | """Echo a message as a resource""" 533 | return f"Resource echo: {message}" 534 | 535 | @mcp.tool() 536 | def echo_tool(message: str) -> str: 537 | """Echo a message as a tool""" 538 | return f"Tool echo: {message}" 539 | 540 | @mcp.prompt() 541 | def echo_prompt(message: str) -> str: 542 | """Create an echo prompt""" 543 | return f"Please process this message: {message}" 544 | ``` 545 | 546 | ### SQLite Explorer 547 | A more complex example showing database integration: 548 | 549 | ```python 550 | from fastmcp import FastMCP 551 | import sqlite3 552 | 553 | mcp = FastMCP("SQLite Explorer") 554 | 555 | @mcp.resource("schema://main") 556 | def get_schema() -> str: 557 | """Provide the database schema as a resource""" 558 | conn = sqlite3.connect("database.db") 559 | schema = conn.execute( 560 | "SELECT sql FROM sqlite_master WHERE type='table'" 561 | ).fetchall() 562 | return "\n".join(sql[0] for sql in schema if sql[0]) 563 | 564 | @mcp.tool() 565 | def query_data(sql: str) -> str: 566 | """Execute SQL queries safely""" 567 | conn = sqlite3.connect("database.db") 568 | try: 569 | result = conn.execute(sql).fetchall() 570 | return "\n".join(str(row) for row in result) 571 | except Exception as e: 572 | return f"Error: {str(e)}" 573 | 574 | @mcp.prompt() 575 | def analyze_table(table: str) -> str: 576 | """Create a prompt template for analyzing tables""" 577 | return f"""Please analyze this database table: 578 | Table: {table} 579 | Schema: 580 | {get_schema()} 581 | 582 | What insights can you provide about the structure and relationships?""" 583 | ``` 584 | 585 | ## Contributing 586 | 587 |
588 | 589 |

Open Developer Guide

590 | 591 | ### Prerequisites 592 | 593 | FastMCP requires Python 3.10+ and [uv](https://docs.astral.sh/uv/). 594 | 595 | ### Installation 596 | 597 | For development, we recommend installing FastMCP with development dependencies, which includes various utilities the maintainers find useful. 598 | 599 | ```bash 600 | git clone https://github.com/jlowin/fastmcp.git 601 | cd fastmcp 602 | uv sync --frozen --extra dev 603 | ``` 604 | 605 | For running tests only (e.g., in CI), you only need the testing dependencies: 606 | 607 | ```bash 608 | uv sync --frozen --extra tests 609 | ``` 610 | 611 | ### Testing 612 | 613 | Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns. 614 | 615 | Run tests from the root directory: 616 | 617 | 618 | ```bash 619 | pytest -vv 620 | ``` 621 | 622 | ### Formatting 623 | 624 | FastMCP enforces a variety of required formats, which you can automatically enforce with pre-commit. 625 | 626 | Install the pre-commit hooks: 627 | 628 | ```bash 629 | pre-commit install 630 | ``` 631 | 632 | The hooks will now run on every commit (as well as on every PR). To run them manually: 633 | 634 | ```bash 635 | pre-commit run --all-files 636 | ``` 637 | 638 | ### Opening a Pull Request 639 | 640 | Fork the repository and create a new branch: 641 | 642 | ```bash 643 | git checkout -b my-branch 644 | ``` 645 | 646 | Make your changes and commit them: 647 | 648 | 649 | ```bash 650 | git add . && git commit -m "My changes" 651 | ``` 652 | 653 | Push your changes to your fork: 654 | 655 | 656 | ```bash 657 | git push origin my-branch 658 | ``` 659 | 660 | Feel free to reach out in a GitHub issue or discussion if you have any questions! 661 | 662 |
663 | 664 | 665 | ================================================ 666 | File: /examples/simple_echo.py 667 | ================================================ 668 | """ 669 | FastMCP Echo Server 670 | """ 671 | 672 | from fastmcp import FastMCP 673 | 674 | 675 | # Create server 676 | mcp = FastMCP("Echo Server") 677 | 678 | 679 | @mcp.tool() 680 | def echo(text: str) -> str: 681 | """Echo the input text""" 682 | return text 683 | 684 | 685 | ================================================ 686 | File: /examples/memory.py 687 | ================================================ 688 | # /// script 689 | # dependencies = ["pydantic-ai-slim[openai]", "asyncpg", "numpy", "pgvector", "fastmcp"] 690 | # /// 691 | 692 | # uv pip install 'pydantic-ai-slim[openai]' asyncpg numpy pgvector fastmcp 693 | 694 | """ 695 | Recursive memory system inspired by the human brain's clustering of memories. 696 | Uses OpenAI's 'text-embedding-3-small' model and pgvector for efficient similarity search. 697 | """ 698 | 699 | import asyncio 700 | import math 701 | import os 702 | from dataclasses import dataclass 703 | from datetime import datetime, timezone 704 | from pathlib import Path 705 | from typing import Annotated, Self 706 | 707 | import asyncpg 708 | import numpy as np 709 | from openai import AsyncOpenAI 710 | from pgvector.asyncpg import register_vector # Import register_vector 711 | from pydantic import BaseModel, Field 712 | from pydantic_ai import Agent 713 | 714 | from fastmcp import FastMCP 715 | 716 | MAX_DEPTH = 5 717 | SIMILARITY_THRESHOLD = 0.7 718 | DECAY_FACTOR = 0.99 719 | REINFORCEMENT_FACTOR = 1.1 720 | 721 | DEFAULT_LLM_MODEL = "openai:gpt-4o" 722 | DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small" 723 | 724 | mcp = FastMCP( 725 | "memory", 726 | dependencies=[ 727 | "pydantic-ai-slim[openai]", 728 | "asyncpg", 729 | "numpy", 730 | "pgvector", 731 | ], 732 | ) 733 | 734 | DB_DSN = "postgresql://postgres:postgres@localhost:54320/memory_db" 735 | # reset memory with rm ~/.fastmcp/{USER}/memory/* 736 | PROFILE_DIR = ( 737 | Path.home() / ".fastmcp" / os.environ.get("USER", "anon") / "memory" 738 | ).resolve() 739 | PROFILE_DIR.mkdir(parents=True, exist_ok=True) 740 | 741 | 742 | def cosine_similarity(a: list[float], b: list[float]) -> float: 743 | a_array = np.array(a, dtype=np.float64) 744 | b_array = np.array(b, dtype=np.float64) 745 | return np.dot(a_array, b_array) / ( 746 | np.linalg.norm(a_array) * np.linalg.norm(b_array) 747 | ) 748 | 749 | 750 | async def do_ai[T]( 751 | user_prompt: str, 752 | system_prompt: str, 753 | result_type: type[T] | Annotated, 754 | deps=None, 755 | ) -> T: 756 | agent = Agent( 757 | DEFAULT_LLM_MODEL, 758 | system_prompt=system_prompt, 759 | result_type=result_type, 760 | ) 761 | result = await agent.run(user_prompt, deps=deps) 762 | return result.data 763 | 764 | 765 | @dataclass 766 | class Deps: 767 | openai: AsyncOpenAI 768 | pool: asyncpg.Pool 769 | 770 | 771 | async def get_db_pool() -> asyncpg.Pool: 772 | async def init(conn): 773 | await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;") 774 | await register_vector(conn) 775 | 776 | pool = await asyncpg.create_pool(DB_DSN, init=init) 777 | return pool 778 | 779 | 780 | class MemoryNode(BaseModel): 781 | id: int | None = None 782 | content: str 783 | summary: str = "" 784 | importance: float = 1.0 785 | access_count: int = 0 786 | timestamp: float = Field( 787 | default_factory=lambda: datetime.now(timezone.utc).timestamp() 788 | ) 789 | embedding: list[float] 790 | 791 | @classmethod 792 | async def from_content(cls, content: str, deps: Deps): 793 | embedding = await get_embedding(content, deps) 794 | return cls(content=content, embedding=embedding) 795 | 796 | async def save(self, deps: Deps): 797 | async with deps.pool.acquire() as conn: 798 | if self.id is None: 799 | result = await conn.fetchrow( 800 | """ 801 | INSERT INTO memories (content, summary, importance, access_count, timestamp, embedding) 802 | VALUES ($1, $2, $3, $4, $5, $6) 803 | RETURNING id 804 | """, 805 | self.content, 806 | self.summary, 807 | self.importance, 808 | self.access_count, 809 | self.timestamp, 810 | self.embedding, 811 | ) 812 | self.id = result["id"] 813 | else: 814 | await conn.execute( 815 | """ 816 | UPDATE memories 817 | SET content = $1, summary = $2, importance = $3, 818 | access_count = $4, timestamp = $5, embedding = $6 819 | WHERE id = $7 820 | """, 821 | self.content, 822 | self.summary, 823 | self.importance, 824 | self.access_count, 825 | self.timestamp, 826 | self.embedding, 827 | self.id, 828 | ) 829 | 830 | async def merge_with(self, other: Self, deps: Deps): 831 | self.content = await do_ai( 832 | f"{self.content}\n\n{other.content}", 833 | "Combine the following two texts into a single, coherent text.", 834 | str, 835 | deps, 836 | ) 837 | self.importance += other.importance 838 | self.access_count += other.access_count 839 | self.embedding = [(a + b) / 2 for a, b in zip(self.embedding, other.embedding)] 840 | self.summary = await do_ai( 841 | self.content, "Summarize the following text concisely.", str, deps 842 | ) 843 | await self.save(deps) 844 | # Delete the merged node from the database 845 | if other.id is not None: 846 | await delete_memory(other.id, deps) 847 | 848 | def get_effective_importance(self): 849 | return self.importance * (1 + math.log(self.access_count + 1)) 850 | 851 | 852 | async def get_embedding(text: str, deps: Deps) -> list[float]: 853 | embedding_response = await deps.openai.embeddings.create( 854 | input=text, 855 | model=DEFAULT_EMBEDDING_MODEL, 856 | ) 857 | return embedding_response.data[0].embedding 858 | 859 | 860 | async def delete_memory(memory_id: int, deps: Deps): 861 | async with deps.pool.acquire() as conn: 862 | await conn.execute("DELETE FROM memories WHERE id = $1", memory_id) 863 | 864 | 865 | async def add_memory(content: str, deps: Deps): 866 | new_memory = await MemoryNode.from_content(content, deps) 867 | await new_memory.save(deps) 868 | 869 | similar_memories = await find_similar_memories(new_memory.embedding, deps) 870 | for memory in similar_memories: 871 | if memory.id != new_memory.id: 872 | await new_memory.merge_with(memory, deps) 873 | 874 | await update_importance(new_memory.embedding, deps) 875 | 876 | await prune_memories(deps) 877 | 878 | return f"Remembered: {content}" 879 | 880 | 881 | async def find_similar_memories(embedding: list[float], deps: Deps) -> list[MemoryNode]: 882 | async with deps.pool.acquire() as conn: 883 | rows = await conn.fetch( 884 | """ 885 | SELECT id, content, summary, importance, access_count, timestamp, embedding 886 | FROM memories 887 | ORDER BY embedding <-> $1 888 | LIMIT 5 889 | """, 890 | embedding, 891 | ) 892 | memories = [ 893 | MemoryNode( 894 | id=row["id"], 895 | content=row["content"], 896 | summary=row["summary"], 897 | importance=row["importance"], 898 | access_count=row["access_count"], 899 | timestamp=row["timestamp"], 900 | embedding=row["embedding"], 901 | ) 902 | for row in rows 903 | ] 904 | return memories 905 | 906 | 907 | async def update_importance(user_embedding: list[float], deps: Deps): 908 | async with deps.pool.acquire() as conn: 909 | rows = await conn.fetch( 910 | "SELECT id, importance, access_count, embedding FROM memories" 911 | ) 912 | for row in rows: 913 | memory_embedding = row["embedding"] 914 | similarity = cosine_similarity(user_embedding, memory_embedding) 915 | if similarity > SIMILARITY_THRESHOLD: 916 | new_importance = row["importance"] * REINFORCEMENT_FACTOR 917 | new_access_count = row["access_count"] + 1 918 | else: 919 | new_importance = row["importance"] * DECAY_FACTOR 920 | new_access_count = row["access_count"] 921 | await conn.execute( 922 | """ 923 | UPDATE memories 924 | SET importance = $1, access_count = $2 925 | WHERE id = $3 926 | """, 927 | new_importance, 928 | new_access_count, 929 | row["id"], 930 | ) 931 | 932 | 933 | async def prune_memories(deps: Deps): 934 | async with deps.pool.acquire() as conn: 935 | rows = await conn.fetch( 936 | """ 937 | SELECT id, importance, access_count 938 | FROM memories 939 | ORDER BY importance DESC 940 | OFFSET $1 941 | """, 942 | MAX_DEPTH, 943 | ) 944 | for row in rows: 945 | await conn.execute("DELETE FROM memories WHERE id = $1", row["id"]) 946 | 947 | 948 | async def display_memory_tree(deps: Deps) -> str: 949 | async with deps.pool.acquire() as conn: 950 | rows = await conn.fetch( 951 | """ 952 | SELECT content, summary, importance, access_count 953 | FROM memories 954 | ORDER BY importance DESC 955 | LIMIT $1 956 | """, 957 | MAX_DEPTH, 958 | ) 959 | result = "" 960 | for row in rows: 961 | effective_importance = row["importance"] * ( 962 | 1 + math.log(row["access_count"] + 1) 963 | ) 964 | summary = row["summary"] or row["content"] 965 | result += f"- {summary} (Importance: {effective_importance:.2f})\n" 966 | return result 967 | 968 | 969 | @mcp.tool() 970 | async def remember( 971 | contents: list[str] = Field( 972 | description="List of observations or memories to store" 973 | ), 974 | ): 975 | deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool()) 976 | try: 977 | return "\n".join( 978 | await asyncio.gather(*[add_memory(content, deps) for content in contents]) 979 | ) 980 | finally: 981 | await deps.pool.close() 982 | 983 | 984 | @mcp.tool() 985 | async def read_profile() -> str: 986 | deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool()) 987 | profile = await display_memory_tree(deps) 988 | await deps.pool.close() 989 | return profile 990 | 991 | 992 | async def initialize_database(): 993 | pool = await asyncpg.create_pool( 994 | "postgresql://postgres:postgres@localhost:54320/postgres" 995 | ) 996 | try: 997 | async with pool.acquire() as conn: 998 | await conn.execute(""" 999 | SELECT pg_terminate_backend(pg_stat_activity.pid) 1000 | FROM pg_stat_activity 1001 | WHERE pg_stat_activity.datname = 'memory_db' 1002 | AND pid <> pg_backend_pid(); 1003 | """) 1004 | await conn.execute("DROP DATABASE IF EXISTS memory_db;") 1005 | await conn.execute("CREATE DATABASE memory_db;") 1006 | finally: 1007 | await pool.close() 1008 | 1009 | pool = await asyncpg.create_pool(DB_DSN) 1010 | try: 1011 | async with pool.acquire() as conn: 1012 | await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;") 1013 | 1014 | await register_vector(conn) 1015 | 1016 | await conn.execute(""" 1017 | CREATE TABLE IF NOT EXISTS memories ( 1018 | id SERIAL PRIMARY KEY, 1019 | content TEXT NOT NULL, 1020 | summary TEXT, 1021 | importance REAL NOT NULL, 1022 | access_count INT NOT NULL, 1023 | timestamp DOUBLE PRECISION NOT NULL, 1024 | embedding vector(1536) NOT NULL 1025 | ); 1026 | CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw (embedding vector_l2_ops); 1027 | """) 1028 | finally: 1029 | await pool.close() 1030 | 1031 | 1032 | if __name__ == "__main__": 1033 | asyncio.run(initialize_database()) 1034 | 1035 | 1036 | ================================================ 1037 | File: /examples/readme-quickstart.py 1038 | ================================================ 1039 | from fastmcp import FastMCP 1040 | 1041 | 1042 | # Create an MCP server 1043 | mcp = FastMCP("Demo") 1044 | 1045 | 1046 | # Add an addition tool 1047 | @mcp.tool() 1048 | def add(a: int, b: int) -> int: 1049 | """Add two numbers""" 1050 | return a + b 1051 | 1052 | 1053 | # Add a dynamic greeting resource 1054 | @mcp.resource("greeting://{name}") 1055 | def get_greeting(name: str) -> str: 1056 | """Get a personalized greeting""" 1057 | return f"Hello, {name}!" 1058 | 1059 | 1060 | ================================================ 1061 | File: /examples/text_me.py 1062 | ================================================ 1063 | # /// script 1064 | # dependencies = ["fastmcp"] 1065 | # /// 1066 | 1067 | """ 1068 | FastMCP Text Me Server 1069 | -------------------------------- 1070 | This defines a simple FastMCP server that sends a text message to a phone number via https://surgemsg.com/. 1071 | 1072 | To run this example, create a `.env` file with the following values: 1073 | 1074 | SURGE_API_KEY=... 1075 | SURGE_ACCOUNT_ID=... 1076 | SURGE_MY_PHONE_NUMBER=... 1077 | SURGE_MY_FIRST_NAME=... 1078 | SURGE_MY_LAST_NAME=... 1079 | 1080 | Visit https://surgemsg.com/ and click "Get Started" to obtain these values. 1081 | """ 1082 | 1083 | from typing import Annotated 1084 | import httpx 1085 | from pydantic import BeforeValidator 1086 | from pydantic_settings import BaseSettings, SettingsConfigDict 1087 | 1088 | from fastmcp import FastMCP 1089 | 1090 | 1091 | class SurgeSettings(BaseSettings): 1092 | model_config: SettingsConfigDict = SettingsConfigDict( 1093 | env_prefix="SURGE_", env_file=".env" 1094 | ) 1095 | 1096 | api_key: str 1097 | account_id: str 1098 | my_phone_number: Annotated[ 1099 | str, BeforeValidator(lambda v: "+" + v if not v.startswith("+") else v) 1100 | ] 1101 | my_first_name: str 1102 | my_last_name: str 1103 | 1104 | 1105 | # Create server 1106 | mcp = FastMCP("Text me") 1107 | surge_settings = SurgeSettings() # type: ignore 1108 | 1109 | 1110 | @mcp.tool(name="textme", description="Send a text message to me") 1111 | def text_me(text_content: str) -> str: 1112 | """Send a text message to a phone number via https://surgemsg.com/""" 1113 | with httpx.Client() as client: 1114 | response = client.post( 1115 | "https://api.surgemsg.com/messages", 1116 | headers={ 1117 | "Authorization": f"Bearer {surge_settings.api_key}", 1118 | "Surge-Account": surge_settings.account_id, 1119 | "Content-Type": "application/json", 1120 | }, 1121 | json={ 1122 | "body": text_content, 1123 | "conversation": { 1124 | "contact": { 1125 | "first_name": surge_settings.my_first_name, 1126 | "last_name": surge_settings.my_last_name, 1127 | "phone_number": surge_settings.my_phone_number, 1128 | } 1129 | }, 1130 | }, 1131 | ) 1132 | response.raise_for_status() 1133 | return f"Message sent: {text_content}" 1134 | 1135 | 1136 | ================================================ 1137 | File: /examples/screenshot.py 1138 | ================================================ 1139 | """ 1140 | FastMCP Screenshot Example 1141 | 1142 | Give Claude a tool to capture and view screenshots. 1143 | """ 1144 | 1145 | import io 1146 | from fastmcp import FastMCP, Image 1147 | 1148 | 1149 | # Create server 1150 | mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"]) 1151 | 1152 | 1153 | @mcp.tool() 1154 | def take_screenshot() -> Image: 1155 | """ 1156 | Take a screenshot of the user's screen and return it as an image. Use 1157 | this tool anytime the user wants you to look at something they're doing. 1158 | """ 1159 | import pyautogui 1160 | 1161 | buffer = io.BytesIO() 1162 | 1163 | # if the file exceeds ~1MB, it will be rejected by Claude 1164 | screenshot = pyautogui.screenshot() 1165 | screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True) 1166 | return Image(data=buffer.getvalue(), format="jpeg") 1167 | 1168 | 1169 | ================================================ 1170 | File: /examples/echo.py 1171 | ================================================ 1172 | """ 1173 | FastMCP Echo Server 1174 | """ 1175 | 1176 | from fastmcp import FastMCP 1177 | 1178 | # Create server 1179 | mcp = FastMCP("Echo Server") 1180 | 1181 | 1182 | @mcp.tool() 1183 | def echo_tool(text: str) -> str: 1184 | """Echo the input text""" 1185 | return text 1186 | 1187 | 1188 | @mcp.resource("echo://static") 1189 | def echo_resource() -> str: 1190 | return "Echo!" 1191 | 1192 | 1193 | @mcp.resource("echo://{text}") 1194 | def echo_template(text: str) -> str: 1195 | """Echo the input text""" 1196 | return f"Echo: {text}" 1197 | 1198 | 1199 | @mcp.prompt("echo") 1200 | def echo_prompt(text: str) -> str: 1201 | return text 1202 | 1203 | 1204 | ================================================ 1205 | File: /examples/desktop.py 1206 | ================================================ 1207 | """ 1208 | FastMCP Desktop Example 1209 | 1210 | A simple example that exposes the desktop directory as a resource. 1211 | """ 1212 | 1213 | from pathlib import Path 1214 | 1215 | from fastmcp.server import FastMCP 1216 | 1217 | # Create server 1218 | mcp = FastMCP("Demo") 1219 | 1220 | 1221 | @mcp.resource("dir://desktop") 1222 | def desktop() -> list[str]: 1223 | """List the files in the user's desktop""" 1224 | desktop = Path.home() / "Desktop" 1225 | return [str(f) for f in desktop.iterdir()] 1226 | 1227 | 1228 | @mcp.tool() 1229 | def add(a: int, b: int) -> int: 1230 | """Add two numbers""" 1231 | return a + b 1232 | 1233 | 1234 | ================================================ 1235 | File: /examples/complex_inputs.py 1236 | ================================================ 1237 | """ 1238 | FastMCP Complex inputs Example 1239 | 1240 | Demonstrates validation via pydantic with complex models. 1241 | """ 1242 | 1243 | from pydantic import BaseModel, Field 1244 | from typing import Annotated 1245 | from fastmcp.server import FastMCP 1246 | 1247 | mcp = FastMCP("Shrimp Tank") 1248 | 1249 | 1250 | class ShrimpTank(BaseModel): 1251 | class Shrimp(BaseModel): 1252 | name: Annotated[str, Field(max_length=10)] 1253 | 1254 | shrimp: list[Shrimp] 1255 | 1256 | 1257 | @mcp.tool() 1258 | def name_shrimp( 1259 | tank: ShrimpTank, 1260 | # You can use pydantic Field in function signatures for validation. 1261 | extra_names: Annotated[list[str], Field(max_length=10)], 1262 | ) -> list[str]: 1263 | """List all shrimp names in the tank""" 1264 | return [shrimp.name for shrimp in tank.shrimp] + extra_names 1265 | 1266 | 1267 | ================================================ 1268 | File: /Windows_Notes.md 1269 | ================================================ 1270 | # Getting your development environment set up properly 1271 | To get your environment up and running properly, you'll need a slightly different set of commands that are windows specific: 1272 | ```bash 1273 | uv venv 1274 | .venv\Scripts\activate 1275 | uv pip install -e ".[dev]" 1276 | ``` 1277 | 1278 | This will install the package in editable mode, and install the development dependencies. 1279 | 1280 | 1281 | # Fixing `AttributeError: module 'collections' has no attribute 'Callable'` 1282 | - open `.venv\Lib\site-packages\pyreadline\py3k_compat.py` 1283 | - change `return isinstance(x, collections.Callable)` to 1284 | ``` 1285 | from collections.abc import Callable 1286 | return isinstance(x, Callable) 1287 | ``` 1288 | 1289 | # Helpful notes 1290 | For developing FastMCP 1291 | ## Install local development version of FastMCP into a local FastMCP project server 1292 | - ensure 1293 | - change directories to your FastMCP Server location so you can install it in your .venv 1294 | - run `.venv\Scripts\activate` to activate your virtual environment 1295 | - Then run a series of commands to uninstall the old version and install the new 1296 | ```bash 1297 | # First uninstall 1298 | uv pip uninstall fastmcp 1299 | 1300 | # Clean any build artifacts in your fastmcp directory 1301 | cd C:\path\to\fastmcp 1302 | del /s /q *.egg-info 1303 | 1304 | # Then reinstall in your weather project 1305 | cd C:\path\to\new\fastmcp_server 1306 | uv pip install --no-cache-dir -e C:\Users\justj\PycharmProjects\fastmcp 1307 | 1308 | # Check that it installed properly and has the correct git hash 1309 | pip show fastmcp 1310 | ``` 1311 | 1312 | ## Running the FastMCP server with Inspector 1313 | MCP comes with a node.js application called Inspector that can be used to inspect the FastMCP server. To run the inspector, you'll need to install node.js and npm. Then you can run the following commands: 1314 | ```bash 1315 | fastmcp dev server.py 1316 | ``` 1317 | This will launch a web app on http://localhost:5173/ that you can use to inspect the FastMCP server. 1318 | 1319 | ## If you start development before creating a fork - your get out of jail free card 1320 | - Add your fork as a new remote to your local repository `git remote add fork git@github.com:YOUR-USERNAME/REPOSITORY-NAME.git` 1321 | - This will add your repo, short named 'fork', as a remote to your local repository 1322 | - Verify that it was added correctly by running `git remote -v` 1323 | - Commit your changes 1324 | - Push your changes to your fork `git push fork ` 1325 | - Create your pull request on GitHub 1326 | 1327 | 1328 | 1329 | 1330 | ================================================ 1331 | File: /src/fastmcp/server.py 1332 | ================================================ 1333 | """FastMCP - A more ergonomic interface for MCP servers.""" 1334 | 1335 | import asyncio 1336 | import functools 1337 | import inspect 1338 | import json 1339 | import re 1340 | from itertools import chain 1341 | from typing import Any, Callable, Dict, Literal, Sequence, TypeVar, ParamSpec 1342 | 1343 | import pydantic_core 1344 | from pydantic import Field 1345 | import uvicorn 1346 | from mcp.server import Server as MCPServer 1347 | from mcp.server.sse import SseServerTransport 1348 | from mcp.server.stdio import stdio_server 1349 | from mcp.shared.context import RequestContext 1350 | from mcp.types import ( 1351 | EmbeddedResource, 1352 | GetPromptResult, 1353 | ImageContent, 1354 | TextContent, 1355 | ) 1356 | from mcp.types import ( 1357 | Prompt as MCPPrompt, 1358 | PromptArgument as MCPPromptArgument, 1359 | ) 1360 | from mcp.types import ( 1361 | Resource as MCPResource, 1362 | ) 1363 | from mcp.types import ( 1364 | ResourceTemplate as MCPResourceTemplate, 1365 | ) 1366 | from mcp.types import ( 1367 | Tool as MCPTool, 1368 | ) 1369 | from pydantic import BaseModel 1370 | from pydantic.networks import AnyUrl 1371 | from pydantic_settings import BaseSettings, SettingsConfigDict 1372 | 1373 | from fastmcp.exceptions import ResourceError 1374 | from fastmcp.prompts import Prompt, PromptManager 1375 | from fastmcp.prompts.base import PromptResult 1376 | from fastmcp.resources import FunctionResource, Resource, ResourceManager 1377 | from fastmcp.tools import ToolManager 1378 | from fastmcp.utilities.logging import configure_logging, get_logger 1379 | from fastmcp.utilities.types import Image 1380 | 1381 | logger = get_logger(__name__) 1382 | 1383 | P = ParamSpec("P") 1384 | R = TypeVar("R") 1385 | R_PromptResult = TypeVar("R_PromptResult", bound=PromptResult) 1386 | 1387 | 1388 | class Settings(BaseSettings): 1389 | """FastMCP server settings. 1390 | 1391 | All settings can be configured via environment variables with the prefix FASTMCP_. 1392 | For example, FASTMCP_DEBUG=true will set debug=True. 1393 | """ 1394 | 1395 | model_config: SettingsConfigDict = SettingsConfigDict( 1396 | env_prefix="FASTMCP_", 1397 | env_file=".env", 1398 | extra="ignore", 1399 | ) 1400 | 1401 | # Server settings 1402 | debug: bool = False 1403 | log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" 1404 | 1405 | # HTTP settings 1406 | host: str = "0.0.0.0" 1407 | port: int = 8000 1408 | 1409 | # resource settings 1410 | warn_on_duplicate_resources: bool = True 1411 | 1412 | # tool settings 1413 | warn_on_duplicate_tools: bool = True 1414 | 1415 | # prompt settings 1416 | warn_on_duplicate_prompts: bool = True 1417 | 1418 | dependencies: list[str] = Field( 1419 | default_factory=list, 1420 | description="List of dependencies to install in the server environment", 1421 | ) 1422 | 1423 | 1424 | class FastMCP: 1425 | def __init__(self, name: str | None = None, **settings: Any): 1426 | self.settings = Settings(**settings) 1427 | self._mcp_server = MCPServer(name=name or "FastMCP") 1428 | self._tool_manager = ToolManager( 1429 | warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools 1430 | ) 1431 | self._resource_manager = ResourceManager( 1432 | warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources 1433 | ) 1434 | self._prompt_manager = PromptManager( 1435 | warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts 1436 | ) 1437 | self.dependencies = self.settings.dependencies 1438 | 1439 | # Set up MCP protocol handlers 1440 | self._setup_handlers() 1441 | 1442 | # Configure logging 1443 | configure_logging(self.settings.log_level) 1444 | 1445 | @property 1446 | def name(self) -> str: 1447 | return self._mcp_server.name 1448 | 1449 | def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None: 1450 | """Run the FastMCP server. Note this is a synchronous function. 1451 | 1452 | Args: 1453 | transport: Transport protocol to use ("stdio" or "sse") 1454 | """ 1455 | TRANSPORTS = Literal["stdio", "sse"] 1456 | if transport not in TRANSPORTS.__args__: # type: ignore 1457 | raise ValueError(f"Unknown transport: {transport}") 1458 | 1459 | if transport == "stdio": 1460 | asyncio.run(self.run_stdio_async()) 1461 | else: # transport == "sse" 1462 | asyncio.run(self.run_sse_async()) 1463 | 1464 | def _setup_handlers(self) -> None: 1465 | """Set up core MCP protocol handlers.""" 1466 | self._mcp_server.list_tools()(self.list_tools) 1467 | self._mcp_server.call_tool()(self.call_tool) 1468 | self._mcp_server.list_resources()(self.list_resources) 1469 | self._mcp_server.read_resource()(self.read_resource) 1470 | self._mcp_server.list_prompts()(self.list_prompts) 1471 | self._mcp_server.get_prompt()(self.get_prompt) 1472 | # TODO: This has not been added to MCP yet, see https://github.com/jlowin/fastmcp/issues/10 1473 | # self._mcp_server.list_resource_templates()(self.list_resource_templates) 1474 | 1475 | async def list_tools(self) -> list[MCPTool]: 1476 | """List all available tools.""" 1477 | tools = self._tool_manager.list_tools() 1478 | return [ 1479 | MCPTool( 1480 | name=info.name, 1481 | description=info.description, 1482 | inputSchema=info.parameters, 1483 | ) 1484 | for info in tools 1485 | ] 1486 | 1487 | def get_context(self) -> "Context": 1488 | """ 1489 | Returns a Context object. Note that the context will only be valid 1490 | during a request; outside a request, most methods will error. 1491 | """ 1492 | try: 1493 | request_context = self._mcp_server.request_context 1494 | except LookupError: 1495 | request_context = None 1496 | return Context(request_context=request_context, fastmcp=self) 1497 | 1498 | async def call_tool( 1499 | self, name: str, arguments: dict 1500 | ) -> Sequence[TextContent | ImageContent | EmbeddedResource]: 1501 | """Call a tool by name with arguments.""" 1502 | context = self.get_context() 1503 | result = await self._tool_manager.call_tool(name, arguments, context=context) 1504 | converted_result = _convert_to_content(result) 1505 | return converted_result 1506 | 1507 | async def list_resources(self) -> list[MCPResource]: 1508 | """List all available resources.""" 1509 | 1510 | resources = self._resource_manager.list_resources() 1511 | return [ 1512 | MCPResource( 1513 | uri=resource.uri, 1514 | name=resource.name or "", 1515 | description=resource.description, 1516 | mimeType=resource.mime_type, 1517 | ) 1518 | for resource in resources 1519 | ] 1520 | 1521 | async def list_resource_templates(self) -> list[MCPResourceTemplate]: 1522 | templates = self._resource_manager.list_templates() 1523 | return [ 1524 | MCPResourceTemplate( 1525 | uriTemplate=template.uri_template, 1526 | name=template.name, 1527 | description=template.description, 1528 | ) 1529 | for template in templates 1530 | ] 1531 | 1532 | async def read_resource(self, uri: AnyUrl | str) -> str | bytes: 1533 | """Read a resource by URI.""" 1534 | resource = await self._resource_manager.get_resource(uri) 1535 | if not resource: 1536 | raise ResourceError(f"Unknown resource: {uri}") 1537 | 1538 | try: 1539 | return await resource.read() 1540 | except Exception as e: 1541 | logger.error(f"Error reading resource {uri}: {e}") 1542 | raise ResourceError(str(e)) 1543 | 1544 | def add_tool( 1545 | self, 1546 | fn: Callable, 1547 | name: str | None = None, 1548 | description: str | None = None, 1549 | ) -> None: 1550 | """Add a tool to the server. 1551 | 1552 | The tool function can optionally request a Context object by adding a parameter 1553 | with the Context type annotation. See the @tool decorator for examples. 1554 | 1555 | Args: 1556 | fn: The function to register as a tool 1557 | name: Optional name for the tool (defaults to function name) 1558 | description: Optional description of what the tool does 1559 | """ 1560 | self._tool_manager.add_tool(fn, name=name, description=description) 1561 | 1562 | def tool( 1563 | self, name: str | None = None, description: str | None = None 1564 | ) -> Callable[[Callable[P, R]], Callable[P, R]]: 1565 | """Decorator to register a tool. 1566 | 1567 | Tools can optionally request a Context object by adding a parameter with the Context type annotation. 1568 | The context provides access to MCP capabilities like logging, progress reporting, and resource access. 1569 | 1570 | Args: 1571 | name: Optional name for the tool (defaults to function name) 1572 | description: Optional description of what the tool does 1573 | 1574 | Example: 1575 | @server.tool() 1576 | def my_tool(x: int) -> str: 1577 | return str(x) 1578 | 1579 | @server.tool() 1580 | def tool_with_context(x: int, ctx: Context) -> str: 1581 | ctx.info(f"Processing {x}") 1582 | return str(x) 1583 | 1584 | @server.tool() 1585 | async def async_tool(x: int, context: Context) -> str: 1586 | await context.report_progress(50, 100) 1587 | return str(x) 1588 | """ 1589 | # Check if user passed function directly instead of calling decorator 1590 | if callable(name): 1591 | raise TypeError( 1592 | "The @tool decorator was used incorrectly. " 1593 | "Did you forget to call it? Use @tool() instead of @tool" 1594 | ) 1595 | 1596 | def decorator(fn: Callable[P, R]) -> Callable[P, R]: 1597 | self.add_tool(fn, name=name, description=description) 1598 | return fn 1599 | 1600 | return decorator 1601 | 1602 | def add_resource(self, resource: Resource) -> None: 1603 | """Add a resource to the server. 1604 | 1605 | Args: 1606 | resource: A Resource instance to add 1607 | """ 1608 | self._resource_manager.add_resource(resource) 1609 | 1610 | def resource( 1611 | self, 1612 | uri: str, 1613 | *, 1614 | name: str | None = None, 1615 | description: str | None = None, 1616 | mime_type: str | None = None, 1617 | ) -> Callable[[Callable[P, R]], Callable[P, R]]: 1618 | """Decorator to register a function as a resource. 1619 | 1620 | The function will be called when the resource is read to generate its content. 1621 | The function can return: 1622 | - str for text content 1623 | - bytes for binary content 1624 | - other types will be converted to JSON 1625 | 1626 | If the URI contains parameters (e.g. "resource://{param}") or the function 1627 | has parameters, it will be registered as a template resource. 1628 | 1629 | Args: 1630 | uri: URI for the resource (e.g. "resource://my-resource" or "resource://{param}") 1631 | name: Optional name for the resource 1632 | description: Optional description of the resource 1633 | mime_type: Optional MIME type for the resource 1634 | 1635 | Example: 1636 | @server.resource("resource://my-resource") 1637 | def get_data() -> str: 1638 | return "Hello, world!" 1639 | 1640 | @server.resource("resource://{city}/weather") 1641 | def get_weather(city: str) -> str: 1642 | return f"Weather for {city}" 1643 | """ 1644 | # Check if user passed function directly instead of calling decorator 1645 | if callable(uri): 1646 | raise TypeError( 1647 | "The @resource decorator was used incorrectly. " 1648 | "Did you forget to call it? Use @resource('uri') instead of @resource" 1649 | ) 1650 | 1651 | def decorator(fn: Callable[P, R]) -> Callable[P, R]: 1652 | @functools.wraps(fn) 1653 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: 1654 | return fn(*args, **kwargs) 1655 | 1656 | # Check if this should be a template 1657 | has_uri_params = "{" in uri and "}" in uri 1658 | has_func_params = bool(inspect.signature(fn).parameters) 1659 | 1660 | if has_uri_params or has_func_params: 1661 | # Validate that URI params match function params 1662 | uri_params = set(re.findall(r"{(\w+)}", uri)) 1663 | func_params = set(inspect.signature(fn).parameters.keys()) 1664 | 1665 | if uri_params != func_params: 1666 | raise ValueError( 1667 | f"Mismatch between URI parameters {uri_params} " 1668 | f"and function parameters {func_params}" 1669 | ) 1670 | 1671 | # Register as template 1672 | self._resource_manager.add_template( 1673 | wrapper, 1674 | uri_template=uri, 1675 | name=name, 1676 | description=description, 1677 | mime_type=mime_type or "text/plain", 1678 | ) 1679 | else: 1680 | # Register as regular resource 1681 | resource = FunctionResource( 1682 | uri=AnyUrl(uri), 1683 | name=name, 1684 | description=description, 1685 | mime_type=mime_type or "text/plain", 1686 | fn=wrapper, 1687 | ) 1688 | self.add_resource(resource) 1689 | return wrapper 1690 | 1691 | return decorator 1692 | 1693 | def add_prompt(self, prompt: Prompt) -> None: 1694 | """Add a prompt to the server. 1695 | 1696 | Args: 1697 | prompt: A Prompt instance to add 1698 | """ 1699 | self._prompt_manager.add_prompt(prompt) 1700 | 1701 | def prompt( 1702 | self, name: str | None = None, description: str | None = None 1703 | ) -> Callable[[Callable[P, R_PromptResult]], Callable[P, R_PromptResult]]: 1704 | """Decorator to register a prompt. 1705 | 1706 | Args: 1707 | name: Optional name for the prompt (defaults to function name) 1708 | description: Optional description of what the prompt does 1709 | 1710 | Example: 1711 | @server.prompt() 1712 | def analyze_table(table_name: str) -> list[Message]: 1713 | schema = read_table_schema(table_name) 1714 | return [ 1715 | { 1716 | "role": "user", 1717 | "content": f"Analyze this schema:\n{schema}" 1718 | } 1719 | ] 1720 | 1721 | @server.prompt() 1722 | async def analyze_file(path: str) -> list[Message]: 1723 | content = await read_file(path) 1724 | return [ 1725 | { 1726 | "role": "user", 1727 | "content": { 1728 | "type": "resource", 1729 | "resource": { 1730 | "uri": f"file://{path}", 1731 | "text": content 1732 | } 1733 | } 1734 | } 1735 | ] 1736 | """ 1737 | # Check if user passed function directly instead of calling decorator 1738 | if callable(name): 1739 | raise TypeError( 1740 | "The @prompt decorator was used incorrectly. " 1741 | "Did you forget to call it? Use @prompt() instead of @prompt" 1742 | ) 1743 | 1744 | def decorator(func: Callable[P, R_PromptResult]) -> Callable[P, R_PromptResult]: 1745 | prompt = Prompt.from_function(func, name=name, description=description) 1746 | self.add_prompt(prompt) 1747 | return func 1748 | 1749 | return decorator 1750 | 1751 | async def run_stdio_async(self) -> None: 1752 | """Run the server using stdio transport.""" 1753 | async with stdio_server() as (read_stream, write_stream): 1754 | await self._mcp_server.run( 1755 | read_stream, 1756 | write_stream, 1757 | self._mcp_server.create_initialization_options(), 1758 | ) 1759 | 1760 | async def run_sse_async(self) -> None: 1761 | """Run the server using SSE transport.""" 1762 | from starlette.applications import Starlette 1763 | from starlette.routing import Route 1764 | 1765 | sse = SseServerTransport("/messages") 1766 | 1767 | async def handle_sse(request): 1768 | async with sse.connect_sse( 1769 | request.scope, request.receive, request._send 1770 | ) as streams: 1771 | await self._mcp_server.run( 1772 | streams[0], 1773 | streams[1], 1774 | self._mcp_server.create_initialization_options(), 1775 | ) 1776 | 1777 | async def handle_messages(request): 1778 | await sse.handle_post_message(request.scope, request.receive, request._send) 1779 | 1780 | starlette_app = Starlette( 1781 | debug=self.settings.debug, 1782 | routes=[ 1783 | Route("/sse", endpoint=handle_sse), 1784 | Route("/messages", endpoint=handle_messages, methods=["POST"]), 1785 | ], 1786 | ) 1787 | 1788 | config = uvicorn.Config( 1789 | starlette_app, 1790 | host=self.settings.host, 1791 | port=self.settings.port, 1792 | log_level=self.settings.log_level.lower(), 1793 | ) 1794 | server = uvicorn.Server(config) 1795 | await server.serve() 1796 | 1797 | async def list_prompts(self) -> list[MCPPrompt]: 1798 | """List all available prompts.""" 1799 | prompts = self._prompt_manager.list_prompts() 1800 | return [ 1801 | MCPPrompt( 1802 | name=prompt.name, 1803 | description=prompt.description, 1804 | arguments=[ 1805 | MCPPromptArgument( 1806 | name=arg.name, 1807 | description=arg.description, 1808 | required=arg.required, 1809 | ) 1810 | for arg in (prompt.arguments or []) 1811 | ], 1812 | ) 1813 | for prompt in prompts 1814 | ] 1815 | 1816 | async def get_prompt( 1817 | self, name: str, arguments: Dict[str, Any] | None = None 1818 | ) -> GetPromptResult: 1819 | """Get a prompt by name with arguments.""" 1820 | try: 1821 | messages = await self._prompt_manager.render_prompt(name, arguments) 1822 | 1823 | return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages)) 1824 | except Exception as e: 1825 | logger.error(f"Error getting prompt {name}: {e}") 1826 | raise ValueError(str(e)) 1827 | 1828 | 1829 | def _convert_to_content( 1830 | result: Any, 1831 | ) -> Sequence[TextContent | ImageContent | EmbeddedResource]: 1832 | """Convert a result to a sequence of content objects.""" 1833 | if result is None: 1834 | return [] 1835 | 1836 | if isinstance(result, (TextContent, ImageContent, EmbeddedResource)): 1837 | return [result] 1838 | 1839 | if isinstance(result, Image): 1840 | return [result.to_image_content()] 1841 | 1842 | if isinstance(result, (list, tuple)): 1843 | return list(chain.from_iterable(_convert_to_content(item) for item in result)) 1844 | 1845 | if not isinstance(result, str): 1846 | try: 1847 | result = json.dumps(pydantic_core.to_jsonable_python(result)) 1848 | except Exception: 1849 | result = str(result) 1850 | 1851 | return [TextContent(type="text", text=result)] 1852 | 1853 | 1854 | class Context(BaseModel): 1855 | """Context object providing access to MCP capabilities. 1856 | 1857 | This provides a cleaner interface to MCP's RequestContext functionality. 1858 | It gets injected into tool and resource functions that request it via type hints. 1859 | 1860 | To use context in a tool function, add a parameter with the Context type annotation: 1861 | 1862 | ```python 1863 | @server.tool() 1864 | def my_tool(x: int, ctx: Context) -> str: 1865 | # Log messages to the client 1866 | ctx.info(f"Processing {x}") 1867 | ctx.debug("Debug info") 1868 | ctx.warning("Warning message") 1869 | ctx.error("Error message") 1870 | 1871 | # Report progress 1872 | ctx.report_progress(50, 100) 1873 | 1874 | # Access resources 1875 | data = ctx.read_resource("resource://data") 1876 | 1877 | # Get request info 1878 | request_id = ctx.request_id 1879 | client_id = ctx.client_id 1880 | 1881 | return str(x) 1882 | ``` 1883 | 1884 | The context parameter name can be anything as long as it's annotated with Context. 1885 | The context is optional - tools that don't need it can omit the parameter. 1886 | """ 1887 | 1888 | _request_context: RequestContext | None 1889 | _fastmcp: FastMCP | None 1890 | 1891 | def __init__( 1892 | self, 1893 | *, 1894 | request_context: RequestContext | None = None, 1895 | fastmcp: FastMCP | None = None, 1896 | **kwargs: Any, 1897 | ): 1898 | super().__init__(**kwargs) 1899 | self._request_context = request_context 1900 | self._fastmcp = fastmcp 1901 | 1902 | @property 1903 | def fastmcp(self) -> FastMCP: 1904 | """Access to the FastMCP server.""" 1905 | if self._fastmcp is None: 1906 | raise ValueError("Context is not available outside of a request") 1907 | return self._fastmcp 1908 | 1909 | @property 1910 | def request_context(self) -> RequestContext: 1911 | """Access to the underlying request context.""" 1912 | if self._request_context is None: 1913 | raise ValueError("Context is not available outside of a request") 1914 | return self._request_context 1915 | 1916 | async def report_progress( 1917 | self, progress: float, total: float | None = None 1918 | ) -> None: 1919 | """Report progress for the current operation. 1920 | 1921 | Args: 1922 | progress: Current progress value e.g. 24 1923 | total: Optional total value e.g. 100 1924 | """ 1925 | 1926 | progress_token = ( 1927 | self.request_context.meta.progressToken 1928 | if self.request_context.meta 1929 | else None 1930 | ) 1931 | 1932 | if not progress_token: 1933 | return 1934 | 1935 | await self.request_context.session.send_progress_notification( 1936 | progress_token=progress_token, progress=progress, total=total 1937 | ) 1938 | 1939 | async def read_resource(self, uri: str | AnyUrl) -> str | bytes: 1940 | """Read a resource by URI. 1941 | 1942 | Args: 1943 | uri: Resource URI to read 1944 | 1945 | Returns: 1946 | The resource content as either text or bytes 1947 | """ 1948 | assert ( 1949 | self._fastmcp is not None 1950 | ), "Context is not available outside of a request" 1951 | return await self._fastmcp.read_resource(uri) 1952 | 1953 | def log( 1954 | self, 1955 | level: Literal["debug", "info", "warning", "error"], 1956 | message: str, 1957 | *, 1958 | logger_name: str | None = None, 1959 | ) -> None: 1960 | """Send a log message to the client. 1961 | 1962 | Args: 1963 | level: Log level (debug, info, warning, error) 1964 | message: Log message 1965 | logger_name: Optional logger name 1966 | **extra: Additional structured data to include 1967 | """ 1968 | self.request_context.session.send_log_message( 1969 | level=level, data=message, logger=logger_name 1970 | ) 1971 | 1972 | @property 1973 | def client_id(self) -> str | None: 1974 | """Get the client ID if available.""" 1975 | return ( 1976 | getattr(self.request_context.meta, "client_id", None) 1977 | if self.request_context.meta 1978 | else None 1979 | ) 1980 | 1981 | @property 1982 | def request_id(self) -> str: 1983 | """Get the unique ID for this request.""" 1984 | return str(self.request_context.request_id) 1985 | 1986 | @property 1987 | def session(self): 1988 | """Access to the underlying session for advanced usage.""" 1989 | return self.request_context.session 1990 | 1991 | # Convenience methods for common log levels 1992 | def debug(self, message: str, **extra: Any) -> None: 1993 | """Send a debug log message.""" 1994 | self.log("debug", message, **extra) 1995 | 1996 | def info(self, message: str, **extra: Any) -> None: 1997 | """Send an info log message.""" 1998 | self.log("info", message, **extra) 1999 | 2000 | def warning(self, message: str, **extra: Any) -> None: 2001 | """Send a warning log message.""" 2002 | self.log("warning", message, **extra) 2003 | 2004 | def error(self, message: str, **extra: Any) -> None: 2005 | """Send an error log message.""" 2006 | self.log("error", message, **extra) 2007 | 2008 | 2009 | ================================================ 2010 | File: /src/fastmcp/tools/tool_manager.py 2011 | ================================================ 2012 | from fastmcp.exceptions import ToolError 2013 | 2014 | from fastmcp.tools.base import Tool 2015 | 2016 | 2017 | from typing import Any, Callable, Dict, Optional, TYPE_CHECKING 2018 | 2019 | from fastmcp.utilities.logging import get_logger 2020 | 2021 | if TYPE_CHECKING: 2022 | from fastmcp.server import Context 2023 | 2024 | logger = get_logger(__name__) 2025 | 2026 | 2027 | class ToolManager: 2028 | """Manages FastMCP tools.""" 2029 | 2030 | def __init__(self, warn_on_duplicate_tools: bool = True): 2031 | self._tools: Dict[str, Tool] = {} 2032 | self.warn_on_duplicate_tools = warn_on_duplicate_tools 2033 | 2034 | def get_tool(self, name: str) -> Optional[Tool]: 2035 | """Get tool by name.""" 2036 | return self._tools.get(name) 2037 | 2038 | def list_tools(self) -> list[Tool]: 2039 | """List all registered tools.""" 2040 | return list(self._tools.values()) 2041 | 2042 | def add_tool( 2043 | self, 2044 | fn: Callable, 2045 | name: Optional[str] = None, 2046 | description: Optional[str] = None, 2047 | ) -> Tool: 2048 | """Add a tool to the server.""" 2049 | tool = Tool.from_function(fn, name=name, description=description) 2050 | existing = self._tools.get(tool.name) 2051 | if existing: 2052 | if self.warn_on_duplicate_tools: 2053 | logger.warning(f"Tool already exists: {tool.name}") 2054 | return existing 2055 | self._tools[tool.name] = tool 2056 | return tool 2057 | 2058 | async def call_tool( 2059 | self, name: str, arguments: dict, context: Optional["Context"] = None 2060 | ) -> Any: 2061 | """Call a tool by name with arguments.""" 2062 | tool = self.get_tool(name) 2063 | if not tool: 2064 | raise ToolError(f"Unknown tool: {name}") 2065 | 2066 | return await tool.run(arguments, context=context) 2067 | 2068 | 2069 | ================================================ 2070 | File: /src/fastmcp/tools/__init__.py 2071 | ================================================ 2072 | from .base import Tool 2073 | from .tool_manager import ToolManager 2074 | 2075 | __all__ = ["Tool", "ToolManager"] 2076 | 2077 | 2078 | ================================================ 2079 | File: /src/fastmcp/tools/base.py 2080 | ================================================ 2081 | import fastmcp 2082 | from fastmcp.exceptions import ToolError 2083 | 2084 | from fastmcp.utilities.func_metadata import func_metadata, FuncMetadata 2085 | from pydantic import BaseModel, Field 2086 | 2087 | 2088 | import inspect 2089 | from typing import TYPE_CHECKING, Any, Callable, Optional 2090 | 2091 | if TYPE_CHECKING: 2092 | from fastmcp.server import Context 2093 | 2094 | 2095 | class Tool(BaseModel): 2096 | """Internal tool registration info.""" 2097 | 2098 | fn: Callable = Field(exclude=True) 2099 | name: str = Field(description="Name of the tool") 2100 | description: str = Field(description="Description of what the tool does") 2101 | parameters: dict = Field(description="JSON schema for tool parameters") 2102 | fn_metadata: FuncMetadata = Field( 2103 | description="Metadata about the function including a pydantic model for tool arguments" 2104 | ) 2105 | is_async: bool = Field(description="Whether the tool is async") 2106 | context_kwarg: Optional[str] = Field( 2107 | None, description="Name of the kwarg that should receive context" 2108 | ) 2109 | 2110 | @classmethod 2111 | def from_function( 2112 | cls, 2113 | fn: Callable, 2114 | name: Optional[str] = None, 2115 | description: Optional[str] = None, 2116 | context_kwarg: Optional[str] = None, 2117 | ) -> "Tool": 2118 | """Create a Tool from a function.""" 2119 | func_name = name or fn.__name__ 2120 | 2121 | if func_name == "": 2122 | raise ValueError("You must provide a name for lambda functions") 2123 | 2124 | func_doc = description or fn.__doc__ or "" 2125 | is_async = inspect.iscoroutinefunction(fn) 2126 | 2127 | # Find context parameter if it exists 2128 | if context_kwarg is None: 2129 | sig = inspect.signature(fn) 2130 | for param_name, param in sig.parameters.items(): 2131 | if param.annotation is fastmcp.Context: 2132 | context_kwarg = param_name 2133 | break 2134 | 2135 | func_arg_metadata = func_metadata( 2136 | fn, 2137 | skip_names=[context_kwarg] if context_kwarg is not None else [], 2138 | ) 2139 | parameters = func_arg_metadata.arg_model.model_json_schema() 2140 | 2141 | return cls( 2142 | fn=fn, 2143 | name=func_name, 2144 | description=func_doc, 2145 | parameters=parameters, 2146 | fn_metadata=func_arg_metadata, 2147 | is_async=is_async, 2148 | context_kwarg=context_kwarg, 2149 | ) 2150 | 2151 | async def run(self, arguments: dict, context: Optional["Context"] = None) -> Any: 2152 | """Run the tool with arguments.""" 2153 | try: 2154 | return await self.fn_metadata.call_fn_with_arg_validation( 2155 | self.fn, 2156 | self.is_async, 2157 | arguments, 2158 | {self.context_kwarg: context} 2159 | if self.context_kwarg is not None 2160 | else None, 2161 | ) 2162 | except Exception as e: 2163 | raise ToolError(f"Error executing tool {self.name}: {e}") from e 2164 | 2165 | 2166 | ================================================ 2167 | File: /src/fastmcp/resources/resource_manager.py 2168 | ================================================ 2169 | """Resource manager functionality.""" 2170 | 2171 | from typing import Callable, Dict, Optional, Union 2172 | 2173 | from pydantic import AnyUrl 2174 | 2175 | from fastmcp.resources.base import Resource 2176 | from fastmcp.resources.templates import ResourceTemplate 2177 | from fastmcp.utilities.logging import get_logger 2178 | 2179 | logger = get_logger(__name__) 2180 | 2181 | 2182 | class ResourceManager: 2183 | """Manages FastMCP resources.""" 2184 | 2185 | def __init__(self, warn_on_duplicate_resources: bool = True): 2186 | self._resources: Dict[str, Resource] = {} 2187 | self._templates: Dict[str, ResourceTemplate] = {} 2188 | self.warn_on_duplicate_resources = warn_on_duplicate_resources 2189 | 2190 | def add_resource(self, resource: Resource) -> Resource: 2191 | """Add a resource to the manager. 2192 | 2193 | Args: 2194 | resource: A Resource instance to add 2195 | 2196 | Returns: 2197 | The added resource. If a resource with the same URI already exists, 2198 | returns the existing resource. 2199 | """ 2200 | logger.debug( 2201 | "Adding resource", 2202 | extra={ 2203 | "uri": resource.uri, 2204 | "type": type(resource).__name__, 2205 | "name": resource.name, 2206 | }, 2207 | ) 2208 | existing = self._resources.get(str(resource.uri)) 2209 | if existing: 2210 | if self.warn_on_duplicate_resources: 2211 | logger.warning(f"Resource already exists: {resource.uri}") 2212 | return existing 2213 | self._resources[str(resource.uri)] = resource 2214 | return resource 2215 | 2216 | def add_template( 2217 | self, 2218 | fn: Callable, 2219 | uri_template: str, 2220 | name: Optional[str] = None, 2221 | description: Optional[str] = None, 2222 | mime_type: Optional[str] = None, 2223 | ) -> ResourceTemplate: 2224 | """Add a template from a function.""" 2225 | template = ResourceTemplate.from_function( 2226 | fn, 2227 | uri_template=uri_template, 2228 | name=name, 2229 | description=description, 2230 | mime_type=mime_type, 2231 | ) 2232 | self._templates[template.uri_template] = template 2233 | return template 2234 | 2235 | async def get_resource(self, uri: Union[AnyUrl, str]) -> Optional[Resource]: 2236 | """Get resource by URI, checking concrete resources first, then templates.""" 2237 | uri_str = str(uri) 2238 | logger.debug("Getting resource", extra={"uri": uri_str}) 2239 | 2240 | # First check concrete resources 2241 | if resource := self._resources.get(uri_str): 2242 | return resource 2243 | 2244 | # Then check templates 2245 | for template in self._templates.values(): 2246 | if params := template.matches(uri_str): 2247 | try: 2248 | return await template.create_resource(uri_str, params) 2249 | except Exception as e: 2250 | raise ValueError(f"Error creating resource from template: {e}") 2251 | 2252 | raise ValueError(f"Unknown resource: {uri}") 2253 | 2254 | def list_resources(self) -> list[Resource]: 2255 | """List all registered resources.""" 2256 | logger.debug("Listing resources", extra={"count": len(self._resources)}) 2257 | return list(self._resources.values()) 2258 | 2259 | def list_templates(self) -> list[ResourceTemplate]: 2260 | """List all registered templates.""" 2261 | logger.debug("Listing templates", extra={"count": len(self._templates)}) 2262 | return list(self._templates.values()) 2263 | 2264 | 2265 | ================================================ 2266 | File: /src/fastmcp/resources/__init__.py 2267 | ================================================ 2268 | from .base import Resource 2269 | from .types import ( 2270 | TextResource, 2271 | BinaryResource, 2272 | FunctionResource, 2273 | FileResource, 2274 | HttpResource, 2275 | DirectoryResource, 2276 | ) 2277 | from .templates import ResourceTemplate 2278 | from .resource_manager import ResourceManager 2279 | 2280 | __all__ = [ 2281 | "Resource", 2282 | "TextResource", 2283 | "BinaryResource", 2284 | "FunctionResource", 2285 | "FileResource", 2286 | "HttpResource", 2287 | "DirectoryResource", 2288 | "ResourceTemplate", 2289 | "ResourceManager", 2290 | ] 2291 | 2292 | 2293 | ================================================ 2294 | File: /src/fastmcp/resources/types.py 2295 | ================================================ 2296 | """Concrete resource implementations.""" 2297 | 2298 | import asyncio 2299 | import json 2300 | from pathlib import Path 2301 | from typing import Any, Callable, Union 2302 | 2303 | import httpx 2304 | import pydantic.json 2305 | import pydantic_core 2306 | from pydantic import Field, ValidationInfo 2307 | 2308 | from fastmcp.resources.base import Resource 2309 | 2310 | 2311 | class TextResource(Resource): 2312 | """A resource that reads from a string.""" 2313 | 2314 | text: str = Field(description="Text content of the resource") 2315 | 2316 | async def read(self) -> str: 2317 | """Read the text content.""" 2318 | return self.text 2319 | 2320 | 2321 | class BinaryResource(Resource): 2322 | """A resource that reads from bytes.""" 2323 | 2324 | data: bytes = Field(description="Binary content of the resource") 2325 | 2326 | async def read(self) -> bytes: 2327 | """Read the binary content.""" 2328 | return self.data 2329 | 2330 | 2331 | class FunctionResource(Resource): 2332 | """A resource that defers data loading by wrapping a function. 2333 | 2334 | The function is only called when the resource is read, allowing for lazy loading 2335 | of potentially expensive data. This is particularly useful when listing resources, 2336 | as the function won't be called until the resource is actually accessed. 2337 | 2338 | The function can return: 2339 | - str for text content (default) 2340 | - bytes for binary content 2341 | - other types will be converted to JSON 2342 | """ 2343 | 2344 | fn: Callable[[], Any] = Field(exclude=True) 2345 | 2346 | async def read(self) -> Union[str, bytes]: 2347 | """Read the resource by calling the wrapped function.""" 2348 | try: 2349 | result = self.fn() 2350 | if isinstance(result, Resource): 2351 | return await result.read() 2352 | if isinstance(result, bytes): 2353 | return result 2354 | if isinstance(result, str): 2355 | return result 2356 | try: 2357 | return json.dumps(pydantic_core.to_jsonable_python(result)) 2358 | except (TypeError, pydantic_core.PydanticSerializationError): 2359 | # If JSON serialization fails, try str() 2360 | return str(result) 2361 | except Exception as e: 2362 | raise ValueError(f"Error reading resource {self.uri}: {e}") 2363 | 2364 | 2365 | class FileResource(Resource): 2366 | """A resource that reads from a file. 2367 | 2368 | Set is_binary=True to read file as binary data instead of text. 2369 | """ 2370 | 2371 | path: Path = Field(description="Path to the file") 2372 | is_binary: bool = Field( 2373 | default=False, 2374 | description="Whether to read the file as binary data", 2375 | ) 2376 | mime_type: str = Field( 2377 | default="text/plain", 2378 | description="MIME type of the resource content", 2379 | ) 2380 | 2381 | @pydantic.field_validator("path") 2382 | @classmethod 2383 | def validate_absolute_path(cls, path: Path) -> Path: 2384 | """Ensure path is absolute.""" 2385 | if not path.is_absolute(): 2386 | raise ValueError("Path must be absolute") 2387 | return path 2388 | 2389 | @pydantic.field_validator("is_binary") 2390 | @classmethod 2391 | def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool: 2392 | """Set is_binary based on mime_type if not explicitly set.""" 2393 | if is_binary: 2394 | return True 2395 | mime_type = info.data.get("mime_type", "text/plain") 2396 | return not mime_type.startswith("text/") 2397 | 2398 | async def read(self) -> Union[str, bytes]: 2399 | """Read the file content.""" 2400 | try: 2401 | if self.is_binary: 2402 | return await asyncio.to_thread(self.path.read_bytes) 2403 | return await asyncio.to_thread(self.path.read_text) 2404 | except Exception as e: 2405 | raise ValueError(f"Error reading file {self.path}: {e}") 2406 | 2407 | 2408 | class HttpResource(Resource): 2409 | """A resource that reads from an HTTP endpoint.""" 2410 | 2411 | url: str = Field(description="URL to fetch content from") 2412 | mime_type: str | None = Field( 2413 | default="application/json", description="MIME type of the resource content" 2414 | ) 2415 | 2416 | async def read(self) -> Union[str, bytes]: 2417 | """Read the HTTP content.""" 2418 | async with httpx.AsyncClient() as client: 2419 | response = await client.get(self.url) 2420 | response.raise_for_status() 2421 | return response.text 2422 | 2423 | 2424 | class DirectoryResource(Resource): 2425 | """A resource that lists files in a directory.""" 2426 | 2427 | path: Path = Field(description="Path to the directory") 2428 | recursive: bool = Field( 2429 | default=False, description="Whether to list files recursively" 2430 | ) 2431 | pattern: str | None = Field( 2432 | default=None, description="Optional glob pattern to filter files" 2433 | ) 2434 | mime_type: str | None = Field( 2435 | default="application/json", description="MIME type of the resource content" 2436 | ) 2437 | 2438 | @pydantic.field_validator("path") 2439 | @classmethod 2440 | def validate_absolute_path(cls, path: Path) -> Path: 2441 | """Ensure path is absolute.""" 2442 | if not path.is_absolute(): 2443 | raise ValueError("Path must be absolute") 2444 | return path 2445 | 2446 | def list_files(self) -> list[Path]: 2447 | """List files in the directory.""" 2448 | if not self.path.exists(): 2449 | raise FileNotFoundError(f"Directory not found: {self.path}") 2450 | if not self.path.is_dir(): 2451 | raise NotADirectoryError(f"Not a directory: {self.path}") 2452 | 2453 | try: 2454 | if self.pattern: 2455 | return ( 2456 | list(self.path.glob(self.pattern)) 2457 | if not self.recursive 2458 | else list(self.path.rglob(self.pattern)) 2459 | ) 2460 | return ( 2461 | list(self.path.glob("*")) 2462 | if not self.recursive 2463 | else list(self.path.rglob("*")) 2464 | ) 2465 | except Exception as e: 2466 | raise ValueError(f"Error listing directory {self.path}: {e}") 2467 | 2468 | async def read(self) -> str: # Always returns JSON string 2469 | """Read the directory listing.""" 2470 | try: 2471 | files = await asyncio.to_thread(self.list_files) 2472 | file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()] 2473 | return json.dumps({"files": file_list}, indent=2) 2474 | except Exception as e: 2475 | raise ValueError(f"Error reading directory {self.path}: {e}") 2476 | 2477 | 2478 | ================================================ 2479 | File: /src/fastmcp/resources/templates.py 2480 | ================================================ 2481 | """Resource template functionality.""" 2482 | 2483 | import inspect 2484 | import re 2485 | from typing import Any, Callable, Dict, Optional 2486 | 2487 | from pydantic import BaseModel, Field, TypeAdapter, validate_call 2488 | 2489 | from fastmcp.resources.types import FunctionResource, Resource 2490 | 2491 | 2492 | class ResourceTemplate(BaseModel): 2493 | """A template for dynamically creating resources.""" 2494 | 2495 | uri_template: str = Field( 2496 | description="URI template with parameters (e.g. weather://{city}/current)" 2497 | ) 2498 | name: str = Field(description="Name of the resource") 2499 | description: str | None = Field(description="Description of what the resource does") 2500 | mime_type: str = Field( 2501 | default="text/plain", description="MIME type of the resource content" 2502 | ) 2503 | fn: Callable = Field(exclude=True) 2504 | parameters: dict = Field(description="JSON schema for function parameters") 2505 | 2506 | @classmethod 2507 | def from_function( 2508 | cls, 2509 | fn: Callable, 2510 | uri_template: str, 2511 | name: Optional[str] = None, 2512 | description: Optional[str] = None, 2513 | mime_type: Optional[str] = None, 2514 | ) -> "ResourceTemplate": 2515 | """Create a template from a function.""" 2516 | func_name = name or fn.__name__ 2517 | if func_name == "": 2518 | raise ValueError("You must provide a name for lambda functions") 2519 | 2520 | # Get schema from TypeAdapter - will fail if function isn't properly typed 2521 | parameters = TypeAdapter(fn).json_schema() 2522 | 2523 | # ensure the arguments are properly cast 2524 | fn = validate_call(fn) 2525 | 2526 | return cls( 2527 | uri_template=uri_template, 2528 | name=func_name, 2529 | description=description or fn.__doc__ or "", 2530 | mime_type=mime_type or "text/plain", 2531 | fn=fn, 2532 | parameters=parameters, 2533 | ) 2534 | 2535 | def matches(self, uri: str) -> Optional[Dict[str, Any]]: 2536 | """Check if URI matches template and extract parameters.""" 2537 | # Convert template to regex pattern 2538 | pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)") 2539 | match = re.match(f"^{pattern}$", uri) 2540 | if match: 2541 | return match.groupdict() 2542 | return None 2543 | 2544 | async def create_resource(self, uri: str, params: Dict[str, Any]) -> Resource: 2545 | """Create a resource from the template with the given parameters.""" 2546 | try: 2547 | # Call function and check if result is a coroutine 2548 | result = self.fn(**params) 2549 | if inspect.iscoroutine(result): 2550 | result = await result 2551 | 2552 | return FunctionResource( 2553 | uri=uri, # type: ignore 2554 | name=self.name, 2555 | description=self.description, 2556 | mime_type=self.mime_type, 2557 | fn=lambda: result, # Capture result in closure 2558 | ) 2559 | except Exception as e: 2560 | raise ValueError(f"Error creating resource from template: {e}") 2561 | 2562 | 2563 | ================================================ 2564 | File: /src/fastmcp/resources/base.py 2565 | ================================================ 2566 | """Base classes and interfaces for FastMCP resources.""" 2567 | 2568 | import abc 2569 | from typing import Union, Annotated 2570 | 2571 | from pydantic import ( 2572 | AnyUrl, 2573 | BaseModel, 2574 | ConfigDict, 2575 | Field, 2576 | UrlConstraints, 2577 | ValidationInfo, 2578 | field_validator, 2579 | ) 2580 | 2581 | 2582 | class Resource(BaseModel, abc.ABC): 2583 | """Base class for all resources.""" 2584 | 2585 | model_config = ConfigDict(validate_default=True) 2586 | 2587 | uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field( 2588 | default=..., description="URI of the resource" 2589 | ) 2590 | name: str | None = Field(description="Name of the resource", default=None) 2591 | description: str | None = Field( 2592 | description="Description of the resource", default=None 2593 | ) 2594 | mime_type: str = Field( 2595 | default="text/plain", 2596 | description="MIME type of the resource content", 2597 | pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$", 2598 | ) 2599 | 2600 | @field_validator("name", mode="before") 2601 | @classmethod 2602 | def set_default_name(cls, name: str | None, info: ValidationInfo) -> str: 2603 | """Set default name from URI if not provided.""" 2604 | if name: 2605 | return name 2606 | if uri := info.data.get("uri"): 2607 | return str(uri) 2608 | raise ValueError("Either name or uri must be provided") 2609 | 2610 | @abc.abstractmethod 2611 | async def read(self) -> Union[str, bytes]: 2612 | """Read the resource content.""" 2613 | pass 2614 | 2615 | 2616 | ================================================ 2617 | File: /src/fastmcp/__init__.py 2618 | ================================================ 2619 | """FastMCP - A more ergonomic interface for MCP servers.""" 2620 | 2621 | from importlib.metadata import version 2622 | from .server import FastMCP, Context 2623 | from .utilities.types import Image 2624 | 2625 | __version__ = version("fastmcp") 2626 | __all__ = ["FastMCP", "Context", "Image"] 2627 | 2628 | 2629 | ================================================ 2630 | File: /src/fastmcp/cli/claude.py 2631 | ================================================ 2632 | """Claude app integration utilities.""" 2633 | 2634 | import json 2635 | import sys 2636 | from pathlib import Path 2637 | from typing import Optional, Dict 2638 | 2639 | from ..utilities.logging import get_logger 2640 | 2641 | logger = get_logger(__name__) 2642 | 2643 | 2644 | def get_claude_config_path() -> Path | None: 2645 | """Get the Claude config directory based on platform.""" 2646 | if sys.platform == "win32": 2647 | path = Path(Path.home(), "AppData", "Roaming", "Claude") 2648 | elif sys.platform == "darwin": 2649 | path = Path(Path.home(), "Library", "Application Support", "Claude") 2650 | else: 2651 | return None 2652 | 2653 | if path.exists(): 2654 | return path 2655 | return None 2656 | 2657 | 2658 | def update_claude_config( 2659 | file_spec: str, 2660 | server_name: str, 2661 | *, 2662 | with_editable: Optional[Path] = None, 2663 | with_packages: Optional[list[str]] = None, 2664 | env_vars: Optional[Dict[str, str]] = None, 2665 | ) -> bool: 2666 | """Add or update a FastMCP server in Claude's configuration. 2667 | 2668 | Args: 2669 | file_spec: Path to the server file, optionally with :object suffix 2670 | server_name: Name for the server in Claude's config 2671 | with_editable: Optional directory to install in editable mode 2672 | with_packages: Optional list of additional packages to install 2673 | env_vars: Optional dictionary of environment variables. These are merged with 2674 | any existing variables, with new values taking precedence. 2675 | 2676 | Raises: 2677 | RuntimeError: If Claude Desktop's config directory is not found, indicating 2678 | Claude Desktop may not be installed or properly set up. 2679 | """ 2680 | config_dir = get_claude_config_path() 2681 | if not config_dir: 2682 | raise RuntimeError( 2683 | "Claude Desktop config directory not found. Please ensure Claude Desktop " 2684 | "is installed and has been run at least once to initialize its configuration." 2685 | ) 2686 | 2687 | config_file = config_dir / "claude_desktop_config.json" 2688 | if not config_file.exists(): 2689 | try: 2690 | config_file.write_text("{}") 2691 | except Exception as e: 2692 | logger.error( 2693 | "Failed to create Claude config file", 2694 | extra={ 2695 | "error": str(e), 2696 | "config_file": str(config_file), 2697 | }, 2698 | ) 2699 | return False 2700 | 2701 | try: 2702 | config = json.loads(config_file.read_text()) 2703 | if "mcpServers" not in config: 2704 | config["mcpServers"] = {} 2705 | 2706 | # Always preserve existing env vars and merge with new ones 2707 | if ( 2708 | server_name in config["mcpServers"] 2709 | and "env" in config["mcpServers"][server_name] 2710 | ): 2711 | existing_env = config["mcpServers"][server_name]["env"] 2712 | if env_vars: 2713 | # New vars take precedence over existing ones 2714 | env_vars = {**existing_env, **env_vars} 2715 | else: 2716 | env_vars = existing_env 2717 | 2718 | # Build uv run command 2719 | args = ["run"] 2720 | 2721 | # Collect all packages in a set to deduplicate 2722 | packages = {"fastmcp"} 2723 | if with_packages: 2724 | packages.update(pkg for pkg in with_packages if pkg) 2725 | 2726 | # Add all packages with --with 2727 | for pkg in sorted(packages): 2728 | args.extend(["--with", pkg]) 2729 | 2730 | if with_editable: 2731 | args.extend(["--with-editable", str(with_editable)]) 2732 | 2733 | # Convert file path to absolute before adding to command 2734 | # Split off any :object suffix first 2735 | if ":" in file_spec: 2736 | file_path, server_object = file_spec.rsplit(":", 1) 2737 | file_spec = f"{Path(file_path).resolve()}:{server_object}" 2738 | else: 2739 | file_spec = str(Path(file_spec).resolve()) 2740 | 2741 | # Add fastmcp run command 2742 | args.extend(["fastmcp", "run", file_spec]) 2743 | 2744 | server_config = { 2745 | "command": "uv", 2746 | "args": args, 2747 | } 2748 | 2749 | # Add environment variables if specified 2750 | if env_vars: 2751 | server_config["env"] = env_vars 2752 | 2753 | config["mcpServers"][server_name] = server_config 2754 | 2755 | config_file.write_text(json.dumps(config, indent=2)) 2756 | logger.info( 2757 | f"Added server '{server_name}' to Claude config", 2758 | extra={"config_file": str(config_file)}, 2759 | ) 2760 | return True 2761 | except Exception as e: 2762 | logger.error( 2763 | "Failed to update Claude config", 2764 | extra={ 2765 | "error": str(e), 2766 | "config_file": str(config_file), 2767 | }, 2768 | ) 2769 | return False 2770 | 2771 | 2772 | ================================================ 2773 | File: /src/fastmcp/cli/__init__.py 2774 | ================================================ 2775 | """FastMCP CLI package.""" 2776 | 2777 | from .cli import app 2778 | 2779 | 2780 | if __name__ == "__main__": 2781 | app() 2782 | 2783 | 2784 | ================================================ 2785 | File: /src/fastmcp/cli/cli.py 2786 | ================================================ 2787 | """FastMCP CLI tools.""" 2788 | 2789 | import importlib.metadata 2790 | import importlib.util 2791 | import os 2792 | import subprocess 2793 | import sys 2794 | from pathlib import Path 2795 | from typing import Dict, Optional, Tuple 2796 | 2797 | import dotenv 2798 | import typer 2799 | from typing_extensions import Annotated 2800 | 2801 | from fastmcp.cli import claude 2802 | from fastmcp.utilities.logging import get_logger 2803 | 2804 | logger = get_logger("cli") 2805 | 2806 | app = typer.Typer( 2807 | name="fastmcp", 2808 | help="FastMCP development tools", 2809 | add_completion=False, 2810 | no_args_is_help=True, # Show help if no args provided 2811 | ) 2812 | 2813 | 2814 | def _get_npx_command(): 2815 | """Get the correct npx command for the current platform.""" 2816 | if sys.platform == "win32": 2817 | # Try both npx.cmd and npx.exe on Windows 2818 | for cmd in ["npx.cmd", "npx.exe", "npx"]: 2819 | try: 2820 | subprocess.run( 2821 | [cmd, "--version"], check=True, capture_output=True, shell=True 2822 | ) 2823 | return cmd 2824 | except subprocess.CalledProcessError: 2825 | continue 2826 | return None 2827 | return "npx" # On Unix-like systems, just use npx 2828 | 2829 | 2830 | def _parse_env_var(env_var: str) -> Tuple[str, str]: 2831 | """Parse environment variable string in format KEY=VALUE.""" 2832 | if "=" not in env_var: 2833 | logger.error( 2834 | f"Invalid environment variable format: {env_var}. Must be KEY=VALUE" 2835 | ) 2836 | sys.exit(1) 2837 | key, value = env_var.split("=", 1) 2838 | return key.strip(), value.strip() 2839 | 2840 | 2841 | def _build_uv_command( 2842 | file_spec: str, 2843 | with_editable: Optional[Path] = None, 2844 | with_packages: Optional[list[str]] = None, 2845 | ) -> list[str]: 2846 | """Build the uv run command that runs a FastMCP server through fastmcp run.""" 2847 | cmd = ["uv"] 2848 | 2849 | cmd.extend(["run", "--with", "fastmcp"]) 2850 | 2851 | if with_editable: 2852 | cmd.extend(["--with-editable", str(with_editable)]) 2853 | 2854 | if with_packages: 2855 | for pkg in with_packages: 2856 | if pkg: 2857 | cmd.extend(["--with", pkg]) 2858 | 2859 | # Add fastmcp run command 2860 | cmd.extend(["fastmcp", "run", file_spec]) 2861 | return cmd 2862 | 2863 | 2864 | def _parse_file_path(file_spec: str) -> Tuple[Path, Optional[str]]: 2865 | """Parse a file path that may include a server object specification. 2866 | 2867 | Args: 2868 | file_spec: Path to file, optionally with :object suffix 2869 | 2870 | Returns: 2871 | Tuple of (file_path, server_object) 2872 | """ 2873 | # First check if we have a Windows path (e.g., C:\...) 2874 | has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":" 2875 | 2876 | # Split on the last colon, but only if it's not part of the Windows drive letter 2877 | # and there's actually another colon in the string after the drive letter 2878 | if ":" in (file_spec[2:] if has_windows_drive else file_spec): 2879 | file_str, server_object = file_spec.rsplit(":", 1) 2880 | else: 2881 | file_str, server_object = file_spec, None 2882 | 2883 | # Resolve the file path 2884 | file_path = Path(file_str).expanduser().resolve() 2885 | if not file_path.exists(): 2886 | logger.error(f"File not found: {file_path}") 2887 | sys.exit(1) 2888 | if not file_path.is_file(): 2889 | logger.error(f"Not a file: {file_path}") 2890 | sys.exit(1) 2891 | 2892 | return file_path, server_object 2893 | 2894 | 2895 | def _import_server(file: Path, server_object: Optional[str] = None): 2896 | """Import a FastMCP server from a file. 2897 | 2898 | Args: 2899 | file: Path to the file 2900 | server_object: Optional object name in format "module:object" or just "object" 2901 | 2902 | Returns: 2903 | The server object 2904 | """ 2905 | # Add parent directory to Python path so imports can be resolved 2906 | file_dir = str(file.parent) 2907 | if file_dir not in sys.path: 2908 | sys.path.insert(0, file_dir) 2909 | 2910 | # Import the module 2911 | spec = importlib.util.spec_from_file_location("server_module", file) 2912 | if not spec or not spec.loader: 2913 | logger.error("Could not load module", extra={"file": str(file)}) 2914 | sys.exit(1) 2915 | 2916 | module = importlib.util.module_from_spec(spec) 2917 | spec.loader.exec_module(module) 2918 | 2919 | # If no object specified, try common server names 2920 | if not server_object: 2921 | # Look for the most common server object names 2922 | for name in ["mcp", "server", "app"]: 2923 | if hasattr(module, name): 2924 | return getattr(module, name) 2925 | 2926 | logger.error( 2927 | f"No server object found in {file}. Please either:\n" 2928 | "1. Use a standard variable name (mcp, server, or app)\n" 2929 | "2. Specify the object name with file:object syntax", 2930 | extra={"file": str(file)}, 2931 | ) 2932 | sys.exit(1) 2933 | 2934 | # Handle module:object syntax 2935 | if ":" in server_object: 2936 | module_name, object_name = server_object.split(":", 1) 2937 | try: 2938 | server_module = importlib.import_module(module_name) 2939 | server = getattr(server_module, object_name, None) 2940 | except ImportError: 2941 | logger.error( 2942 | f"Could not import module '{module_name}'", 2943 | extra={"file": str(file)}, 2944 | ) 2945 | sys.exit(1) 2946 | else: 2947 | # Just object name 2948 | server = getattr(module, server_object, None) 2949 | 2950 | if server is None: 2951 | logger.error( 2952 | f"Server object '{server_object}' not found", 2953 | extra={"file": str(file)}, 2954 | ) 2955 | sys.exit(1) 2956 | 2957 | return server 2958 | 2959 | 2960 | @app.command() 2961 | def version() -> None: 2962 | """Show the FastMCP version.""" 2963 | try: 2964 | version = importlib.metadata.version("fastmcp") 2965 | print(f"FastMCP version {version}") 2966 | except importlib.metadata.PackageNotFoundError: 2967 | print("FastMCP version unknown (package not installed)") 2968 | sys.exit(1) 2969 | 2970 | 2971 | @app.command() 2972 | def dev( 2973 | file_spec: str = typer.Argument( 2974 | ..., 2975 | help="Python file to run, optionally with :object suffix", 2976 | ), 2977 | with_editable: Annotated[ 2978 | Optional[Path], 2979 | typer.Option( 2980 | "--with-editable", 2981 | "-e", 2982 | help="Directory containing pyproject.toml to install in editable mode", 2983 | exists=True, 2984 | file_okay=False, 2985 | resolve_path=True, 2986 | ), 2987 | ] = None, 2988 | with_packages: Annotated[ 2989 | list[str], 2990 | typer.Option( 2991 | "--with", 2992 | help="Additional packages to install", 2993 | ), 2994 | ] = [], 2995 | ) -> None: 2996 | """Run a FastMCP server with the MCP Inspector.""" 2997 | file, server_object = _parse_file_path(file_spec) 2998 | 2999 | logger.debug( 3000 | "Starting dev server", 3001 | extra={ 3002 | "file": str(file), 3003 | "server_object": server_object, 3004 | "with_editable": str(with_editable) if with_editable else None, 3005 | "with_packages": with_packages, 3006 | }, 3007 | ) 3008 | 3009 | try: 3010 | # Import server to get dependencies 3011 | server = _import_server(file, server_object) 3012 | if hasattr(server, "dependencies"): 3013 | with_packages = list(set(with_packages + server.dependencies)) 3014 | 3015 | uv_cmd = _build_uv_command(file_spec, with_editable, with_packages) 3016 | 3017 | # Get the correct npx command 3018 | npx_cmd = _get_npx_command() 3019 | if not npx_cmd: 3020 | logger.error( 3021 | "npx not found. Please ensure Node.js and npm are properly installed " 3022 | "and added to your system PATH." 3023 | ) 3024 | sys.exit(1) 3025 | 3026 | # Run the MCP Inspector command with shell=True on Windows 3027 | shell = sys.platform == "win32" 3028 | process = subprocess.run( 3029 | [npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd, 3030 | check=True, 3031 | shell=shell, 3032 | env=dict(os.environ.items()), # Convert to list of tuples for env update 3033 | ) 3034 | sys.exit(process.returncode) 3035 | except subprocess.CalledProcessError as e: 3036 | logger.error( 3037 | "Dev server failed", 3038 | extra={ 3039 | "file": str(file), 3040 | "error": str(e), 3041 | "returncode": e.returncode, 3042 | }, 3043 | ) 3044 | sys.exit(e.returncode) 3045 | except FileNotFoundError: 3046 | logger.error( 3047 | "npx not found. Please ensure Node.js and npm are properly installed " 3048 | "and added to your system PATH. You may need to restart your terminal " 3049 | "after installation.", 3050 | extra={"file": str(file)}, 3051 | ) 3052 | sys.exit(1) 3053 | 3054 | 3055 | @app.command() 3056 | def run( 3057 | file_spec: str = typer.Argument( 3058 | ..., 3059 | help="Python file to run, optionally with :object suffix", 3060 | ), 3061 | transport: Annotated[ 3062 | Optional[str], 3063 | typer.Option( 3064 | "--transport", 3065 | "-t", 3066 | help="Transport protocol to use (stdio or sse)", 3067 | ), 3068 | ] = None, 3069 | ) -> None: 3070 | """Run a FastMCP server. 3071 | 3072 | The server can be specified in two ways: 3073 | 1. Module approach: server.py - runs the module directly, expecting a server.run() call 3074 | 2. Import approach: server.py:app - imports and runs the specified server object 3075 | 3076 | Note: This command runs the server directly. You are responsible for ensuring 3077 | all dependencies are available. For dependency management, use fastmcp install 3078 | or fastmcp dev instead. 3079 | """ 3080 | file, server_object = _parse_file_path(file_spec) 3081 | 3082 | logger.debug( 3083 | "Running server", 3084 | extra={ 3085 | "file": str(file), 3086 | "server_object": server_object, 3087 | "transport": transport, 3088 | }, 3089 | ) 3090 | 3091 | try: 3092 | # Import and get server object 3093 | server = _import_server(file, server_object) 3094 | 3095 | # Run the server 3096 | kwargs = {} 3097 | if transport: 3098 | kwargs["transport"] = transport 3099 | 3100 | server.run(**kwargs) 3101 | 3102 | except Exception as e: 3103 | logger.error( 3104 | f"Failed to run server: {e}", 3105 | extra={ 3106 | "file": str(file), 3107 | "error": str(e), 3108 | }, 3109 | ) 3110 | sys.exit(1) 3111 | 3112 | 3113 | @app.command() 3114 | def install( 3115 | file_spec: str = typer.Argument( 3116 | ..., 3117 | help="Python file to run, optionally with :object suffix", 3118 | ), 3119 | server_name: Annotated[ 3120 | Optional[str], 3121 | typer.Option( 3122 | "--name", 3123 | "-n", 3124 | help="Custom name for the server (defaults to server's name attribute or file name)", 3125 | ), 3126 | ] = None, 3127 | with_editable: Annotated[ 3128 | Optional[Path], 3129 | typer.Option( 3130 | "--with-editable", 3131 | "-e", 3132 | help="Directory containing pyproject.toml to install in editable mode", 3133 | exists=True, 3134 | file_okay=False, 3135 | resolve_path=True, 3136 | ), 3137 | ] = None, 3138 | with_packages: Annotated[ 3139 | list[str], 3140 | typer.Option( 3141 | "--with", 3142 | help="Additional packages to install", 3143 | ), 3144 | ] = [], 3145 | env_vars: Annotated[ 3146 | list[str], 3147 | typer.Option( 3148 | "--env-var", 3149 | "-e", 3150 | help="Environment variables in KEY=VALUE format", 3151 | ), 3152 | ] = [], 3153 | env_file: Annotated[ 3154 | Optional[Path], 3155 | typer.Option( 3156 | "--env-file", 3157 | "-f", 3158 | help="Load environment variables from a .env file", 3159 | exists=True, 3160 | file_okay=True, 3161 | dir_okay=False, 3162 | resolve_path=True, 3163 | ), 3164 | ] = None, 3165 | ) -> None: 3166 | """Install a FastMCP server in the Claude desktop app. 3167 | 3168 | Environment variables are preserved once added and only updated if new values 3169 | are explicitly provided. 3170 | """ 3171 | file, server_object = _parse_file_path(file_spec) 3172 | 3173 | logger.debug( 3174 | "Installing server", 3175 | extra={ 3176 | "file": str(file), 3177 | "server_name": server_name, 3178 | "server_object": server_object, 3179 | "with_editable": str(with_editable) if with_editable else None, 3180 | "with_packages": with_packages, 3181 | }, 3182 | ) 3183 | 3184 | if not claude.get_claude_config_path(): 3185 | logger.error("Claude app not found") 3186 | sys.exit(1) 3187 | 3188 | # Try to import server to get its name, but fall back to file name if dependencies missing 3189 | name = server_name 3190 | server = None 3191 | if not name: 3192 | try: 3193 | server = _import_server(file, server_object) 3194 | name = server.name 3195 | except (ImportError, ModuleNotFoundError) as e: 3196 | logger.debug( 3197 | "Could not import server (likely missing dependencies), using file name", 3198 | extra={"error": str(e)}, 3199 | ) 3200 | name = file.stem 3201 | 3202 | # Get server dependencies if available 3203 | server_dependencies = getattr(server, "dependencies", []) if server else [] 3204 | if server_dependencies: 3205 | with_packages = list(set(with_packages + server_dependencies)) 3206 | 3207 | # Process environment variables if provided 3208 | env_dict: Optional[Dict[str, str]] = None 3209 | if env_file or env_vars: 3210 | env_dict = {} 3211 | # Load from .env file if specified 3212 | if env_file: 3213 | try: 3214 | env_dict |= { 3215 | k: v 3216 | for k, v in dotenv.dotenv_values(env_file).items() 3217 | if v is not None 3218 | } 3219 | except Exception as e: 3220 | logger.error(f"Failed to load .env file: {e}") 3221 | sys.exit(1) 3222 | 3223 | # Add command line environment variables 3224 | for env_var in env_vars: 3225 | key, value = _parse_env_var(env_var) 3226 | env_dict[key] = value 3227 | 3228 | if claude.update_claude_config( 3229 | file_spec, 3230 | name, 3231 | with_editable=with_editable, 3232 | with_packages=with_packages, 3233 | env_vars=env_dict, 3234 | ): 3235 | logger.info(f"Successfully installed {name} in Claude app") 3236 | else: 3237 | logger.error(f"Failed to install {name} in Claude app") 3238 | sys.exit(1) 3239 | 3240 | 3241 | ================================================ 3242 | File: /src/fastmcp/utilities/logging.py 3243 | ================================================ 3244 | """Logging utilities for FastMCP.""" 3245 | 3246 | import logging 3247 | from typing import Literal 3248 | 3249 | from rich.console import Console 3250 | from rich.logging import RichHandler 3251 | 3252 | 3253 | def get_logger(name: str) -> logging.Logger: 3254 | """Get a logger nested under FastMCP namespace. 3255 | 3256 | Args: 3257 | name: the name of the logger, which will be prefixed with 'FastMCP.' 3258 | 3259 | Returns: 3260 | a configured logger instance 3261 | """ 3262 | return logging.getLogger(f"FastMCP.{name}") 3263 | 3264 | 3265 | def configure_logging( 3266 | level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", 3267 | ) -> None: 3268 | """Configure logging for FastMCP. 3269 | 3270 | Args: 3271 | level: the log level to use 3272 | """ 3273 | logging.basicConfig( 3274 | level=level, 3275 | format="%(message)s", 3276 | handlers=[RichHandler(console=Console(stderr=True), rich_tracebacks=True)], 3277 | ) 3278 | 3279 | 3280 | ================================================ 3281 | File: /src/fastmcp/utilities/func_metadata.py 3282 | ================================================ 3283 | import inspect 3284 | from collections.abc import Callable, Sequence, Awaitable 3285 | from typing import ( 3286 | Annotated, 3287 | Any, 3288 | Dict, 3289 | ForwardRef, 3290 | ) 3291 | from pydantic import Field 3292 | from fastmcp.exceptions import InvalidSignature 3293 | from pydantic._internal._typing_extra import eval_type_lenient 3294 | import json 3295 | from pydantic import BaseModel 3296 | from pydantic.fields import FieldInfo 3297 | from pydantic import ConfigDict, create_model 3298 | from pydantic import WithJsonSchema 3299 | from pydantic_core import PydanticUndefined 3300 | from fastmcp.utilities.logging import get_logger 3301 | 3302 | 3303 | logger = get_logger(__name__) 3304 | 3305 | 3306 | class ArgModelBase(BaseModel): 3307 | """A model representing the arguments to a function.""" 3308 | 3309 | def model_dump_one_level(self) -> dict[str, Any]: 3310 | """Return a dict of the model's fields, one level deep. 3311 | 3312 | That is, sub-models etc are not dumped - they are kept as pydantic models. 3313 | """ 3314 | kwargs: dict[str, Any] = {} 3315 | for field_name in self.model_fields.keys(): 3316 | kwargs[field_name] = getattr(self, field_name) 3317 | return kwargs 3318 | 3319 | model_config = ConfigDict( 3320 | arbitrary_types_allowed=True, 3321 | ) 3322 | 3323 | 3324 | class FuncMetadata(BaseModel): 3325 | arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)] 3326 | # We can add things in the future like 3327 | # - Maybe some args are excluded from attempting to parse from JSON 3328 | # - Maybe some args are special (like context) for dependency injection 3329 | 3330 | async def call_fn_with_arg_validation( 3331 | self, 3332 | fn: Callable[..., Any] | Awaitable[Any], 3333 | fn_is_async: bool, 3334 | arguments_to_validate: dict[str, Any], 3335 | arguments_to_pass_directly: dict[str, Any] | None, 3336 | ) -> Any: 3337 | """Call the given function with arguments validated and injected. 3338 | 3339 | Arguments are first attempted to be parsed from JSON, then validated against 3340 | the argument model, before being passed to the function. 3341 | """ 3342 | arguments_pre_parsed = self.pre_parse_json(arguments_to_validate) 3343 | arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed) 3344 | arguments_parsed_dict = arguments_parsed_model.model_dump_one_level() 3345 | 3346 | arguments_parsed_dict |= arguments_to_pass_directly or {} 3347 | 3348 | if fn_is_async: 3349 | if isinstance(fn, Awaitable): 3350 | return await fn 3351 | return await fn(**arguments_parsed_dict) 3352 | if isinstance(fn, Callable): 3353 | return fn(**arguments_parsed_dict) 3354 | raise TypeError("fn must be either Callable or Awaitable") 3355 | 3356 | def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]: 3357 | """Pre-parse data from JSON. 3358 | 3359 | Return a dict with same keys as input but with values parsed from JSON 3360 | if appropriate. 3361 | 3362 | This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside 3363 | a string rather than an actual list. Claude desktop is prone to this - in fact 3364 | it seems incapable of NOT doing this. For sub-models, it tends to pass 3365 | dicts (JSON objects) as JSON strings, which can be pre-parsed here. 3366 | """ 3367 | new_data = data.copy() # Shallow copy 3368 | for field_name, field_info in self.arg_model.model_fields.items(): 3369 | if field_name not in data.keys(): 3370 | continue 3371 | if isinstance(data[field_name], str): 3372 | try: 3373 | pre_parsed = json.loads(data[field_name]) 3374 | except json.JSONDecodeError: 3375 | continue # Not JSON - skip 3376 | if isinstance(pre_parsed, (str, int, float)): 3377 | # This is likely that the raw value is e.g. `"hello"` which we 3378 | # Should really be parsed as '"hello"' in Python - but if we parse 3379 | # it as JSON it'll turn into just 'hello'. So we skip it. 3380 | continue 3381 | new_data[field_name] = pre_parsed 3382 | assert new_data.keys() == data.keys() 3383 | return new_data 3384 | 3385 | model_config = ConfigDict( 3386 | arbitrary_types_allowed=True, 3387 | ) 3388 | 3389 | 3390 | def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata: 3391 | """Given a function, return metadata including a pydantic model representing its signature. 3392 | 3393 | The use case for this is 3394 | ``` 3395 | meta = func_to_pyd(func) 3396 | validated_args = meta.arg_model.model_validate(some_raw_data_dict) 3397 | return func(**validated_args.model_dump_one_level()) 3398 | ``` 3399 | 3400 | **critically** it also provides pre-parse helper to attempt to parse things from JSON. 3401 | 3402 | Args: 3403 | func: The function to convert to a pydantic model 3404 | skip_names: A list of parameter names to skip. These will not be included in 3405 | the model. 3406 | Returns: 3407 | A pydantic model representing the function's signature. 3408 | """ 3409 | sig = _get_typed_signature(func) 3410 | params = sig.parameters 3411 | dynamic_pydantic_model_params: dict[str, Any] = {} 3412 | globalns = getattr(func, "__globals__", {}) 3413 | for param in params.values(): 3414 | if param.name.startswith("_"): 3415 | raise InvalidSignature( 3416 | f"Parameter {param.name} of {func.__name__} may not start with an underscore" 3417 | ) 3418 | if param.name in skip_names: 3419 | continue 3420 | annotation = param.annotation 3421 | 3422 | # `x: None` / `x: None = None` 3423 | if annotation is None: 3424 | annotation = Annotated[ 3425 | None, 3426 | Field( 3427 | default=param.default 3428 | if param.default is not inspect.Parameter.empty 3429 | else PydanticUndefined 3430 | ), 3431 | ] 3432 | 3433 | # Untyped field 3434 | if annotation is inspect.Parameter.empty: 3435 | annotation = Annotated[ 3436 | Any, 3437 | Field(), 3438 | # 🤷 3439 | WithJsonSchema({"title": param.name, "type": "string"}), 3440 | ] 3441 | 3442 | field_info = FieldInfo.from_annotated_attribute( 3443 | _get_typed_annotation(annotation, globalns), 3444 | param.default 3445 | if param.default is not inspect.Parameter.empty 3446 | else PydanticUndefined, 3447 | ) 3448 | dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info) 3449 | continue 3450 | 3451 | arguments_model = create_model( 3452 | f"{func.__name__}Arguments", 3453 | **dynamic_pydantic_model_params, 3454 | __base__=ArgModelBase, 3455 | ) 3456 | resp = FuncMetadata(arg_model=arguments_model) 3457 | return resp 3458 | 3459 | 3460 | def _get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: 3461 | if isinstance(annotation, str): 3462 | annotation = ForwardRef(annotation) 3463 | annotation = eval_type_lenient(annotation, globalns, globalns) 3464 | 3465 | return annotation 3466 | 3467 | 3468 | def _get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: 3469 | """Get function signature while evaluating forward references""" 3470 | signature = inspect.signature(call) 3471 | globalns = getattr(call, "__globals__", {}) 3472 | typed_params = [ 3473 | inspect.Parameter( 3474 | name=param.name, 3475 | kind=param.kind, 3476 | default=param.default, 3477 | annotation=_get_typed_annotation(param.annotation, globalns), 3478 | ) 3479 | for param in signature.parameters.values() 3480 | ] 3481 | typed_signature = inspect.Signature(typed_params) 3482 | return typed_signature 3483 | 3484 | 3485 | ================================================ 3486 | File: /src/fastmcp/utilities/__init__.py 3487 | ================================================ 3488 | """FastMCP utility modules.""" 3489 | 3490 | 3491 | ================================================ 3492 | File: /src/fastmcp/utilities/types.py 3493 | ================================================ 3494 | """Common types used across FastMCP.""" 3495 | 3496 | import base64 3497 | from pathlib import Path 3498 | from typing import Optional, Union 3499 | 3500 | from mcp.types import ImageContent 3501 | 3502 | 3503 | class Image: 3504 | """Helper class for returning images from tools.""" 3505 | 3506 | def __init__( 3507 | self, 3508 | path: Optional[Union[str, Path]] = None, 3509 | data: Optional[bytes] = None, 3510 | format: Optional[str] = None, 3511 | ): 3512 | if path is None and data is None: 3513 | raise ValueError("Either path or data must be provided") 3514 | if path is not None and data is not None: 3515 | raise ValueError("Only one of path or data can be provided") 3516 | 3517 | self.path = Path(path) if path else None 3518 | self.data = data 3519 | self._format = format 3520 | self._mime_type = self._get_mime_type() 3521 | 3522 | def _get_mime_type(self) -> str: 3523 | """Get MIME type from format or guess from file extension.""" 3524 | if self._format: 3525 | return f"image/{self._format.lower()}" 3526 | 3527 | if self.path: 3528 | suffix = self.path.suffix.lower() 3529 | return { 3530 | ".png": "image/png", 3531 | ".jpg": "image/jpeg", 3532 | ".jpeg": "image/jpeg", 3533 | ".gif": "image/gif", 3534 | ".webp": "image/webp", 3535 | }.get(suffix, "application/octet-stream") 3536 | return "image/png" # default for raw binary data 3537 | 3538 | def to_image_content(self) -> ImageContent: 3539 | """Convert to MCP ImageContent.""" 3540 | if self.path: 3541 | with open(self.path, "rb") as f: 3542 | data = base64.b64encode(f.read()).decode() 3543 | elif self.data is not None: 3544 | data = base64.b64encode(self.data).decode() 3545 | else: 3546 | raise ValueError("No image data available") 3547 | 3548 | return ImageContent(type="image", data=data, mimeType=self._mime_type) 3549 | 3550 | 3551 | ================================================ 3552 | File: /src/fastmcp/prompts/prompt_manager.py 3553 | ================================================ 3554 | """Prompt management functionality.""" 3555 | 3556 | from typing import Dict, Optional 3557 | 3558 | 3559 | from fastmcp.prompts.base import Prompt 3560 | from fastmcp.utilities.logging import get_logger 3561 | 3562 | logger = get_logger(__name__) 3563 | 3564 | 3565 | class PromptManager: 3566 | """Manages FastMCP prompts.""" 3567 | 3568 | def __init__(self, warn_on_duplicate_prompts: bool = True): 3569 | self._prompts: Dict[str, Prompt] = {} 3570 | self.warn_on_duplicate_prompts = warn_on_duplicate_prompts 3571 | 3572 | def add_prompt(self, prompt: Prompt) -> Prompt: 3573 | """Add a prompt to the manager.""" 3574 | logger.debug(f"Adding prompt: {prompt.name}") 3575 | existing = self._prompts.get(prompt.name) 3576 | if existing: 3577 | if self.warn_on_duplicate_prompts: 3578 | logger.warning(f"Prompt already exists: {prompt.name}") 3579 | return existing 3580 | self._prompts[prompt.name] = prompt 3581 | return prompt 3582 | 3583 | def get_prompt(self, name: str) -> Optional[Prompt]: 3584 | """Get prompt by name.""" 3585 | return self._prompts.get(name) 3586 | 3587 | def list_prompts(self) -> list[Prompt]: 3588 | """List all registered prompts.""" 3589 | return list(self._prompts.values()) 3590 | 3591 | 3592 | ================================================ 3593 | File: /src/fastmcp/prompts/__init__.py 3594 | ================================================ 3595 | from .base import Prompt 3596 | from .manager import PromptManager 3597 | 3598 | __all__ = ["Prompt", "PromptManager"] 3599 | 3600 | 3601 | ================================================ 3602 | File: /src/fastmcp/prompts/manager.py 3603 | ================================================ 3604 | """Prompt management functionality.""" 3605 | 3606 | from typing import Any, Dict, Optional 3607 | 3608 | from fastmcp.prompts.base import Message, Prompt 3609 | from fastmcp.utilities.logging import get_logger 3610 | 3611 | logger = get_logger(__name__) 3612 | 3613 | 3614 | class PromptManager: 3615 | """Manages FastMCP prompts.""" 3616 | 3617 | def __init__(self, warn_on_duplicate_prompts: bool = True): 3618 | self._prompts: Dict[str, Prompt] = {} 3619 | self.warn_on_duplicate_prompts = warn_on_duplicate_prompts 3620 | 3621 | def get_prompt(self, name: str) -> Optional[Prompt]: 3622 | """Get prompt by name.""" 3623 | return self._prompts.get(name) 3624 | 3625 | def list_prompts(self) -> list[Prompt]: 3626 | """List all registered prompts.""" 3627 | return list(self._prompts.values()) 3628 | 3629 | def add_prompt( 3630 | self, 3631 | prompt: Prompt, 3632 | ) -> Prompt: 3633 | """Add a prompt to the manager.""" 3634 | 3635 | # Check for duplicates 3636 | existing = self._prompts.get(prompt.name) 3637 | if existing: 3638 | if self.warn_on_duplicate_prompts: 3639 | logger.warning(f"Prompt already exists: {prompt.name}") 3640 | return existing 3641 | 3642 | self._prompts[prompt.name] = prompt 3643 | return prompt 3644 | 3645 | async def render_prompt( 3646 | self, name: str, arguments: Optional[Dict[str, Any]] = None 3647 | ) -> list[Message]: 3648 | """Render a prompt by name with arguments.""" 3649 | prompt = self.get_prompt(name) 3650 | if not prompt: 3651 | raise ValueError(f"Unknown prompt: {name}") 3652 | 3653 | return await prompt.render(arguments) 3654 | 3655 | 3656 | ================================================ 3657 | File: /src/fastmcp/prompts/base.py 3658 | ================================================ 3659 | """Base classes for FastMCP prompts.""" 3660 | 3661 | import json 3662 | from typing import Any, Callable, Dict, Literal, Optional, Sequence, Awaitable 3663 | import inspect 3664 | 3665 | from pydantic import BaseModel, Field, TypeAdapter, validate_call 3666 | from mcp.types import TextContent, ImageContent, EmbeddedResource 3667 | import pydantic_core 3668 | 3669 | CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource 3670 | 3671 | 3672 | class Message(BaseModel): 3673 | """Base class for all prompt messages.""" 3674 | 3675 | role: Literal["user", "assistant"] 3676 | content: CONTENT_TYPES 3677 | 3678 | def __init__(self, content: str | CONTENT_TYPES, **kwargs): 3679 | if isinstance(content, str): 3680 | content = TextContent(type="text", text=content) 3681 | super().__init__(content=content, **kwargs) 3682 | 3683 | 3684 | class UserMessage(Message): 3685 | """A message from the user.""" 3686 | 3687 | role: Literal["user"] = "user" 3688 | 3689 | def __init__(self, content: str | CONTENT_TYPES, **kwargs): 3690 | super().__init__(content=content, **kwargs) 3691 | 3692 | 3693 | class AssistantMessage(Message): 3694 | """A message from the assistant.""" 3695 | 3696 | role: Literal["assistant"] = "assistant" 3697 | 3698 | def __init__(self, content: str | CONTENT_TYPES, **kwargs): 3699 | super().__init__(content=content, **kwargs) 3700 | 3701 | 3702 | message_validator = TypeAdapter(UserMessage | AssistantMessage) 3703 | 3704 | SyncPromptResult = ( 3705 | str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]] 3706 | ) 3707 | PromptResult = SyncPromptResult | Awaitable[SyncPromptResult] 3708 | 3709 | 3710 | class PromptArgument(BaseModel): 3711 | """An argument that can be passed to a prompt.""" 3712 | 3713 | name: str = Field(description="Name of the argument") 3714 | description: str | None = Field( 3715 | None, description="Description of what the argument does" 3716 | ) 3717 | required: bool = Field( 3718 | default=False, description="Whether the argument is required" 3719 | ) 3720 | 3721 | 3722 | class Prompt(BaseModel): 3723 | """A prompt template that can be rendered with parameters.""" 3724 | 3725 | name: str = Field(description="Name of the prompt") 3726 | description: str | None = Field( 3727 | None, description="Description of what the prompt does" 3728 | ) 3729 | arguments: list[PromptArgument] | None = Field( 3730 | None, description="Arguments that can be passed to the prompt" 3731 | ) 3732 | fn: Callable = Field(exclude=True) 3733 | 3734 | @classmethod 3735 | def from_function( 3736 | cls, 3737 | fn: Callable[..., PromptResult], 3738 | name: Optional[str] = None, 3739 | description: Optional[str] = None, 3740 | ) -> "Prompt": 3741 | """Create a Prompt from a function. 3742 | 3743 | The function can return: 3744 | - A string (converted to a message) 3745 | - A Message object 3746 | - A dict (converted to a message) 3747 | - A sequence of any of the above 3748 | """ 3749 | func_name = name or fn.__name__ 3750 | 3751 | if func_name == "": 3752 | raise ValueError("You must provide a name for lambda functions") 3753 | 3754 | # Get schema from TypeAdapter - will fail if function isn't properly typed 3755 | parameters = TypeAdapter(fn).json_schema() 3756 | 3757 | # Convert parameters to PromptArguments 3758 | arguments = [] 3759 | if "properties" in parameters: 3760 | for param_name, param in parameters["properties"].items(): 3761 | required = param_name in parameters.get("required", []) 3762 | arguments.append( 3763 | PromptArgument( 3764 | name=param_name, 3765 | description=param.get("description"), 3766 | required=required, 3767 | ) 3768 | ) 3769 | 3770 | # ensure the arguments are properly cast 3771 | fn = validate_call(fn) 3772 | 3773 | return cls( 3774 | name=func_name, 3775 | description=description or fn.__doc__ or "", 3776 | arguments=arguments, 3777 | fn=fn, 3778 | ) 3779 | 3780 | async def render(self, arguments: Optional[Dict[str, Any]] = None) -> list[Message]: 3781 | """Render the prompt with arguments.""" 3782 | # Validate required arguments 3783 | if self.arguments: 3784 | required = {arg.name for arg in self.arguments if arg.required} 3785 | provided = set(arguments or {}) 3786 | missing = required - provided 3787 | if missing: 3788 | raise ValueError(f"Missing required arguments: {missing}") 3789 | 3790 | try: 3791 | # Call function and check if result is a coroutine 3792 | result = self.fn(**(arguments or {})) 3793 | if inspect.iscoroutine(result): 3794 | result = await result 3795 | 3796 | # Validate messages 3797 | if not isinstance(result, (list, tuple)): 3798 | result = [result] 3799 | 3800 | # Convert result to messages 3801 | messages = [] 3802 | for msg in result: 3803 | try: 3804 | if isinstance(msg, Message): 3805 | messages.append(msg) 3806 | elif isinstance(msg, dict): 3807 | msg = message_validator.validate_python(msg) 3808 | messages.append(msg) 3809 | elif isinstance(msg, str): 3810 | messages.append( 3811 | UserMessage(content=TextContent(type="text", text=msg)) 3812 | ) 3813 | else: 3814 | msg = json.dumps(pydantic_core.to_jsonable_python(msg)) 3815 | messages.append(Message(role="user", content=msg)) 3816 | except Exception: 3817 | raise ValueError( 3818 | f"Could not convert prompt result to message: {msg}" 3819 | ) 3820 | 3821 | return messages 3822 | except Exception as e: 3823 | raise ValueError(f"Error rendering prompt {self.name}: {e}") 3824 | 3825 | 3826 | ================================================ 3827 | File: /src/fastmcp/py.typed 3828 | ================================================ 3829 | 3830 | 3831 | ================================================ 3832 | File: /src/fastmcp/exceptions.py 3833 | ================================================ 3834 | """Custom exceptions for FastMCP.""" 3835 | 3836 | 3837 | class FastMCPError(Exception): 3838 | """Base error for FastMCP.""" 3839 | 3840 | 3841 | class ValidationError(FastMCPError): 3842 | """Error in validating parameters or return values.""" 3843 | 3844 | 3845 | class ResourceError(FastMCPError): 3846 | """Error in resource operations.""" 3847 | 3848 | 3849 | class ToolError(FastMCPError): 3850 | """Error in tool operations.""" 3851 | 3852 | 3853 | class InvalidSignature(Exception): 3854 | """Invalid signature for use with FastMCP.""" 3855 | 3856 | 3857 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastmcp==0.4.1 -------------------------------------------------------------------------------- /sqlite_explorer.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import sqlite3 3 | import os 4 | from typing import List, Dict, Any, Optional 5 | from fastmcp import FastMCP 6 | 7 | # Initialize FastMCP server 8 | mcp = FastMCP("SQLite Explorer", 9 | log_level="CRITICAL") 10 | 11 | # Path to Messages database - must be provided via SQLITE_DB_PATH environment variable 12 | if 'SQLITE_DB_PATH' not in os.environ: 13 | raise ValueError("SQLITE_DB_PATH environment variable must be set") 14 | DB_PATH = Path(os.environ['SQLITE_DB_PATH']) 15 | 16 | class SQLiteConnection: 17 | def __init__(self, db_path: Path): 18 | self.db_path = db_path 19 | self.conn = None 20 | 21 | def __enter__(self): 22 | self.conn = sqlite3.connect(str(self.db_path)) 23 | self.conn.row_factory = sqlite3.Row 24 | return self.conn 25 | 26 | def __exit__(self, exc_type, exc_val, exc_tb): 27 | if self.conn: 28 | self.conn.close() 29 | 30 | @mcp.tool() 31 | def read_query( 32 | query: str, 33 | params: Optional[List[Any]] = None, 34 | fetch_all: bool = True, 35 | row_limit: int = 1000 36 | ) -> List[Dict[str, Any]]: 37 | """Execute a query on the Messages database. 38 | 39 | Args: 40 | query: SELECT SQL query to execute 41 | params: Optional list of parameters for the query 42 | fetch_all: If True, fetches all results. If False, fetches one row. 43 | row_limit: Maximum number of rows to return (default 1000) 44 | 45 | Returns: 46 | List of dictionaries containing the query results 47 | """ 48 | if not DB_PATH.exists(): 49 | raise FileNotFoundError(f"Messages database not found at: {DB_PATH}") 50 | 51 | # Clean and validate the query 52 | query = query.strip() 53 | 54 | # Remove trailing semicolon if present 55 | if query.endswith(';'): 56 | query = query[:-1].strip() 57 | 58 | # Check for multiple statements by looking for semicolons not inside quotes 59 | def contains_multiple_statements(sql: str) -> bool: 60 | in_single_quote = False 61 | in_double_quote = False 62 | for char in sql: 63 | if char == "'" and not in_double_quote: 64 | in_single_quote = not in_single_quote 65 | elif char == '"' and not in_single_quote: 66 | in_double_quote = not in_double_quote 67 | elif char == ';' and not in_single_quote and not in_double_quote: 68 | return True 69 | return False 70 | 71 | if contains_multiple_statements(query): 72 | raise ValueError("Multiple SQL statements are not allowed") 73 | 74 | # Validate query type (allowing common CTEs) 75 | query_lower = query.lower() 76 | if not any(query_lower.startswith(prefix) for prefix in ('select', 'with')): 77 | raise ValueError("Only SELECT queries (including WITH clauses) are allowed for safety") 78 | 79 | params = params or [] 80 | 81 | with SQLiteConnection(DB_PATH) as conn: 82 | cursor = conn.cursor() 83 | 84 | try: 85 | # Only add LIMIT if query doesn't already have one 86 | if 'limit' not in query_lower: 87 | query = f"{query} LIMIT {row_limit}" 88 | 89 | cursor.execute(query, params) 90 | 91 | if fetch_all: 92 | results = cursor.fetchall() 93 | else: 94 | results = [cursor.fetchone()] 95 | 96 | return [dict(row) for row in results if row is not None] 97 | 98 | except sqlite3.Error as e: 99 | raise ValueError(f"SQLite error: {str(e)}") 100 | 101 | @mcp.tool() 102 | def list_tables() -> List[str]: 103 | """List all tables in the Messages database. 104 | 105 | Returns: 106 | List of table names in the database 107 | """ 108 | if not DB_PATH.exists(): 109 | raise FileNotFoundError(f"Messages database not found at: {DB_PATH}") 110 | 111 | with SQLiteConnection(DB_PATH) as conn: 112 | cursor = conn.cursor() 113 | 114 | try: 115 | cursor.execute(""" 116 | SELECT name FROM sqlite_master 117 | WHERE type='table' 118 | ORDER BY name 119 | """) 120 | 121 | return [row['name'] for row in cursor.fetchall()] 122 | 123 | except sqlite3.Error as e: 124 | raise ValueError(f"SQLite error: {str(e)}") 125 | 126 | @mcp.tool() 127 | def describe_table(table_name: str) -> List[Dict[str, str]]: 128 | """Get detailed information about a table's schema. 129 | 130 | Args: 131 | table_name: Name of the table to describe 132 | 133 | Returns: 134 | List of dictionaries containing column information: 135 | - name: Column name 136 | - type: Column data type 137 | - notnull: Whether the column can contain NULL values 138 | - dflt_value: Default value for the column 139 | - pk: Whether the column is part of the primary key 140 | """ 141 | if not DB_PATH.exists(): 142 | raise FileNotFoundError(f"Messages database not found at: {DB_PATH}") 143 | 144 | with SQLiteConnection(DB_PATH) as conn: 145 | cursor = conn.cursor() 146 | 147 | try: 148 | # Verify table exists 149 | cursor.execute(""" 150 | SELECT name FROM sqlite_master 151 | WHERE type='table' AND name=? 152 | """, [table_name]) 153 | 154 | if not cursor.fetchone(): 155 | raise ValueError(f"Table '{table_name}' does not exist") 156 | 157 | # Get table schema 158 | cursor.execute(f"PRAGMA table_info({table_name})") 159 | columns = cursor.fetchall() 160 | 161 | return [dict(row) for row in columns] 162 | 163 | except sqlite3.Error as e: 164 | raise ValueError(f"SQLite error: {str(e)}") --------------------------------------------------------------------------------