Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
ae4f592
1
Parent(s): 28d92a7
Support dependencies
Browse files- README.md +86 -62
- examples/screenshot.py +8 -8
- src/fastmcp/cli/claude.py +10 -6
- src/fastmcp/cli/cli.py +15 -12
- src/fastmcp/server.py +7 -0
- tests/test_cli.py +118 -42
README.md
CHANGED
|
@@ -62,15 +62,20 @@ FastMCP handles all the complex protocol details and server management, so you c
|
|
| 62 |
- [Prompts](#prompts)
|
| 63 |
- [Images](#images)
|
| 64 |
- [Context](#context)
|
| 65 |
-
- [
|
| 66 |
-
- [Development](#development)
|
| 67 |
-
|
| 68 |
-
- [
|
| 69 |
-
|
| 70 |
- [Examples](#examples)
|
| 71 |
- [Echo Server](#echo-server)
|
| 72 |
- [SQLite Explorer](#sqlite-explorer)
|
| 73 |
- [Contributing](#contributing)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
## Installation
|
| 76 |
|
|
@@ -154,8 +159,10 @@ mcp = FastMCP("My App")
|
|
| 154 |
|
| 155 |
# Configure host/port for HTTP transport (optional)
|
| 156 |
mcp = FastMCP("My App", host="localhost", port=8000)
|
|
|
|
|
|
|
|
|
|
| 157 |
```
|
| 158 |
-
*Note: All of the following code examples assume you've created a FastMCP server instance called `mcp`, as shown above.*
|
| 159 |
|
| 160 |
### Resources
|
| 161 |
|
|
@@ -284,84 +291,101 @@ The Context object provides:
|
|
| 284 |
- Resource access through `read_resource()`
|
| 285 |
- Request metadata via `request_id` and `client_id`
|
| 286 |
|
| 287 |
-
##
|
| 288 |
-
|
| 289 |
-
The FastMCP CLI helps you develop and deploy MCP servers.
|
| 290 |
|
| 291 |
-
|
| 292 |
|
| 293 |
-
|
| 294 |
|
| 295 |
-
|
| 296 |
|
| 297 |
-
Test and debug your server with the MCP Inspector:
|
| 298 |
```bash
|
| 299 |
-
# Provide the fully qualified path to your server
|
| 300 |
-
fastmcp dev server.py:my_mcp_server
|
| 301 |
-
|
| 302 |
-
# Or just the file if your server is named 'mcp', 'server', or 'app'
|
| 303 |
fastmcp dev server.py
|
| 304 |
```
|
| 305 |
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
-
#
|
| 313 |
-
fastmcp dev server.py --with-editable .
|
| 314 |
-
```
|
| 315 |
|
| 316 |
-
|
| 317 |
|
| 318 |
-
The MCP Inspector runs servers in an isolated environment. Environment variables must be set through the Inspector UI and are not inherited from your system. The Inspector does not currently support setting environment variables via command line (see [Issue #94](https://github.com/modelcontextprotocol/inspector/issues/94)).
|
| 319 |
-
|
| 320 |
-
### Claude Desktop
|
| 321 |
-
|
| 322 |
-
Install your server in Claude Desktop:
|
| 323 |
```bash
|
| 324 |
-
# Basic usage (name is taken from your FastMCP instance)
|
| 325 |
fastmcp install server.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
-
|
| 328 |
-
fastmcp
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
| 332 |
```
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
|
| 339 |
-
#
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
-
Claude Desktop runs servers in an isolated environment. Environment variables from your system are NOT automatically available to the server - you must explicitly provide them during installation:
|
| 342 |
|
| 343 |
-
|
| 344 |
-
# Single env var
|
| 345 |
-
fastmcp install server.py -e API_KEY=abc123
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
| 349 |
|
| 350 |
-
#
|
| 351 |
-
fastmcp install server.py -f .env
|
| 352 |
-
```
|
| 353 |
|
| 354 |
-
|
| 355 |
|
| 356 |
```bash
|
| 357 |
-
#
|
| 358 |
-
fastmcp
|
| 359 |
-
|
| 360 |
-
# Second install - FOO and BAZ are preserved
|
| 361 |
-
fastmcp install server.py -e NEW=value
|
| 362 |
|
| 363 |
-
#
|
| 364 |
-
fastmcp
|
| 365 |
```
|
| 366 |
|
| 367 |
## Examples
|
|
@@ -437,11 +461,11 @@ What insights can you provide about the structure and relationships?"""
|
|
| 437 |
|
| 438 |
<summary><h3>Open Developer Guide</h3></summary>
|
| 439 |
|
| 440 |
-
###
|
| 441 |
|
| 442 |
FastMCP requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
| 443 |
|
| 444 |
-
###
|
| 445 |
|
| 446 |
Create a fork of this repository, then clone it:
|
| 447 |
|
|
@@ -460,7 +484,7 @@ uv sync --frozen --all-extras --dev
|
|
| 460 |
|
| 461 |
|
| 462 |
|
| 463 |
-
###
|
| 464 |
|
| 465 |
Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns.
|
| 466 |
|
|
@@ -471,7 +495,7 @@ Run tests from the root directory:
|
|
| 471 |
pytest -vv
|
| 472 |
```
|
| 473 |
|
| 474 |
-
###
|
| 475 |
|
| 476 |
FastMCP enforces a variety of required formats, which you can automatically enforce with pre-commit.
|
| 477 |
|
|
@@ -487,7 +511,7 @@ The hooks will now run on every commit (as well as on every PR). To run them man
|
|
| 487 |
pre-commit run --all-files
|
| 488 |
```
|
| 489 |
|
| 490 |
-
###
|
| 491 |
|
| 492 |
Fork the repository and create a new branch:
|
| 493 |
|
|
|
|
| 62 |
- [Prompts](#prompts)
|
| 63 |
- [Images](#images)
|
| 64 |
- [Context](#context)
|
| 65 |
+
- [Running Your Server](#running-your-server)
|
| 66 |
+
- [Development Mode (Recommended for Building \& Testing)](#development-mode-recommended-for-building--testing)
|
| 67 |
+
- [Claude Desktop Integration (For Regular Use)](#claude-desktop-integration-for-regular-use)
|
| 68 |
+
- [Direct Execution (For Advanced Use Cases)](#direct-execution-for-advanced-use-cases)
|
| 69 |
+
- [Server Object Names](#server-object-names)
|
| 70 |
- [Examples](#examples)
|
| 71 |
- [Echo Server](#echo-server)
|
| 72 |
- [SQLite Explorer](#sqlite-explorer)
|
| 73 |
- [Contributing](#contributing)
|
| 74 |
+
- [Prerequisites](#prerequisites)
|
| 75 |
+
- [Installation](#installation-1)
|
| 76 |
+
- [Testing](#testing)
|
| 77 |
+
- [Formatting](#formatting)
|
| 78 |
+
- [Opening a Pull Request](#opening-a-pull-request)
|
| 79 |
|
| 80 |
## Installation
|
| 81 |
|
|
|
|
| 159 |
|
| 160 |
# Configure host/port for HTTP transport (optional)
|
| 161 |
mcp = FastMCP("My App", host="localhost", port=8000)
|
| 162 |
+
|
| 163 |
+
# Specify dependencies for deployment and development
|
| 164 |
+
mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
|
| 165 |
```
|
|
|
|
| 166 |
|
| 167 |
### Resources
|
| 168 |
|
|
|
|
| 291 |
- Resource access through `read_resource()`
|
| 292 |
- Request metadata via `request_id` and `client_id`
|
| 293 |
|
| 294 |
+
## Running Your Server
|
|
|
|
|
|
|
| 295 |
|
| 296 |
+
There are three main ways to use your FastMCP server, each suited for different stages of development:
|
| 297 |
|
| 298 |
+
### Development Mode (Recommended for Building & Testing)
|
| 299 |
|
| 300 |
+
The fastest way to test and debug your server is with the MCP Inspector:
|
| 301 |
|
|
|
|
| 302 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
fastmcp dev server.py
|
| 304 |
```
|
| 305 |
|
| 306 |
+
This launches a web interface where you can:
|
| 307 |
+
- Test your tools and resources interactively
|
| 308 |
+
- See detailed logs and error messages
|
| 309 |
+
- Monitor server performance
|
| 310 |
+
- Set environment variables for testing
|
| 311 |
|
| 312 |
+
During development, you can:
|
| 313 |
+
- Add dependencies with `--with`:
|
| 314 |
+
```bash
|
| 315 |
+
fastmcp dev server.py --with pandas --with numpy
|
| 316 |
+
```
|
| 317 |
+
- Mount your local code for live updates:
|
| 318 |
+
```bash
|
| 319 |
+
fastmcp dev server.py --with-editable .
|
| 320 |
+
```
|
| 321 |
|
| 322 |
+
### Claude Desktop Integration (For Regular Use)
|
|
|
|
|
|
|
| 323 |
|
| 324 |
+
Once your server is ready, install it in Claude Desktop to use it with Claude:
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
```bash
|
|
|
|
| 327 |
fastmcp install server.py
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
Your server will run in an isolated environment with:
|
| 331 |
+
- Automatic installation of dependencies specified in your FastMCP instance:
|
| 332 |
+
```python
|
| 333 |
+
mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
|
| 334 |
+
```
|
| 335 |
+
- Custom naming via `--name`:
|
| 336 |
+
```bash
|
| 337 |
+
fastmcp install server.py --name "My Analytics Server"
|
| 338 |
+
```
|
| 339 |
+
- Environment variable management:
|
| 340 |
+
```bash
|
| 341 |
+
# Set variables individually
|
| 342 |
+
fastmcp install server.py -e API_KEY=abc123 -e DB_URL=postgres://...
|
| 343 |
+
|
| 344 |
+
# Or load from a .env file
|
| 345 |
+
fastmcp install server.py -f .env
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
### Direct Execution (For Advanced Use Cases)
|
| 349 |
+
|
| 350 |
+
For advanced scenarios like custom deployments or running without Claude, you can execute your server directly:
|
| 351 |
|
| 352 |
+
```python
|
| 353 |
+
from fastmcp import FastMCP
|
| 354 |
|
| 355 |
+
mcp = FastMCP("My App")
|
| 356 |
+
|
| 357 |
+
if __name__ == "__main__":
|
| 358 |
+
mcp.run()
|
| 359 |
```
|
| 360 |
|
| 361 |
+
Run it with:
|
| 362 |
+
```bash
|
| 363 |
+
# Using the FastMCP CLI
|
| 364 |
+
fastmcp run server.py
|
| 365 |
|
| 366 |
+
# Or with Python/uv directly
|
| 367 |
+
python server.py
|
| 368 |
+
uv run python server.py
|
| 369 |
+
```
|
| 370 |
|
|
|
|
| 371 |
|
| 372 |
+
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.
|
|
|
|
|
|
|
| 373 |
|
| 374 |
+
Choose this method when you need:
|
| 375 |
+
- Custom deployment configurations
|
| 376 |
+
- Integration with other services
|
| 377 |
+
- Direct control over the server lifecycle
|
| 378 |
|
| 379 |
+
### Server Object Names
|
|
|
|
|
|
|
| 380 |
|
| 381 |
+
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`:
|
| 382 |
|
| 383 |
```bash
|
| 384 |
+
# Using a standard name
|
| 385 |
+
fastmcp run server.py
|
|
|
|
|
|
|
|
|
|
| 386 |
|
| 387 |
+
# Using a custom name
|
| 388 |
+
fastmcp run server.py:my_custom_server
|
| 389 |
```
|
| 390 |
|
| 391 |
## Examples
|
|
|
|
| 461 |
|
| 462 |
<summary><h3>Open Developer Guide</h3></summary>
|
| 463 |
|
| 464 |
+
### Prerequisites
|
| 465 |
|
| 466 |
FastMCP requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
| 467 |
|
| 468 |
+
### Installation
|
| 469 |
|
| 470 |
Create a fork of this repository, then clone it:
|
| 471 |
|
|
|
|
| 484 |
|
| 485 |
|
| 486 |
|
| 487 |
+
### Testing
|
| 488 |
|
| 489 |
Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns.
|
| 490 |
|
|
|
|
| 495 |
pytest -vv
|
| 496 |
```
|
| 497 |
|
| 498 |
+
### Formatting
|
| 499 |
|
| 500 |
FastMCP enforces a variety of required formats, which you can automatically enforce with pre-commit.
|
| 501 |
|
|
|
|
| 511 |
pre-commit run --all-files
|
| 512 |
```
|
| 513 |
|
| 514 |
+
### Opening a Pull Request
|
| 515 |
|
| 516 |
Fork the repository and create a new branch:
|
| 517 |
|
examples/screenshot.py
CHANGED
|
@@ -1,7 +1,3 @@
|
|
| 1 |
-
# /// script
|
| 2 |
-
# dependencies = ["fastmcp", "pyautogui", "Pillow"]
|
| 3 |
-
# ///
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
FastMCP Screenshot Example
|
| 7 |
|
|
@@ -9,20 +5,24 @@ Give Claude a tool to capture and view screenshots.
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
import io
|
| 12 |
-
|
| 13 |
from fastmcp import FastMCP, Image
|
| 14 |
|
|
|
|
| 15 |
# Create server
|
| 16 |
-
mcp = FastMCP("Screenshot Demo")
|
| 17 |
|
| 18 |
|
| 19 |
@mcp.tool()
|
| 20 |
def take_screenshot() -> Image:
|
| 21 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 22 |
import pyautogui
|
| 23 |
|
| 24 |
-
screenshot = pyautogui.screenshot()
|
| 25 |
buffer = io.BytesIO()
|
|
|
|
| 26 |
# if the file exceeds ~1MB, it will be rejected by Claude
|
|
|
|
| 27 |
screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True)
|
| 28 |
return Image(data=buffer.getvalue(), format="jpeg")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
FastMCP Screenshot Example
|
| 3 |
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import io
|
|
|
|
| 8 |
from fastmcp import FastMCP, Image
|
| 9 |
|
| 10 |
+
|
| 11 |
# Create server
|
| 12 |
+
mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"])
|
| 13 |
|
| 14 |
|
| 15 |
@mcp.tool()
|
| 16 |
def take_screenshot() -> Image:
|
| 17 |
+
"""
|
| 18 |
+
Take a screenshot of the user's screen and return it as an image. Use
|
| 19 |
+
this tool anytime the user wants you to look at something they're doing.
|
| 20 |
+
"""
|
| 21 |
import pyautogui
|
| 22 |
|
|
|
|
| 23 |
buffer = io.BytesIO()
|
| 24 |
+
|
| 25 |
# if the file exceeds ~1MB, it will be rejected by Claude
|
| 26 |
+
screenshot = pyautogui.screenshot()
|
| 27 |
screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True)
|
| 28 |
return Image(data=buffer.getvalue(), format="jpeg")
|
src/fastmcp/cli/claude.py
CHANGED
|
@@ -68,16 +68,20 @@ def update_claude_config(
|
|
| 68 |
env_vars = existing_env
|
| 69 |
|
| 70 |
# Build uv run command
|
| 71 |
-
args = ["run"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
if with_editable:
|
| 74 |
args.extend(["--with-editable", str(with_editable)])
|
| 75 |
|
| 76 |
-
if with_packages:
|
| 77 |
-
for pkg in with_packages:
|
| 78 |
-
if pkg:
|
| 79 |
-
args.extend(["--with", pkg])
|
| 80 |
-
|
| 81 |
# Convert file path to absolute before adding to command
|
| 82 |
# Split off any :object suffix first
|
| 83 |
if ":" in file_spec:
|
|
|
|
| 68 |
env_vars = existing_env
|
| 69 |
|
| 70 |
# Build uv run command
|
| 71 |
+
args = ["run"]
|
| 72 |
+
|
| 73 |
+
# Collect all packages in a set to deduplicate
|
| 74 |
+
packages = {"fastmcp"}
|
| 75 |
+
if with_packages:
|
| 76 |
+
packages.update(pkg for pkg in with_packages if pkg)
|
| 77 |
+
|
| 78 |
+
# Add all packages with --with
|
| 79 |
+
for pkg in sorted(packages):
|
| 80 |
+
args.extend(["--with", pkg])
|
| 81 |
|
| 82 |
if with_editable:
|
| 83 |
args.extend(["--with-editable", str(with_editable)])
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
# Convert file path to absolute before adding to command
|
| 86 |
# Split off any :object suffix first
|
| 87 |
if ":" in file_spec:
|
src/fastmcp/cli/cli.py
CHANGED
|
@@ -193,6 +193,11 @@ def dev(
|
|
| 193 |
)
|
| 194 |
|
| 195 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
| 197 |
# Run the MCP Inspector command
|
| 198 |
process = subprocess.run(
|
|
@@ -232,23 +237,16 @@ def run(
|
|
| 232 |
help="Transport protocol to use (stdio or sse)",
|
| 233 |
),
|
| 234 |
] = None,
|
| 235 |
-
with_editable: Annotated[
|
| 236 |
-
Optional[Path],
|
| 237 |
-
typer.Option(
|
| 238 |
-
"--with-editable",
|
| 239 |
-
"-e",
|
| 240 |
-
help="Directory containing pyproject.toml to install in editable mode",
|
| 241 |
-
exists=True,
|
| 242 |
-
file_okay=False,
|
| 243 |
-
resolve_path=True,
|
| 244 |
-
),
|
| 245 |
-
] = None,
|
| 246 |
) -> None:
|
| 247 |
"""Run a FastMCP server.
|
| 248 |
|
| 249 |
The server can be specified in two ways:
|
| 250 |
1. Module approach: server.py - runs the module directly, expecting a server.run() call
|
| 251 |
2. Import approach: server.py:app - imports and runs the specified server object
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
"""
|
| 253 |
file, server_object = _parse_file_path(file_spec)
|
| 254 |
|
|
@@ -258,7 +256,6 @@ def run(
|
|
| 258 |
"file": str(file),
|
| 259 |
"server_object": server_object,
|
| 260 |
"transport": transport,
|
| 261 |
-
"with_editable": str(with_editable) if with_editable else None,
|
| 262 |
},
|
| 263 |
)
|
| 264 |
|
|
@@ -361,6 +358,7 @@ def install(
|
|
| 361 |
|
| 362 |
# Try to import server to get its name, but fall back to file name if dependencies missing
|
| 363 |
name = server_name
|
|
|
|
| 364 |
if not name:
|
| 365 |
try:
|
| 366 |
server = _import_server(file, server_object)
|
|
@@ -372,6 +370,11 @@ def install(
|
|
| 372 |
)
|
| 373 |
name = file.stem
|
| 374 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
# Process environment variables if provided
|
| 376 |
env_dict: Optional[Dict[str, str]] = None
|
| 377 |
if env_file or env_vars:
|
|
|
|
| 193 |
)
|
| 194 |
|
| 195 |
try:
|
| 196 |
+
# Import server to get dependencies
|
| 197 |
+
server = _import_server(file, server_object)
|
| 198 |
+
if hasattr(server, "dependencies"):
|
| 199 |
+
with_packages = list(set(with_packages + server.dependencies))
|
| 200 |
+
|
| 201 |
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
| 202 |
# Run the MCP Inspector command
|
| 203 |
process = subprocess.run(
|
|
|
|
| 237 |
help="Transport protocol to use (stdio or sse)",
|
| 238 |
),
|
| 239 |
] = None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
) -> None:
|
| 241 |
"""Run a FastMCP server.
|
| 242 |
|
| 243 |
The server can be specified in two ways:
|
| 244 |
1. Module approach: server.py - runs the module directly, expecting a server.run() call
|
| 245 |
2. Import approach: server.py:app - imports and runs the specified server object
|
| 246 |
+
|
| 247 |
+
Note: This command runs the server directly. You are responsible for ensuring
|
| 248 |
+
all dependencies are available. For dependency management, use fastmcp install
|
| 249 |
+
or fastmcp dev instead.
|
| 250 |
"""
|
| 251 |
file, server_object = _parse_file_path(file_spec)
|
| 252 |
|
|
|
|
| 256 |
"file": str(file),
|
| 257 |
"server_object": server_object,
|
| 258 |
"transport": transport,
|
|
|
|
| 259 |
},
|
| 260 |
)
|
| 261 |
|
|
|
|
| 358 |
|
| 359 |
# Try to import server to get its name, but fall back to file name if dependencies missing
|
| 360 |
name = server_name
|
| 361 |
+
server = None
|
| 362 |
if not name:
|
| 363 |
try:
|
| 364 |
server = _import_server(file, server_object)
|
|
|
|
| 370 |
)
|
| 371 |
name = file.stem
|
| 372 |
|
| 373 |
+
# Get server dependencies if available
|
| 374 |
+
server_dependencies = getattr(server, "dependencies", []) if server else []
|
| 375 |
+
if server_dependencies:
|
| 376 |
+
with_packages = list(set(with_packages + server_dependencies))
|
| 377 |
+
|
| 378 |
# Process environment variables if provided
|
| 379 |
env_dict: Optional[Dict[str, str]] = None
|
| 380 |
if env_file or env_vars:
|
src/fastmcp/server.py
CHANGED
|
@@ -9,6 +9,7 @@ from itertools import chain
|
|
| 9 |
from typing import Any, Callable, Dict, Literal, Sequence
|
| 10 |
|
| 11 |
import pydantic_core
|
|
|
|
| 12 |
import uvicorn
|
| 13 |
from mcp.server import Server as MCPServer
|
| 14 |
from mcp.server.sse import SseServerTransport
|
|
@@ -76,6 +77,11 @@ class Settings(BaseSettings):
|
|
| 76 |
# prompt settings
|
| 77 |
warn_on_duplicate_prompts: bool = True
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
class FastMCP:
|
| 81 |
def __init__(self, name: str | None = None, **settings: Any):
|
|
@@ -90,6 +96,7 @@ class FastMCP:
|
|
| 90 |
self._prompt_manager = PromptManager(
|
| 91 |
warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
|
| 92 |
)
|
|
|
|
| 93 |
|
| 94 |
# Set up MCP protocol handlers
|
| 95 |
self._setup_handlers()
|
|
|
|
| 9 |
from typing import Any, Callable, Dict, Literal, Sequence
|
| 10 |
|
| 11 |
import pydantic_core
|
| 12 |
+
from pydantic import Field
|
| 13 |
import uvicorn
|
| 14 |
from mcp.server import Server as MCPServer
|
| 15 |
from mcp.server.sse import SseServerTransport
|
|
|
|
| 77 |
# prompt settings
|
| 78 |
warn_on_duplicate_prompts: bool = True
|
| 79 |
|
| 80 |
+
dependencies: list[str] = Field(
|
| 81 |
+
default_factory=list,
|
| 82 |
+
description="List of dependencies to install in the server environment",
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
|
| 86 |
class FastMCP:
|
| 87 |
def __init__(self, name: str | None = None, **settings: Any):
|
|
|
|
| 96 |
self._prompt_manager = PromptManager(
|
| 97 |
warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
|
| 98 |
)
|
| 99 |
+
self.dependencies = self.settings.dependencies
|
| 100 |
|
| 101 |
# Set up MCP protocol handlers
|
| 102 |
self._setup_handlers()
|
tests/test_cli.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""Tests for the FastMCP CLI."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
-
from unittest.mock import
|
| 5 |
|
| 6 |
import pytest
|
| 7 |
from typer.testing import CliRunner
|
|
@@ -19,11 +19,13 @@ def mock_config(tmp_path):
|
|
| 19 |
|
| 20 |
|
| 21 |
@pytest.fixture
|
| 22 |
-
def
|
| 23 |
-
"""Create a
|
| 24 |
server_file = tmp_path / "server.py"
|
| 25 |
server_file.write_text(
|
| 26 |
-
"from fastmcp import
|
|
|
|
|
|
|
| 27 |
)
|
| 28 |
return server_file
|
| 29 |
|
|
@@ -67,22 +69,16 @@ def test_parse_env_var():
|
|
| 67 |
),
|
| 68 |
],
|
| 69 |
)
|
| 70 |
-
def test_install_with_env_vars(mock_config,
|
| 71 |
"""Test installing with environment variables."""
|
| 72 |
runner = CliRunner()
|
| 73 |
|
| 74 |
-
with (
|
| 75 |
-
patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
|
| 76 |
-
patch("fastmcp.cli.cli._import_server") as mock_import,
|
| 77 |
-
):
|
| 78 |
mock_config_path.return_value = mock_config.parent
|
| 79 |
-
mock_server = Mock()
|
| 80 |
-
mock_server.name = "test" # Set name as an attribute
|
| 81 |
-
mock_import.return_value = mock_server
|
| 82 |
|
| 83 |
result = runner.invoke(
|
| 84 |
app,
|
| 85 |
-
["install", str(
|
| 86 |
)
|
| 87 |
|
| 88 |
assert result.exit_code == 0
|
|
@@ -95,22 +91,16 @@ def test_install_with_env_vars(mock_config, mock_server_file, args, expected_env
|
|
| 95 |
assert server["env"] == expected_env
|
| 96 |
|
| 97 |
|
| 98 |
-
def test_install_with_env_file(mock_config,
|
| 99 |
"""Test installing with environment variables from a file."""
|
| 100 |
runner = CliRunner()
|
| 101 |
|
| 102 |
-
with (
|
| 103 |
-
patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
|
| 104 |
-
patch("fastmcp.cli.cli._import_server") as mock_import,
|
| 105 |
-
):
|
| 106 |
mock_config_path.return_value = mock_config.parent
|
| 107 |
-
mock_server = Mock()
|
| 108 |
-
mock_server.name = "test" # Set name as an attribute
|
| 109 |
-
mock_import.return_value = mock_server
|
| 110 |
|
| 111 |
result = runner.invoke(
|
| 112 |
app,
|
| 113 |
-
["install", str(
|
| 114 |
)
|
| 115 |
|
| 116 |
assert result.exit_code == 0
|
|
@@ -123,7 +113,7 @@ def test_install_with_env_file(mock_config, mock_server_file, mock_env_file):
|
|
| 123 |
assert server["env"] == {"FOO": "bar", "BAZ": "123"}
|
| 124 |
|
| 125 |
|
| 126 |
-
def test_install_preserves_existing_env_vars(mock_config,
|
| 127 |
"""Test that installing preserves existing environment variables."""
|
| 128 |
# Set up initial config with env vars
|
| 129 |
config = {
|
|
@@ -136,7 +126,7 @@ def test_install_preserves_existing_env_vars(mock_config, mock_server_file):
|
|
| 136 |
"fastmcp",
|
| 137 |
"fastmcp",
|
| 138 |
"run",
|
| 139 |
-
str(
|
| 140 |
],
|
| 141 |
"env": {"FOO": "bar", "BAZ": "123"},
|
| 142 |
}
|
|
@@ -146,19 +136,13 @@ def test_install_preserves_existing_env_vars(mock_config, mock_server_file):
|
|
| 146 |
|
| 147 |
runner = CliRunner()
|
| 148 |
|
| 149 |
-
with (
|
| 150 |
-
patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
|
| 151 |
-
patch("fastmcp.cli.cli._import_server") as mock_import,
|
| 152 |
-
):
|
| 153 |
mock_config_path.return_value = mock_config.parent
|
| 154 |
-
mock_server = Mock()
|
| 155 |
-
mock_server.name = "test" # Set name as an attribute
|
| 156 |
-
mock_import.return_value = mock_server
|
| 157 |
|
| 158 |
# Install with a new env var
|
| 159 |
result = runner.invoke(
|
| 160 |
app,
|
| 161 |
-
["install", str(
|
| 162 |
)
|
| 163 |
|
| 164 |
assert result.exit_code == 0
|
|
@@ -169,7 +153,7 @@ def test_install_preserves_existing_env_vars(mock_config, mock_server_file):
|
|
| 169 |
assert server["env"] == {"FOO": "bar", "BAZ": "123", "NEW": "value"}
|
| 170 |
|
| 171 |
|
| 172 |
-
def test_install_updates_existing_env_vars(mock_config,
|
| 173 |
"""Test that installing updates existing environment variables."""
|
| 174 |
# Set up initial config with env vars
|
| 175 |
config = {
|
|
@@ -182,7 +166,7 @@ def test_install_updates_existing_env_vars(mock_config, mock_server_file):
|
|
| 182 |
"fastmcp",
|
| 183 |
"fastmcp",
|
| 184 |
"run",
|
| 185 |
-
str(
|
| 186 |
],
|
| 187 |
"env": {"FOO": "bar", "BAZ": "123"},
|
| 188 |
}
|
|
@@ -192,19 +176,13 @@ def test_install_updates_existing_env_vars(mock_config, mock_server_file):
|
|
| 192 |
|
| 193 |
runner = CliRunner()
|
| 194 |
|
| 195 |
-
with (
|
| 196 |
-
patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
|
| 197 |
-
patch("fastmcp.cli.cli._import_server") as mock_import,
|
| 198 |
-
):
|
| 199 |
mock_config_path.return_value = mock_config.parent
|
| 200 |
-
mock_server = Mock()
|
| 201 |
-
mock_server.name = "test" # Set name as an attribute
|
| 202 |
-
mock_import.return_value = mock_server
|
| 203 |
|
| 204 |
# Update an existing env var
|
| 205 |
result = runner.invoke(
|
| 206 |
app,
|
| 207 |
-
["install", str(
|
| 208 |
)
|
| 209 |
|
| 210 |
assert result.exit_code == 0
|
|
@@ -213,3 +191,101 @@ def test_install_updates_existing_env_vars(mock_config, mock_server_file):
|
|
| 213 |
config = json.loads(mock_config.read_text())
|
| 214 |
server = next(iter(config["mcpServers"].values()))
|
| 215 |
assert server["env"] == {"FOO": "newvalue", "BAZ": "123"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Tests for the FastMCP CLI."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
+
from unittest.mock import patch
|
| 5 |
|
| 6 |
import pytest
|
| 7 |
from typer.testing import CliRunner
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
@pytest.fixture
|
| 22 |
+
def server_file(tmp_path):
|
| 23 |
+
"""Create a server file."""
|
| 24 |
server_file = tmp_path / "server.py"
|
| 25 |
server_file.write_text(
|
| 26 |
+
"""from fastmcp import FastMCP
|
| 27 |
+
mcp = FastMCP("test")
|
| 28 |
+
"""
|
| 29 |
)
|
| 30 |
return server_file
|
| 31 |
|
|
|
|
| 69 |
),
|
| 70 |
],
|
| 71 |
)
|
| 72 |
+
def test_install_with_env_vars(mock_config, server_file, args, expected_env):
|
| 73 |
"""Test installing with environment variables."""
|
| 74 |
runner = CliRunner()
|
| 75 |
|
| 76 |
+
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
|
|
|
|
|
|
|
|
| 77 |
mock_config_path.return_value = mock_config.parent
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
result = runner.invoke(
|
| 80 |
app,
|
| 81 |
+
["install", str(server_file)] + args,
|
| 82 |
)
|
| 83 |
|
| 84 |
assert result.exit_code == 0
|
|
|
|
| 91 |
assert server["env"] == expected_env
|
| 92 |
|
| 93 |
|
| 94 |
+
def test_install_with_env_file(mock_config, server_file, mock_env_file):
|
| 95 |
"""Test installing with environment variables from a file."""
|
| 96 |
runner = CliRunner()
|
| 97 |
|
| 98 |
+
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
|
|
|
|
|
|
|
|
| 99 |
mock_config_path.return_value = mock_config.parent
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
result = runner.invoke(
|
| 102 |
app,
|
| 103 |
+
["install", str(server_file), "--env-file", str(mock_env_file)],
|
| 104 |
)
|
| 105 |
|
| 106 |
assert result.exit_code == 0
|
|
|
|
| 113 |
assert server["env"] == {"FOO": "bar", "BAZ": "123"}
|
| 114 |
|
| 115 |
|
| 116 |
+
def test_install_preserves_existing_env_vars(mock_config, server_file):
|
| 117 |
"""Test that installing preserves existing environment variables."""
|
| 118 |
# Set up initial config with env vars
|
| 119 |
config = {
|
|
|
|
| 126 |
"fastmcp",
|
| 127 |
"fastmcp",
|
| 128 |
"run",
|
| 129 |
+
str(server_file),
|
| 130 |
],
|
| 131 |
"env": {"FOO": "bar", "BAZ": "123"},
|
| 132 |
}
|
|
|
|
| 136 |
|
| 137 |
runner = CliRunner()
|
| 138 |
|
| 139 |
+
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
|
|
|
|
|
|
|
|
| 140 |
mock_config_path.return_value = mock_config.parent
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
# Install with a new env var
|
| 143 |
result = runner.invoke(
|
| 144 |
app,
|
| 145 |
+
["install", str(server_file), "--env-var", "NEW=value"],
|
| 146 |
)
|
| 147 |
|
| 148 |
assert result.exit_code == 0
|
|
|
|
| 153 |
assert server["env"] == {"FOO": "bar", "BAZ": "123", "NEW": "value"}
|
| 154 |
|
| 155 |
|
| 156 |
+
def test_install_updates_existing_env_vars(mock_config, server_file):
|
| 157 |
"""Test that installing updates existing environment variables."""
|
| 158 |
# Set up initial config with env vars
|
| 159 |
config = {
|
|
|
|
| 166 |
"fastmcp",
|
| 167 |
"fastmcp",
|
| 168 |
"run",
|
| 169 |
+
str(server_file),
|
| 170 |
],
|
| 171 |
"env": {"FOO": "bar", "BAZ": "123"},
|
| 172 |
}
|
|
|
|
| 176 |
|
| 177 |
runner = CliRunner()
|
| 178 |
|
| 179 |
+
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
|
|
|
|
|
|
|
|
|
| 180 |
mock_config_path.return_value = mock_config.parent
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
# Update an existing env var
|
| 183 |
result = runner.invoke(
|
| 184 |
app,
|
| 185 |
+
["install", str(server_file), "--env-var", "FOO=newvalue"],
|
| 186 |
)
|
| 187 |
|
| 188 |
assert result.exit_code == 0
|
|
|
|
| 191 |
config = json.loads(mock_config.read_text())
|
| 192 |
server = next(iter(config["mcpServers"].values()))
|
| 193 |
assert server["env"] == {"FOO": "newvalue", "BAZ": "123"}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def test_server_dependencies(mock_config, server_file):
|
| 197 |
+
"""Test that server dependencies are correctly handled."""
|
| 198 |
+
# Create a server file with dependencies
|
| 199 |
+
server_file = server_file.parent / "server_with_deps.py"
|
| 200 |
+
server_file.write_text(
|
| 201 |
+
"""from fastmcp import FastMCP
|
| 202 |
+
mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
| 203 |
+
"""
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
runner = CliRunner()
|
| 207 |
+
|
| 208 |
+
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
| 209 |
+
mock_config_path.return_value = mock_config.parent
|
| 210 |
+
|
| 211 |
+
result = runner.invoke(app, ["install", str(server_file)])
|
| 212 |
+
|
| 213 |
+
assert result.exit_code == 0
|
| 214 |
+
|
| 215 |
+
# Read the config file and check dependencies were added as --with args
|
| 216 |
+
config = json.loads(mock_config.read_text())
|
| 217 |
+
server = next(iter(config["mcpServers"].values()))
|
| 218 |
+
assert "--with" in server["args"]
|
| 219 |
+
assert "pandas" in server["args"]
|
| 220 |
+
assert "numpy" in server["args"]
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def test_server_dependencies_empty(mock_config, server_file):
|
| 224 |
+
"""Test that server with no dependencies works correctly."""
|
| 225 |
+
runner = CliRunner()
|
| 226 |
+
|
| 227 |
+
with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
|
| 228 |
+
mock_config_path.return_value = mock_config.parent
|
| 229 |
+
|
| 230 |
+
result = runner.invoke(app, ["install", str(server_file)])
|
| 231 |
+
|
| 232 |
+
assert result.exit_code == 0
|
| 233 |
+
|
| 234 |
+
# Read the config file and check only fastmcp is in --with args
|
| 235 |
+
config = json.loads(mock_config.read_text())
|
| 236 |
+
server = next(iter(config["mcpServers"].values()))
|
| 237 |
+
assert server["args"].count("--with") == 1
|
| 238 |
+
assert "fastmcp" in server["args"]
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def test_dev_with_dependencies(mock_config, server_file):
|
| 242 |
+
"""Test that dev command handles dependencies correctly."""
|
| 243 |
+
# Create a server file with dependencies
|
| 244 |
+
server_file = server_file.parent / "server_with_deps.py"
|
| 245 |
+
server_file.write_text(
|
| 246 |
+
"""from fastmcp import FastMCP
|
| 247 |
+
mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
| 248 |
+
"""
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
runner = CliRunner()
|
| 252 |
+
|
| 253 |
+
with patch("subprocess.run") as mock_run:
|
| 254 |
+
mock_run.return_value.returncode = 0 # Set successful return code
|
| 255 |
+
result = runner.invoke(app, ["dev", str(server_file)])
|
| 256 |
+
assert result.exit_code == 0
|
| 257 |
+
|
| 258 |
+
# Check that dependencies were passed to subprocess.run
|
| 259 |
+
mock_run.assert_called_once()
|
| 260 |
+
args = mock_run.call_args[0][0]
|
| 261 |
+
assert "npx" in args
|
| 262 |
+
assert "@modelcontextprotocol/inspector" in args
|
| 263 |
+
assert "uv" in args
|
| 264 |
+
assert "run" in args
|
| 265 |
+
assert "--with" in args
|
| 266 |
+
assert "pandas" in args
|
| 267 |
+
assert "numpy" in args
|
| 268 |
+
assert "fastmcp" in args
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def test_run_with_dependencies(mock_config, server_file):
|
| 272 |
+
"""Test that run command does not handle dependencies."""
|
| 273 |
+
# Create a server file with dependencies
|
| 274 |
+
server_file = server_file.parent / "server_with_deps.py"
|
| 275 |
+
server_file.write_text(
|
| 276 |
+
"""from fastmcp import FastMCP
|
| 277 |
+
mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
| 278 |
+
|
| 279 |
+
if __name__ == "__main__":
|
| 280 |
+
mcp.run()
|
| 281 |
+
"""
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
runner = CliRunner()
|
| 285 |
+
|
| 286 |
+
with patch("subprocess.run") as mock_run:
|
| 287 |
+
result = runner.invoke(app, ["run", str(server_file)])
|
| 288 |
+
assert result.exit_code == 0
|
| 289 |
+
|
| 290 |
+
# Run command should not call subprocess.run
|
| 291 |
+
mock_run.assert_not_called()
|