mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-28 10:18:08 +02:00
Add readme
This commit is contained in:
parent
4d7c7fe052
commit
4f182ab66a
7 changed files with 541 additions and 69 deletions
109
README.md
109
README.md
|
|
@ -1,2 +1,107 @@
|
|||
Notes:
|
||||
- uv must be installed with brew to run local servers
|
||||
# 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.
|
||||
|
||||
The Model Context Protocol is an extremely powerful way to give LLMs access to tools and resources. However, building MCP servers can be difficult and cumbersome. FastMCP provides a simple, intuitive interface for creating MCP servers in Python.
|
||||
|
||||
## 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
|
||||
uv pip install fastmcp
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
Here's a simple example that exposes your desktop directory as a resource and provides a basic addition tool:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Create 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()]
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Resources
|
||||
|
||||
Resources are data sources that can be accessed by the LLM. They can be files, directories, or any other data source. Resources are defined using the `@resource` decorator:
|
||||
|
||||
```python
|
||||
@mcp.resource("file://config.json")
|
||||
def get_config() -> str:
|
||||
"""Read the config file"""
|
||||
return Path("config.json").read_text()
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
Tools are functions that can be called by the LLM. They are defined using the `@tool` decorator:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
def calculate(x: int, y: int) -> int:
|
||||
"""Perform a calculation"""
|
||||
return x + y
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running the Dev Inspector
|
||||
|
||||
FastMCP includes a development server with the MCP Inspector for testing your server:
|
||||
|
||||
```bash
|
||||
fastmcp dev your_server.py
|
||||
```
|
||||
|
||||
### Installing in Claude
|
||||
|
||||
To use your server with Claude Desktop:
|
||||
|
||||
```bash
|
||||
fastmcp install your_server.py --name "My Server"
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
FastMCP can be configured via environment variables with the prefix `FASTMCP_`:
|
||||
|
||||
- `FASTMCP_DEBUG`: Enable debug mode
|
||||
- `FASTMCP_LOG_LEVEL`: Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
- `FASTMCP_HOST`: HTTP server host (default: 0.0.0.0)
|
||||
- `FASTMCP_PORT`: HTTP server port (default: 8000)
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
34
examples/screenshot.py
Normal file
34
examples/screenshot.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# /// script
|
||||
# dependencies = ["pyautogui"]
|
||||
# ///
|
||||
|
||||
"""
|
||||
FastMCP Screenshot Example
|
||||
|
||||
A simple example that provides a tool to capture screenshots.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import io
|
||||
import pyautogui
|
||||
|
||||
from fastmcp.server import FastMCP
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Screenshot Demo")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def take_screenshot() -> str:
|
||||
"""Take a screenshot and return it as a base64 encoded string"""
|
||||
# Capture the screen
|
||||
screenshot = pyautogui.screenshot()
|
||||
|
||||
# Convert to base64
|
||||
buffer = io.BytesIO()
|
||||
screenshot.save(buffer, format="PNG")
|
||||
return base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
|
|
@ -1,72 +1,6 @@
|
|||
"""FastMCP CLI tools."""
|
||||
|
||||
import importlib.metadata
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from .utilities.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
app = typer.Typer(
|
||||
name="fastmcp",
|
||||
help="FastMCP development tools",
|
||||
add_completion=False,
|
||||
no_args_is_help=True, # Show help if no args provided
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def version() -> None:
|
||||
"""Show the FastMCP version."""
|
||||
try:
|
||||
version = importlib.metadata.version("fastmcp")
|
||||
print(f"FastMCP version {version}")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
print("FastMCP version unknown (package not installed)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def dev(
|
||||
file: Path = typer.Argument(
|
||||
...,
|
||||
help="Python file to run",
|
||||
exists=True,
|
||||
dir_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
) -> None:
|
||||
"""Run a FastMCP server with the MCP Inspector."""
|
||||
logger.debug("Starting dev server", extra={"file": str(file)})
|
||||
|
||||
try:
|
||||
# Run the MCP Inspector command
|
||||
process = subprocess.run(
|
||||
["npx", "@modelcontextprotocol/inspector", "uv", "run", str(file)],
|
||||
check=True,
|
||||
)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
"Dev server failed",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"error": str(e),
|
||||
"returncode": e.returncode,
|
||||
},
|
||||
)
|
||||
sys.exit(e.returncode)
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
"npx not found. Please install Node.js and npm.",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
from .cli.cli import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
|
|
|||
5
src/fastmcp/cli/__init__.py
Normal file
5
src/fastmcp/cli/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""FastMCP CLI package."""
|
||||
|
||||
from .cli import app
|
||||
|
||||
__all__ = ["app"]
|
||||
88
src/fastmcp/cli/claude.py
Normal file
88
src/fastmcp/cli/claude.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
"""Claude app integration utilities."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from ..utilities.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_claude_config_path() -> Path | None:
|
||||
"""Get the Claude config directory based on platform."""
|
||||
if sys.platform == "win32":
|
||||
path = Path(Path.home(), "AppData", "Roaming", "Claude")
|
||||
elif sys.platform == "darwin":
|
||||
path = Path(Path.home(), "Library", "Application Support", "Claude")
|
||||
else:
|
||||
return None
|
||||
|
||||
if path.exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def update_claude_config(
|
||||
file: Path,
|
||||
server_name: Optional[str] = None,
|
||||
*,
|
||||
uv_directory: Optional[Path] = None,
|
||||
) -> bool:
|
||||
"""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
|
||||
uv_directory: Optional directory containing pyproject.toml
|
||||
"""
|
||||
config_dir = get_claude_config_path()
|
||||
if not config_dir:
|
||||
return False
|
||||
|
||||
config_file = config_dir / "claude_desktop_config.json"
|
||||
if not config_file.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
config = json.loads(config_file.read_text())
|
||||
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"]:
|
||||
logger.warning(
|
||||
f"Server '{name}' already exists in Claude config",
|
||||
extra={"config_file": str(config_file)},
|
||||
)
|
||||
return False
|
||||
|
||||
# Build uv run command
|
||||
args = []
|
||||
if uv_directory:
|
||||
args.extend(["--directory", str(uv_directory)])
|
||||
args.extend(["run", str(file)])
|
||||
|
||||
config["mcpServers"][name] = {
|
||||
"command": "uv",
|
||||
"args": args,
|
||||
}
|
||||
|
||||
config_file.write_text(json.dumps(config, indent=2))
|
||||
logger.info(
|
||||
f"Added server '{name}' to Claude config",
|
||||
extra={"config_file": str(config_file)},
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to update Claude config",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"config_file": str(config_file),
|
||||
},
|
||||
)
|
||||
return False
|
||||
306
src/fastmcp/cli/cli.py
Normal file
306
src/fastmcp/cli/cli.py
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
"""FastMCP CLI tools."""
|
||||
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import typer
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from ..utilities.logging import get_logger
|
||||
from . import claude
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
app = typer.Typer(
|
||||
name="fastmcp",
|
||||
help="FastMCP development tools",
|
||||
add_completion=False,
|
||||
no_args_is_help=True, # Show help if no args provided
|
||||
)
|
||||
|
||||
|
||||
def _build_uv_command(
|
||||
file: Path,
|
||||
uv_directory: Optional[Path] = None,
|
||||
) -> list[str]:
|
||||
"""Build the uv run command."""
|
||||
cmd = ["uv"]
|
||||
|
||||
if uv_directory:
|
||||
cmd.extend(["--directory", str(uv_directory)])
|
||||
|
||||
cmd.extend(["run", str(file)])
|
||||
return cmd
|
||||
|
||||
|
||||
def _parse_file_path(file_spec: str) -> Tuple[Path, Optional[str]]:
|
||||
"""Parse a file path that may include a server object specification.
|
||||
|
||||
Args:
|
||||
file_spec: Path to file, optionally with :object suffix
|
||||
|
||||
Returns:
|
||||
Tuple of (file_path, server_object)
|
||||
"""
|
||||
if ":" in file_spec:
|
||||
file_str, server_object = file_spec.rsplit(":", 1)
|
||||
else:
|
||||
file_str, server_object = file_spec, None
|
||||
|
||||
file_path = Path(file_str).expanduser().resolve()
|
||||
if not file_path.exists():
|
||||
logger.error(f"File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
if not file_path.is_file():
|
||||
logger.error(f"Not a file: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
return file_path, server_object
|
||||
|
||||
|
||||
def _import_server(file: Path, server_object: Optional[str] = None):
|
||||
"""Import a FastMCP server from a file.
|
||||
|
||||
Args:
|
||||
file: Path to the file
|
||||
server_object: Optional object name in format "module:object" or just "object"
|
||||
|
||||
Returns:
|
||||
The server object
|
||||
"""
|
||||
# Import the module
|
||||
spec = importlib.util.spec_from_file_location("server_module", file)
|
||||
if not spec or not spec.loader:
|
||||
logger.error("Could not load module", extra={"file": str(file)})
|
||||
sys.exit(1)
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# If no object specified, try __main__ block
|
||||
if not server_object:
|
||||
# Look for the most common server object names
|
||||
for name in ["mcp", "server", "app"]:
|
||||
if hasattr(module, name):
|
||||
return getattr(module, name)
|
||||
|
||||
logger.error(
|
||||
f"No server object found in {file}. Please specify the object name with file:object syntax.",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Handle module:object syntax
|
||||
if ":" in server_object:
|
||||
module_name, object_name = server_object.split(":", 1)
|
||||
try:
|
||||
server_module = importlib.import_module(module_name)
|
||||
server = getattr(server_module, object_name, None)
|
||||
except ImportError:
|
||||
logger.error(
|
||||
f"Could not import module '{module_name}'",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Just object name
|
||||
server = getattr(module, server_object, None)
|
||||
|
||||
if server is None:
|
||||
logger.error(
|
||||
f"Server object '{server_object}' not found",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
return server
|
||||
|
||||
|
||||
@app.command()
|
||||
def version() -> None:
|
||||
"""Show the FastMCP version."""
|
||||
try:
|
||||
version = importlib.metadata.version("fastmcp")
|
||||
print(f"FastMCP version {version}")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
print("FastMCP version unknown (package not installed)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def dev(
|
||||
file_spec: str = typer.Argument(
|
||||
...,
|
||||
help="Python file to run, optionally with :object suffix",
|
||||
),
|
||||
uv_directory: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option(
|
||||
"--uv-directory",
|
||||
"-d",
|
||||
help="Directory containing pyproject.toml (defaults to current directory)",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Run a FastMCP server with the MCP Inspector."""
|
||||
file, server_object = _parse_file_path(file_spec)
|
||||
|
||||
logger.debug(
|
||||
"Starting dev server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"server_object": server_object,
|
||||
"uv_directory": str(uv_directory) if uv_directory else None,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
uv_cmd = _build_uv_command(file, uv_directory)
|
||||
# Run the MCP Inspector command
|
||||
process = subprocess.run(
|
||||
["npx", "@modelcontextprotocol/inspector"] + uv_cmd,
|
||||
check=True,
|
||||
)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
"Dev server failed",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"error": str(e),
|
||||
"returncode": e.returncode,
|
||||
},
|
||||
)
|
||||
sys.exit(e.returncode)
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
"npx not found. Please install Node.js and npm.",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def run(
|
||||
file_spec: str = typer.Argument(
|
||||
...,
|
||||
help="Python file to run, optionally with :object suffix",
|
||||
),
|
||||
transport: Annotated[
|
||||
Optional[str],
|
||||
typer.Option(
|
||||
"--transport",
|
||||
"-t",
|
||||
help="Transport protocol to use (stdio or sse)",
|
||||
),
|
||||
] = None,
|
||||
uv_directory: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option(
|
||||
"--uv-directory",
|
||||
"-d",
|
||||
help="Directory containing pyproject.toml (defaults to current directory)",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Run a FastMCP server."""
|
||||
file, server_object = _parse_file_path(file_spec)
|
||||
|
||||
logger.debug(
|
||||
"Running server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"server_object": server_object,
|
||||
"transport": transport,
|
||||
"uv_directory": str(uv_directory) if uv_directory else None,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
uv_cmd = _build_uv_command(file, uv_directory)
|
||||
|
||||
# Import and get server object
|
||||
server = _import_server(file, server_object)
|
||||
|
||||
# Run the server
|
||||
kwargs = {}
|
||||
if transport:
|
||||
kwargs["transport"] = transport
|
||||
|
||||
server.run(**kwargs)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to run server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"error": str(e),
|
||||
},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def install(
|
||||
file_spec: str = typer.Argument(
|
||||
...,
|
||||
help="Python file to run, optionally with :object suffix",
|
||||
),
|
||||
server_name: Annotated[
|
||||
Optional[str],
|
||||
typer.Option(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Custom name for the server (defaults to file name)",
|
||||
),
|
||||
] = None,
|
||||
uv_directory: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option(
|
||||
"--uv-directory",
|
||||
"-d",
|
||||
help="Directory containing pyproject.toml (defaults to current directory)",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Install a FastMCP server in the Claude desktop app."""
|
||||
file, server_object = _parse_file_path(file_spec)
|
||||
|
||||
logger.debug(
|
||||
"Installing server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"server_name": server_name,
|
||||
"server_object": server_object,
|
||||
"uv_directory": str(uv_directory) if uv_directory else None,
|
||||
},
|
||||
)
|
||||
|
||||
if not claude.get_claude_config_path():
|
||||
logger.error("Claude app not found")
|
||||
sys.exit(1)
|
||||
|
||||
if claude.update_claude_config(file, server_name, uv_directory=uv_directory):
|
||||
name = server_name or file.stem
|
||||
print(f"Successfully installed {name} in Claude app")
|
||||
else:
|
||||
name = server_name or file.stem
|
||||
print(f"Failed to install {name} in Claude app")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
Loading…
Add table
Add a link
Reference in a new issue