diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index fa9f48e22..83d50a97c 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,362 @@ -# FastMCP + +# FastMCP -> **Note**: This is experimental software. The Model Context Protocol itself is only a few days old and the specification is still evolving. +
-A fast, pythonic way to build Model Context Protocol (MCP) servers. +[![PyPI - Version](https://img.shields.io/pypi/v/fastmcp.svg)](https://pypi.org/project/fastmcp) +[![Tests](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml) +[![License](https://img.shields.io/github/license/jlowin/fastmcp.svg)](https://github.com/jlowin/fastmcp/blob/main/LICENSE) -Anthropic's new [Model Context Protocol](https://modelcontextprotocol.io) is a powerful way to give broadcast new functionality and context to LLMs. However, developing MCP servers can be cumbersome. FastMCP provides a simple, intuitive interface for creating MCP servers in Python. +
+FastMCP is a high-level, intuitive framework for building [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers with Python. While MCP is a powerful protocol that enables LLMs to interact with local data and tools in a secure, standardized way, the specification can be cumbersome to implement directly. FastMCP lets you build fully compliant MCP servers in the most Pythonic way possible - in many cases, simply decorating a function is all that's required. + +🚧 *Note: FastMCP is under active development, as is the low-level MCP Python SDK* 🏗️ + +Key features: +* **Intuitive**: Designed to feel familiar to Python developers, with powerful type hints and editor support +* **Simple**: Build compliant MCP servers with minimal boilerplate +* **Fast**: High-performance async implementation +* **Full-featured**: Complete implementation of the MCP specification + + ## Table of Contents -- [FastMCP](#fastmcp) - - [Table of Contents](#table-of-contents) - - [Installation](#installation) - - [Quick Start](#quick-start) - - [Core Concepts](#core-concepts) - - [Resources](#resources) - - [Tools](#tools) +- [Installation](#installation) +- [Quickstart](#quickstart) +- [What is MCP?](#what-is-mcp) +- [Core Concepts](#core-concepts) + - [Server](#server) + - [Resources](#resources) + - [Tools](#tools) + - [Prompts](#prompts) + - [Images](#images) + - [Context](#context) +- [Deployment](#deployment) - [Development](#development) - - [Running the Dev Inspector](#running-the-dev-inspector) - - [Installing in Claude](#installing-in-claude) - - [License](#license) + - [Claude Desktop](#claude-desktop) +- [Examples](#examples) + - [Echo Server](#echo-server) + - [SQLite Explorer](#sqlite-explorer) ## Installation -MCP servers require you to use [uv](https://github.com/astral-sh/uv) as your dependency manager. - -Install uv with brew: -```bash -brew install uv -``` -*(Editor's note: I was unable to get MCP servers working unless uv was installed with brew.)* - -Install FastMCP: ```bash +# We strongly recommend installing with uv +brew install uv # on macOS uv pip install fastmcp ``` -## Quick Start +Or with pip: +```bash +pip install fastmcp +``` -Here's a simple example that exposes your desktop directory as a resource and provides a basic addition tool: +## Quickstart + +Let's create a simple MCP server that exposes a calculator tool and some data: ```python -from pathlib import Path from fastmcp import FastMCP -# Create server + +# Create an MCP server mcp = FastMCP("Demo") -@mcp.resource("dir://desktop") -def desktop() -> list[str]: - """List the files in the user's desktop""" - desktop = Path.home() / "Desktop" - return [str(f) for f in desktop.iterdir()] +# Add an addition tool @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers""" return a + b -if __name__ == "__main__": - mcp.run() + +# Add a dynamic greeting resource +@mcp.resource("greeting://{name}") +def get_greeting(name: str) -> str: + """Get a personalized greeting""" + return f"Hello, {name}!" ``` +To use this server, you have two options: + +1. Install it in Claude Desktop: +```bash +fastmcp install server.py +``` + +2. Test it with the MCP Inspector: +```bash +fastmcp dev server.py +``` + +![MCP Inspector](docs/images/mcp-inspector.png) + +## What is MCP? + +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: + +- Expose data through **Resources** (like GET endpoints) +- Provide functionality through **Tools** (like POST endpoints) +- Define interaction patterns through **Prompts** (reusable templates for LLM interactions) + ## Core Concepts -FastMCP makes it easy to expose two types of functionality to LLMs: Resources and Tools. +*Note: All code examples below assume you've created a FastMCP server instance called `mcp`.* + +### Server + +The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: + +```python +from fastmcp import FastMCP + +# Create a named server +mcp = FastMCP("My App") + +# Configure host/port for HTTP transport (optional) +mcp = FastMCP("My App", host="localhost", port=8000) +``` ### Resources -Resources are data sources that can be accessed by the LLM. They're perfect for providing context like files, API responses, or database queries. +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: -FastMCP provides a simple `@resource` decorator that handles both static and dynamic resources. While the MCP spec distinguishes between resources and templates, FastMCP automatically handles this distinction based on your function signature: +- File contents +- Database schemas +- API responses +- System information +Resources can be static: ```python -# Static resource -@mcp.resource("resource://static") -def get_static() -> str: - """Return static content""" - return "Static content" - -# Dynamic resource -@mcp.resource("resource://{city}/weather") -def get_weather(city: str) -> str: - """Get weather for a city""" - return f"Weather for {city}" - -# Multiple parameters are supported -@mcp.resource("db://users/{user_id}/posts/{post_id}") -def get_user_post(user_id: int, post_id: int) -> dict: - """Get a specific post by a user""" - return { - "user_id": user_id, - "post_id": post_id, - "content": "Post content..." - } - -# File resources -@mcp.resource("file://config.json") +@mcp.resource("config://app") def get_config() -> str: - """Read the config file""" - return Path("config.json").read_text() + """Static configuration data""" + return "App configuration here" ``` -Resources can return: -- Strings for text content -- Bytes for binary content -- Other types will be converted to JSON - -When your resource URI includes parameters in curly braces (like `{city}`) and your function accepts matching arguments, FastMCP automatically sets up a template resource behind the scenes. This means you don't need to worry about the distinction between resources and templates in the MCP spec - just write your function, and FastMCP handles the rest. - -> **Note**: If you're familiar with the MCP spec, you might notice that dynamic resources are implemented as templates under the hood. FastMCP simplifies this by providing a unified interface through the `@resource` decorator. This is similar to how web frameworks often unify GET and POST handlers under a single route decorator. - +Or dynamic with parameters (FastMCP automatically handles these as MCP templates): +```python +@mcp.resource("users://{user_id}/profile") +def get_user_profile(user_id: str) -> str: + """Dynamic user data""" + return f"Profile data for user {user_id}" +``` ### Tools -Tools are functions that can be called by the LLM to perform actions. They're great for calculations, API calls, or any interactive functionality. Tools are defined using the `@tool` decorator: +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. +Simple calculation example: ```python @mcp.tool() -def search_docs(query: str, max_results: int = 5) -> list[dict]: - """Search documentation for relevant entries""" - results = perform_search(query, limit=max_results) - return [{"title": r.title, "excerpt": r.excerpt} for r in results] +def calculate_bmi(weight_kg: float, height_m: float) -> float: + """Calculate BMI given weight in kg and height in meters""" + return weight_kg / (height_m ** 2) +``` + +HTTP request example: +```python +import httpx @mcp.tool() -def analyze_image(image_path: str) -> dict: - """Analyze an image and return metadata""" - from PIL import Image - img = Image.open(image_path) - return { - "size": img.size, - "mode": img.mode, - "format": img.format - } +async def fetch_weather(city: str) -> str: + """Fetch current weather for a city""" + async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.weather.com/{city}" + ) + return response.text ``` -Tools support: -- Type hints for parameters -- Default values -- Async functions -- Return value conversion to JSON +### Prompts -## Development +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: -FastMCP includes developer tools to make testing and debugging easier. +```python +@mcp.prompt() +def review_code(code: str) -> str: + return f"Please review this code:\n\n{code}" +``` -### Running the Dev Inspector +Or a more structured sequence of messages: +```python +from fastmcp.prompts.base import UserMessage, AssistantMessage -The MCP Inspector helps you test your server during development: +@mcp.prompt() +def debug_error(error: str) -> list[Message]: + return [ + UserMessage("I'm seeing this error:"), + UserMessage(error), + AssistantMessage("I'll help debug that. What have you tried so far?") + ] +``` + + +### Images + +FastMCP provides an `Image` class that automatically handles image data in your server: + +```python +from fastmcp import FastMCP, Image +from PIL import Image as PILImage + +@mcp.tool() +def create_thumbnail(image_path: str) -> Image: + """Create a thumbnail from an image""" + img = PILImage.open(image_path) + img.thumbnail((100, 100)) + + # FastMCP automatically handles conversion and MIME types + return Image(data=img.tobytes(), format="png") + +@mcp.tool() +def load_image(path: str) -> Image: + """Load an image from disk""" + # FastMCP handles reading and format detection + return Image(path=path) +``` + +Images can be used as the result of both tools and resources. + +### Context + +The Context object gives your tools and resources access to MCP capabilities. To use it, add a parameter annotated with `fastmcp.Context`: + +```python +from fastmcp import FastMCP, Context + +@mcp.tool() +async def long_task(files: list[str], ctx: Context) -> str: + """Process multiple files with progress tracking""" + for i, file in enumerate(files): + ctx.info(f"Processing {file}") + await ctx.report_progress(i, len(files)) + + # Read another resource if needed + data = await ctx.read_resource(f"file://{file}") + + return "Processing complete" +``` + +The Context object provides: +- Progress reporting through `report_progress()` +- Logging via `debug()`, `info()`, `warning()`, and `error()` +- Resource access through `read_resource()` +- Request metadata via `request_id` and `client_id` + +## Deployment + +The FastMCP CLI helps you develop and deploy MCP servers. + +Note that for all deployment commands, you are expected to provide the fully qualified path to your server object. For example, if you have a file `server.py` that contains a FastMCP server named `my_server`, you would provide `path/to/server.py:my_server`. + +If your server variable has one of the standard names (`mcp`, `server`, or `app`), you can omit the server name from the path and just provide the file: `path/to/server.py`. + +### Development + +Test and debug your server with the MCP Inspector: +```bash +# Provide the fully qualified path to your server +fastmcp dev server.py:my_mcp_server + +# Or just the file if your server is named 'mcp', 'server', or 'app' +fastmcp dev server.py +``` + +Your server is run in an isolated environment, so you'll need to indicate any dependencies with the `--with` flag. FastMCP is automatically included. If you are working on a uv project, you can use the `--with-editable` flag to mount your current directory: ```bash -# Basic usage -fastmcp dev your_server.py +# With additional packages +fastmcp dev server.py --with pandas --with numpy -# Install package in editable mode from current directory -fastmcp dev your_server.py --with-editable . - -# Install additional packages -fastmcp dev your_server.py --with pandas --with numpy - -# Combine both -fastmcp dev your_server.py --with-editable . --with pandas --with numpy +# Using your project's dependencies and up-to-date code +fastmcp dev server.py --with-editable . ``` -The `--with` flag automatically includes `fastmcp` and any additional packages you specify. The `--with-editable` flag installs the package from the specified directory in editable mode, which is useful during development. - -### Installing in Claude - -To use your server with Claude Desktop: +### Claude Desktop +Install your server in Claude Desktop: ```bash -# Basic usage -fastmcp install your_server.py --name "My Server" +# Basic usage (name is taken from your FastMCP instance) +fastmcp install server.py -# Install package in editable mode -fastmcp install your_server.py --with-editable . +# With a custom name +fastmcp install server.py --name "My Server" -# Install additional packages -fastmcp install your_server.py --with pandas --with numpy +# With dependencies +fastmcp install server.py --with pandas --with numpy -# Combine options -fastmcp install your_server.py --with-editable . --with pandas --with numpy +# Replace an existing server +fastmcp install server.py --force ``` -## License +The server name in Claude will be: +1. The `--name` parameter if provided +2. The `name` from your FastMCP instance +3. The filename if the server can't be imported -Apache 2.0 \ No newline at end of file +## Examples + +### Echo Server +A simple server demonstrating resources, tools, and prompts: + +```python +from fastmcp import FastMCP + +mcp = FastMCP("Echo") + +@mcp.resource("echo://{message}") +def echo_resource(message: str) -> str: + """Echo a message as a resource""" + return f"Resource echo: {message}" + +@mcp.tool() +def echo_tool(message: str) -> str: + """Echo a message as a tool""" + return f"Tool echo: {message}" + +@mcp.prompt() +def echo_prompt(message: str) -> str: + """Create an echo prompt""" + return f"Please process this message: {message}" +``` + +### SQLite Explorer +A more complex example showing database integration: + +```python +from fastmcp import FastMCP +import sqlite3 + +mcp = FastMCP("SQLite Explorer") + +@mcp.resource("schema://main") +def get_schema() -> str: + """Provide the database schema as a resource""" + conn = sqlite3.connect("database.db") + schema = conn.execute( + "SELECT sql FROM sqlite_master WHERE type='table'" + ).fetchall() + return "\n".join(sql[0] for sql in schema if sql[0]) + +@mcp.tool() +def query_data(sql: str) -> str: + """Execute SQL queries safely""" + conn = sqlite3.connect("database.db") + try: + result = conn.execute(sql).fetchall() + return "\n".join(str(row) for row in result) + except Exception as e: + return f"Error: {str(e)}" + +@mcp.prompt() +def analyze_table(table: str) -> str: + """Create a prompt template for analyzing tables""" + return f"""Please analyze this database table: +Table: {table} +Schema: +{get_schema()} + +What insights can you provide about the structure and relationships?""" +``` \ No newline at end of file diff --git a/docs/assets/demo-inspector.png b/docs/assets/demo-inspector.png new file mode 100644 index 000000000..5d7715387 Binary files /dev/null and b/docs/assets/demo-inspector.png differ diff --git a/examples/desktop.py b/examples/desktop.py index 7c708ce9b..a9b46cc8a 100644 --- a/examples/desktop.py +++ b/examples/desktop.py @@ -23,7 +23,3 @@ def desktop() -> list[str]: def add(a: int, b: int) -> int: """Add two numbers""" return a + b - - -if __name__ == "__main__": - mcp.run() diff --git a/examples/echo.py b/examples/echo.py index bd940807c..48c0883a5 100644 --- a/examples/echo.py +++ b/examples/echo.py @@ -14,8 +14,13 @@ def echo_tool(text: str) -> str: return text +@mcp.resource("echo://static") +def echo_resource() -> str: + return "Echo!" + + @mcp.resource("echo://{text}") -def echo_resource(text: str) -> str: +def echo_template(text: str) -> str: """Echo the input text""" return f"Echo: {text}" @@ -23,7 +28,3 @@ def echo_resource(text: str) -> str: @mcp.prompt("echo") def echo_prompt(text: str) -> str: return text - - -if __name__ == "__main__": - mcp.run() diff --git a/examples/readme-quickstart.py b/examples/readme-quickstart.py new file mode 100644 index 000000000..26a0cc138 --- /dev/null +++ b/examples/readme-quickstart.py @@ -0,0 +1,19 @@ +from fastmcp import FastMCP + + +# Create an MCP server +mcp = FastMCP("Demo") + + +# Add an addition tool +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b + + +# Add a dynamic greeting resource +@mcp.resource("greeting://{name}") +def get_greeting(name: str) -> str: + """Get a personalized greeting""" + return f"Hello, {name}!" diff --git a/examples/screenshot.py b/examples/screenshot.py index bfef5d8e9..012a253f1 100644 --- a/examples/screenshot.py +++ b/examples/screenshot.py @@ -26,7 +26,3 @@ def take_screenshot() -> Image: # if the file exceeds ~1MB, it will be rejected by Claude screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True) return Image(data=buffer.getvalue(), format="jpeg") - - -if __name__ == "__main__": - mcp.run() diff --git a/examples/simple_echo.py b/examples/simple_echo.py index 2a81ca4c0..99f52c6de 100644 --- a/examples/simple_echo.py +++ b/examples/simple_echo.py @@ -13,7 +13,3 @@ mcp = FastMCP("Echo Server") def echo(text: str) -> str: """Echo the input text""" return text - - -if __name__ == "__main__": - mcp.run() diff --git a/src/fastmcp/cli/claude.py b/src/fastmcp/cli/claude.py index 998733b60..be216a7e6 100644 --- a/src/fastmcp/cli/claude.py +++ b/src/fastmcp/cli/claude.py @@ -25,8 +25,8 @@ def get_claude_config_path() -> Path | None: def update_claude_config( - file: Path, - server_name: Optional[str] = None, + file_spec: str, + server_name: str, *, with_editable: Optional[Path] = None, with_packages: Optional[list[str]] = None, @@ -35,9 +35,8 @@ def update_claude_config( """Add the MCP server to Claude's configuration. Args: - file: Path to the server file - server_name: Optional custom name for the server. If not provided, - defaults to the file stem + file_spec: Path to the server file, optionally with :object suffix + server_name: Name for the server in Claude's config with_editable: Optional directory to install in editable mode with_packages: Optional list of additional packages to install force: If True, replace existing server with same name @@ -55,46 +54,49 @@ def update_claude_config( if "mcpServers" not in config: config["mcpServers"] = {} - # Use provided server_name or fall back to file stem - name = server_name or file.stem - if name in config["mcpServers"]: + if server_name in config["mcpServers"]: if not force: logger.warning( - f"Server '{name}' already exists in Claude config. " + f"Server '{server_name}' already exists in Claude config. " "Use `--force` to replace.", extra={"config_file": str(config_file)}, ) return False logger.info( - f"Replacing existing server '{name}' in Claude config", + f"Replacing existing server '{server_name}' in Claude config", extra={"config_file": str(config_file)}, ) # Build uv run command - args = ["run"] + args = ["run", "--with", "fastmcp"] if with_editable: args.extend(["--with-editable", str(with_editable)]) - # Always include fastmcp - args.extend(["--with", "fastmcp"]) - - # Add additional packages if with_packages: for pkg in with_packages: if pkg: args.extend(["--with", pkg]) - args.append(str(file)) + # Convert file path to absolute before adding to command + # Split off any :object suffix first + if ":" in file_spec: + file_path, server_object = file_spec.rsplit(":", 1) + file_spec = f"{Path(file_path).resolve()}:{server_object}" + else: + file_spec = str(Path(file_spec).resolve()) - config["mcpServers"][name] = { + # Add fastmcp run command + args.extend(["fastmcp", "run", file_spec]) + + config["mcpServers"][server_name] = { "command": "uv", "args": args, } config_file.write_text(json.dumps(config, indent=2)) logger.info( - f"Added server '{name}' to Claude config", + f"Added server '{server_name}' to Claude config", extra={"config_file": str(config_file)}, ) return True diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 42e63251b..cab03aab1 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -24,11 +24,11 @@ app = typer.Typer( def _build_uv_command( - file: Path, + file_spec: str, with_editable: Optional[Path] = None, with_packages: Optional[list[str]] = None, ) -> list[str]: - """Build the uv run command.""" + """Build the uv run command that runs a FastMCP server through fastmcp run.""" cmd = ["uv"] cmd.extend(["run", "--with", "fastmcp"]) @@ -41,7 +41,8 @@ def _build_uv_command( if pkg: cmd.extend(["--with", pkg]) - cmd.append(str(file)) + # Add fastmcp run command + cmd.extend(["fastmcp", "run", file_spec]) return cmd @@ -89,7 +90,7 @@ def _import_server(file: Path, server_object: Optional[str] = None): module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - # If no object specified, try __main__ block + # If no object specified, try common server names if not server_object: # Look for the most common server object names for name in ["mcp", "server", "app"]: @@ -97,7 +98,9 @@ def _import_server(file: Path, server_object: Optional[str] = None): return getattr(module, name) logger.error( - f"No server object found in {file}. Please specify the object name with file:object syntax.", + f"No server object found in {file}. Please either:\n" + "1. Use a standard variable name (mcp, server, or app)\n" + "2. Specify the object name with file:object syntax", extra={"file": str(file)}, ) sys.exit(1) @@ -178,7 +181,7 @@ def dev( ) try: - uv_cmd = _build_uv_command(file, with_editable, with_packages) + uv_cmd = _build_uv_command(file_spec, with_editable, with_packages) # Run the MCP Inspector command process = subprocess.run( ["npx", "@modelcontextprotocol/inspector"] + uv_cmd, @@ -229,7 +232,12 @@ def run( ), ] = None, ) -> None: - """Run a FastMCP server.""" + """Run a FastMCP server. + + The server can be specified in two ways: + 1. Module approach: server.py - runs the module directly, expecting a server.run() call + 2. Import approach: server.py:app - imports and runs the specified server object + """ file, server_object = _parse_file_path(file_spec) logger.debug( @@ -338,7 +346,7 @@ def install( name = file.stem if claude.update_claude_config( - file, + file_spec, name, with_editable=with_editable, with_packages=with_packages, @@ -348,7 +356,3 @@ def install( else: print(f"Failed to install {name} in Claude app") sys.exit(1) - - -if __name__ == "__main__": - app() diff --git a/src/fastmcp/resources/base.py b/src/fastmcp/resources/base.py index c3e55d22c..bde9b3820 100644 --- a/src/fastmcp/resources/base.py +++ b/src/fastmcp/resources/base.py @@ -53,13 +53,8 @@ class Resource(BaseModel, abc.ABC): """Set default name from URI if not provided.""" if name: return name - # Extract everything after the protocol (e.g., "desktop" from "resource://desktop") if uri := info.data.get("uri"): - uri_str = str(uri) - if "://" in uri_str: - name = uri_str.split("://", 1)[1] - if name: - return name + return str(uri) raise ValueError("Either name or uri must be provided") @abc.abstractmethod diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py index 417e79960..0dc5a1b49 100644 --- a/tests/resources/test_resources.py +++ b/tests/resources/test_resources.py @@ -46,7 +46,7 @@ class TestResourceValidation: uri="resource://my-resource", fn=dummy_func, ) - assert resource.name == "my-resource" + assert resource.name == "resource://my-resource" def test_resource_name_validation(self): """Test name validation."""