Merge branch 'main' into updates

This commit is contained in:
Jeremiah Lowin 2024-11-30 19:34:19 -05:00
commit 0fb37ec4cc
12 changed files with 552 additions and 167 deletions

201
LICENSE Normal file
View file

@ -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.

415
README.md
View file

@ -1,187 +1,362 @@
# FastMCP
<!-- omit in toc -->
# FastMCP
> **Note**: This is experimental software. The Model Context Protocol itself is only a few days old and the specification is still evolving.
<div align="center">
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.
</div>
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
<!-- omit in toc -->
## 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
## 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?"""
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

View file

@ -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()

View file

@ -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()

View file

@ -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}!"

View file

@ -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()

View file

@ -13,7 +13,3 @@ mcp = FastMCP("Echo Server")
def echo(text: str) -> str:
"""Echo the input text"""
return text
if __name__ == "__main__":
mcp.run()

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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."""