mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
feat: introduce fastmcp.json configuration system (#1517)
This commit is contained in:
parent
108ad4b70d
commit
2d3d5392f1
40 changed files with 3917 additions and 108 deletions
539
docs/deployment/server-configuration.mdx
Normal file
539
docs/deployment/server-configuration.mdx
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
---
|
||||
title: Server Configuration with fastmcp.json
|
||||
sidebarTitle: Server Configuration
|
||||
description: Use fastmcp.json for declarative server configuration
|
||||
icon: file-code
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.11.4" />
|
||||
|
||||
FastMCP supports declarative configuration through `fastmcp.json` files. This is the canonical and preferred way to configure FastMCP projects, providing a single source of truth for server settings, dependencies, and deployment options that replaces complex command-line arguments.
|
||||
|
||||
## Overview
|
||||
|
||||
The `fastmcp.json` configuration file allows you to define all aspects of your FastMCP server in a structured, shareable format. Instead of remembering command-line arguments or writing shell scripts, you declare your server's configuration once and use it everywhere.
|
||||
|
||||
When you have a `fastmcp.json` file, running your server becomes as simple as:
|
||||
|
||||
```bash
|
||||
# Run the server using the configuration
|
||||
fastmcp run fastmcp.json
|
||||
|
||||
# Or if fastmcp.json exists in the current directory
|
||||
fastmcp run
|
||||
```
|
||||
|
||||
This configuration approach ensures reproducible deployments across different environments, from local development to production servers. It works seamlessly with Claude Desktop, VS Code extensions, and any MCP-compatible client.
|
||||
|
||||
## JSON Schema Support
|
||||
|
||||
FastMCP provides JSON schemas for IDE autocomplete and validation. Add the schema reference to your `fastmcp.json` for enhanced developer experience:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two schema URLs are available:
|
||||
- **Version-specific**: `https://gofastmcp.com/schemas/fastmcp_config/v1.json`
|
||||
- **Latest version**: `https://gofastmcp.com/schemas/fastmcp_config/latest.json`
|
||||
|
||||
Modern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.
|
||||
|
||||
## File Structure
|
||||
|
||||
The `fastmcp.json` file has three main sections, each controlling a different aspect of your server:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
// Python environment and dependencies
|
||||
},
|
||||
"deployment": {
|
||||
// Runtime configuration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only the `entrypoint` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
### Entrypoint
|
||||
|
||||
The entrypoint specifies which Python file and object contains your FastMCP server. This field is required and supports multiple formats to accommodate different project structures.
|
||||
|
||||
<Card icon="code" title="Entrypoint Configuration">
|
||||
<ParamField body="entrypoint" type="object | string" required>
|
||||
The server entry point. Can be specified in three formats:
|
||||
|
||||
**Object format** (recommended): Explicit file and object specification
|
||||
```json
|
||||
"entrypoint": {
|
||||
"file": "src/server.py",
|
||||
"object": "mcp"
|
||||
}
|
||||
```
|
||||
|
||||
**String with object**: File path with colon and object name
|
||||
```json
|
||||
"entrypoint": "src/server.py:app"
|
||||
```
|
||||
|
||||
**String format**: Simple path to Python file (searches for common names: mcp, server, app)
|
||||
```json
|
||||
"entrypoint": "server.py"
|
||||
```
|
||||
|
||||
<Expandable title="Path Resolution">
|
||||
- File paths are resolved relative to the configuration file's location
|
||||
- If your `fastmcp.json` is in a project root and references `src/server.py`, FastMCP will look for the server at `<project_root>/src/server.py`
|
||||
- When no object is specified, FastMCP automatically searches for common server names: `mcp`, `server`, or `app`
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
### Environment
|
||||
|
||||
The environment section configures Python dependencies and version requirements. When specified, FastMCP uses `uv` to create an isolated environment for your server, ensuring reproducible deployments across different systems.
|
||||
|
||||
<Card icon="code" title="Environment Configuration">
|
||||
<ParamField body="environment" type="object">
|
||||
Optional Python environment configuration. When any field is specified, FastMCP automatically creates an isolated environment using `uv`.
|
||||
|
||||
<Expandable title="Environment Fields">
|
||||
<ParamField body="python" type="string">
|
||||
Python version constraint. Examples:
|
||||
- Exact version: `"3.12"`
|
||||
- Minimum version: `">=3.10"`
|
||||
- Version range: `">=3.10,<3.13"`
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="dependencies" type="list[str]">
|
||||
List of pip packages with optional version specifiers (PEP 508 format).
|
||||
```json
|
||||
"dependencies": ["pandas>=2.0", "requests", "httpx"]
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="requirements" type="string">
|
||||
Path to a requirements.txt file, resolved relative to the config file location.
|
||||
```json
|
||||
"requirements": "requirements.txt"
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="project" type="string">
|
||||
Path to a project directory containing pyproject.toml for uv project management.
|
||||
```json
|
||||
"project": "."
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="editable" type="string">
|
||||
Path to a package to install in editable/development mode.
|
||||
```json
|
||||
"editable": "./my-package"
|
||||
```
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
When environment configuration is provided, FastMCP:
|
||||
1. Creates an isolated Python environment using `uv`
|
||||
2. Installs the specified dependencies
|
||||
3. Runs your server in this clean environment
|
||||
|
||||
### Deployment
|
||||
|
||||
The deployment section controls runtime configuration including transport protocol, networking, logging, and environment variables.
|
||||
|
||||
<Card icon="code" title="Deployment Configuration">
|
||||
<ParamField body="deployment" type="object">
|
||||
Optional runtime configuration for the server.
|
||||
|
||||
<Expandable title="Deployment Fields">
|
||||
<ParamField body="transport" type="string" default="stdio">
|
||||
Protocol for client communication:
|
||||
- `"stdio"`: Standard input/output for desktop clients
|
||||
- `"http"`: Network-accessible HTTP server
|
||||
- `"sse"`: Server-sent events
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="host" type="string" default="127.0.0.1">
|
||||
Network interface to bind (HTTP transport only):
|
||||
- `"127.0.0.1"`: Local connections only
|
||||
- `"0.0.0.0"`: All network interfaces
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="port" type="integer" default="3000">
|
||||
Port number for HTTP transport.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="path" type="string" default="/mcp/">
|
||||
URL path for the MCP endpoint when using HTTP transport.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="log_level" type="string" default="INFO">
|
||||
Server logging verbosity. Options:
|
||||
- `"DEBUG"`: Detailed debugging information
|
||||
- `"INFO"`: General informational messages
|
||||
- `"WARNING"`: Warning messages
|
||||
- `"ERROR"`: Error messages only
|
||||
- `"CRITICAL"`: Critical errors only
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="env" type="object">
|
||||
Environment variables to set when running the server. Supports `${VAR_NAME}` syntax for runtime interpolation.
|
||||
```json
|
||||
"env": {
|
||||
"API_KEY": "secret-key",
|
||||
"DATABASE_URL": "postgres://${DB_USER}@${DB_HOST}/mydb"
|
||||
}
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="cwd" type="string">
|
||||
Working directory for the server process. Relative paths are resolved from the config file location.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="args" type="list[str]">
|
||||
Command-line arguments to pass to the server, passed after `--` to the server's argument parser.
|
||||
```json
|
||||
"args": ["--config", "server-config.json"]
|
||||
```
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
## Usage with CLI Commands
|
||||
|
||||
FastMCP automatically detects and uses `fastmcp.json` files, making server execution simple and consistent:
|
||||
|
||||
```bash
|
||||
# Auto-detect fastmcp.json in current directory
|
||||
cd my-project
|
||||
fastmcp run # No arguments needed!
|
||||
|
||||
# Or specify a configuration file explicitly
|
||||
fastmcp run prod.fastmcp.json
|
||||
```
|
||||
|
||||
The configuration file works with all FastMCP commands:
|
||||
- **`run`** - Start the server in production mode
|
||||
- **`dev`** - Launch with the Inspector UI for development
|
||||
- **`inspect`** - View server capabilities and configuration
|
||||
- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
|
||||
|
||||
When no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.
|
||||
|
||||
### Custom Naming Patterns
|
||||
|
||||
You can use different configuration files for different environments:
|
||||
|
||||
- `fastmcp.json` - Default configuration
|
||||
- `dev.fastmcp.json` - Development settings
|
||||
- `prod.fastmcp.json` - Production settings
|
||||
- `test_fastmcp.json` - Test configuration
|
||||
|
||||
Any file with "fastmcp.json" in the name is recognized as a configuration file.
|
||||
|
||||
## Examples
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Basic Configuration">
|
||||
|
||||
A minimal configuration for a simple server:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
This configuration explicitly specifies the server object name (`app`), making it clear which object contains your FastMCP server. Uses all defaults: STDIO transport, no special dependencies, standard logging.
|
||||
</Tab>
|
||||
<Tab title="Development Configuration">
|
||||
|
||||
A configuration optimized for local development:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "src/server.py:app",
|
||||
"environment": {
|
||||
"python": "3.12",
|
||||
"dependencies": ["fastmcp[dev]"],
|
||||
"editable": "."
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"log_level": "DEBUG",
|
||||
"env": {
|
||||
"DEBUG": "true",
|
||||
"ENV": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Production Configuration">
|
||||
|
||||
A production-ready configuration with full dependency management:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "app/main.py",
|
||||
"object": "mcp_server"
|
||||
},
|
||||
"environment": {
|
||||
"python": "3.11",
|
||||
"requirements": "requirements/production.txt",
|
||||
"project": "."
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "0.0.0.0",
|
||||
"port": 3000,
|
||||
"path": "/api/mcp/",
|
||||
"log_level": "INFO",
|
||||
"env": {
|
||||
"ENV": "production",
|
||||
"API_BASE_URL": "https://api.example.com",
|
||||
"DATABASE_URL": "postgresql://user:pass@db.example.com/prod"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"args": ["--workers", "4"]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Data Science Server">
|
||||
|
||||
Configuration for a data analysis server with scientific packages:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "analysis_server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"python": "3.11",
|
||||
"dependencies": [
|
||||
"pandas>=2.0",
|
||||
"numpy",
|
||||
"scikit-learn",
|
||||
"matplotlib",
|
||||
"jupyterlab"
|
||||
]
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "stdio",
|
||||
"env": {
|
||||
"MATPLOTLIB_BACKEND": "Agg",
|
||||
"DATA_PATH": "./datasets"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Multi-Environment Setup">
|
||||
|
||||
You can maintain multiple configuration files for different environments:
|
||||
|
||||
**dev.fastmcp.json**:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"log_level": "DEBUG"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**prod.fastmcp.json**:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"requirements": "requirements/production.txt"
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "0.0.0.0",
|
||||
"log_level": "WARNING"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run different configurations:
|
||||
```bash
|
||||
fastmcp run dev.fastmcp.json # Development
|
||||
fastmcp run prod.fastmcp.json # Production
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
## CLI Override Behavior
|
||||
|
||||
Command-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:
|
||||
|
||||
```bash
|
||||
# Config specifies port 3000, CLI overrides to 8080
|
||||
fastmcp run fastmcp.json --port 8080
|
||||
|
||||
# Config specifies stdio, CLI overrides to HTTP
|
||||
fastmcp run fastmcp.json --transport http
|
||||
|
||||
# Add extra dependencies not in config
|
||||
fastmcp run fastmcp.json --with requests --with httpx
|
||||
```
|
||||
|
||||
This precedence order enables:
|
||||
- Quick testing of different settings
|
||||
- Environment-specific overrides in deployment scripts
|
||||
- Debugging with increased log levels
|
||||
- Temporary configuration changes
|
||||
|
||||
## Best Practices
|
||||
|
||||
When using `fastmcp.json` for your projects, consider these recommendations:
|
||||
|
||||
**Version Control**: Always commit your `fastmcp.json` to version control. It's essential project documentation that ensures others can run your server correctly.
|
||||
|
||||
**Environment Variables**: Use the `env` field for configuration values instead of hardcoding them in your Python code. For sensitive values, consider using environment variable references or separate secret management.
|
||||
|
||||
**Dependency Management**: Specify exact versions for production dependencies to ensure reproducible builds:
|
||||
```json
|
||||
{
|
||||
"dependencies": [
|
||||
"pandas==2.1.0",
|
||||
"requests==2.31.0"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Path Resolution**: Remember that paths in the configuration are relative to the config file location. Use relative paths for portability:
|
||||
```json
|
||||
{
|
||||
"entrypoint": "./src/server.py",
|
||||
"environment": {
|
||||
"requirements": "./requirements.txt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Development Workflow**: Use separate configuration files for different environments rather than constantly modifying a single file. The CLI's override behavior makes it easy to switch between configurations.
|
||||
|
||||
### Environment Variable Interpolation
|
||||
|
||||
The `env` field in deployment configuration supports runtime interpolation of environment variables using `${VAR_NAME}` syntax. This enables dynamic configuration based on your deployment environment:
|
||||
|
||||
```json
|
||||
{
|
||||
"deployment": {
|
||||
"env": {
|
||||
"API_URL": "https://api.${ENVIRONMENT}.example.com",
|
||||
"DATABASE_URL": "postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/myapp",
|
||||
"CACHE_KEY": "myapp_${ENVIRONMENT}_${VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the server starts, FastMCP replaces `${ENVIRONMENT}`, `${DB_USER}`, etc. with values from your system's environment variables. If a variable doesn't exist, the placeholder is preserved as-is.
|
||||
|
||||
**Example**: If your system has `ENVIRONMENT=production` and `DB_HOST=db.example.com`:
|
||||
```json
|
||||
// Configuration
|
||||
{
|
||||
"deployment": {
|
||||
"env": {
|
||||
"API_URL": "https://api.${ENVIRONMENT}.example.com",
|
||||
"DB_HOST": "${DB_HOST}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result at runtime
|
||||
{
|
||||
"API_URL": "https://api.production.example.com",
|
||||
"DB_HOST": "db.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
This feature is particularly useful for:
|
||||
- Deploying the same configuration across development, staging, and production
|
||||
- Keeping sensitive values out of configuration files
|
||||
- Building dynamic URLs and connection strings
|
||||
- Creating environment-specific prefixes or suffixes
|
||||
|
||||
## Migrating from CLI Arguments
|
||||
|
||||
If you're currently using command-line arguments or shell scripts, migrating to `fastmcp.json` simplifies your workflow. Here's how common CLI patterns map to configuration:
|
||||
|
||||
**CLI Command**:
|
||||
```bash
|
||||
uv run --with pandas --with requests \
|
||||
fastmcp run server.py \
|
||||
--transport http \
|
||||
--port 8000 \
|
||||
--log-level INFO
|
||||
```
|
||||
|
||||
**Equivalent fastmcp.json**:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "requests"]
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"port": 8000,
|
||||
"log_level": "INFO"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now simply run:
|
||||
```bash
|
||||
fastmcp run # Automatically finds and uses fastmcp.json
|
||||
```
|
||||
|
||||
The configuration file approach provides better documentation, easier sharing, and consistent execution across different environments while maintaining the flexibility to override settings when needed.
|
||||
|
|
@ -110,6 +110,7 @@
|
|||
"icon": "rocket",
|
||||
"pages": [
|
||||
"deployment/running-server",
|
||||
"deployment/server-configuration",
|
||||
"deployment/testing",
|
||||
"deployment/self-hosted",
|
||||
"deployment/fastmcp-cloud"
|
||||
|
|
|
|||
|
|
@ -81,17 +81,22 @@ fastmcp install claude-code server.py --with-requirements requirements.txt
|
|||
fastmcp install claude-code server.py --with-editable ./my-local-package
|
||||
```
|
||||
|
||||
Alternatively, you can specify dependencies directly in your server code:
|
||||
Alternatively, you can use a `fastmcp.json` configuration file (recommended):
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(
|
||||
name="Dice Roller",
|
||||
dependencies=["pandas", "requests"]
|
||||
)
|
||||
```json fastmcp.json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "requests"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Python Version and Project Configuration
|
||||
|
||||
Control the Python environment for your server with these options:
|
||||
|
|
|
|||
|
|
@ -98,17 +98,22 @@ fastmcp install claude-desktop server.py --with-requirements requirements.txt
|
|||
fastmcp install claude-desktop server.py --with-editable ./my-local-package
|
||||
```
|
||||
|
||||
Alternatively, you can specify dependencies directly in your server code:
|
||||
Alternatively, you can use a `fastmcp.json` configuration file (recommended):
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(
|
||||
name="Dice Roller",
|
||||
dependencies=["pandas", "requests"]
|
||||
)
|
||||
```json fastmcp.json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "requests"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Python Version and Project Directory
|
||||
|
||||
FastMCP allows you to control the Python environment for your server:
|
||||
|
|
|
|||
|
|
@ -99,17 +99,22 @@ fastmcp install cursor server.py --with-requirements requirements.txt
|
|||
fastmcp install cursor server.py --with-editable ./my-local-package
|
||||
```
|
||||
|
||||
Alternatively, you can specify dependencies directly in your server code:
|
||||
Alternatively, you can use a `fastmcp.json` configuration file (recommended):
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(
|
||||
name="Dice Roller",
|
||||
dependencies=["pandas", "requests"]
|
||||
)
|
||||
```json fastmcp.json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "requests"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Python Version and Project Configuration
|
||||
|
||||
Control your server's Python environment with these options:
|
||||
|
|
|
|||
|
|
@ -174,17 +174,27 @@ fastmcp install mcp-json server.py --with-editable ./my-package
|
|||
fastmcp install mcp-json server.py --with-requirements requirements.txt
|
||||
```
|
||||
|
||||
You can also specify dependencies directly in your server code:
|
||||
You can also use a `fastmcp.json` configuration file (recommended):
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(
|
||||
name="Data Analysis Server",
|
||||
dependencies=["pandas", "matplotlib", "seaborn"]
|
||||
)
|
||||
```json fastmcp.json
|
||||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"dependencies": ["pandas", "matplotlib", "seaborn"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then simply install with:
|
||||
```bash
|
||||
fastmcp install mcp-json fastmcp.json
|
||||
```
|
||||
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ fastmcp --help
|
|||
|
||||
| Command | Purpose | Dependency Management |
|
||||
| ------- | ------- | --------------------- |
|
||||
| `run` | Run a FastMCP server directly | **Supports:** Local files, factory functions, URLs, MCP configs. **Deps:** Uses your local environment directly. With `--python`, `--with`, `--project`, or `--with-requirements`: Runs via `uv run` subprocess |
|
||||
| `dev` | Run a server with the MCP Inspector for testing | **Supports:** Local files only. **Deps:** Always runs via `uv run` subprocess (never uses your local environment); dependencies must be specified or available in a uv-managed project |
|
||||
| `install` | Install a server in MCP client applications | **Supports:** Local files only. **Deps:** Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` |
|
||||
| `inspect` | Generate a JSON report about a FastMCP server | **Supports:** Local files only. **Deps:** Uses your current environment; you are responsible for ensuring all dependencies are available |
|
||||
| `run` | Run a FastMCP server directly | **Supports:** Local files, factory functions, URLs, fastmcp.json configs, MCP configs. **Deps:** Uses your local environment directly. With `--python`, `--with`, `--project`, or `--with-requirements`: Runs via `uv run` subprocess. With fastmcp.json: Automatically manages dependencies based on configuration |
|
||||
| `dev` | Run a server with the MCP Inspector for testing | **Supports:** Local files and fastmcp.json configs. **Deps:** Always runs via `uv run` subprocess (never uses your local environment); dependencies must be specified or available in a uv-managed project. With fastmcp.json: Uses configured dependencies |
|
||||
| `install` | Install a server in MCP client applications | **Supports:** Local files and fastmcp.json configs. **Deps:** Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable`. With fastmcp.json: Uses configured dependencies |
|
||||
| `inspect` | Generate a JSON report about a FastMCP server | **Supports:** Local files and fastmcp.json configs. **Deps:** Uses your current environment; you are responsible for ensuring all dependencies are available |
|
||||
| `version` | Display version information | N/A |
|
||||
|
||||
## `fastmcp run`
|
||||
|
|
@ -61,7 +61,8 @@ The `fastmcp run` command supports the following entrypoints:
|
|||
2. **[Explicit server object](#explicit-server-object)**: `server.py:custom_name` - imports and uses the specified server object
|
||||
3. **[Factory function](#factory-function)**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
|
||||
4. **[Remote server proxy](#remote-server-proxy)**: `https://example.com/mcp-server` - connects to a remote server and creates a **local proxy server**
|
||||
5. **MCP configuration file**: `mcp.json` - runs servers defined in a standard MCP configuration file
|
||||
5. **[FastMCP configuration file](#fastmcp-configuration)**: `fastmcp.json` - runs servers using FastMCP's declarative configuration format (auto-detects files in current directory)
|
||||
6. **MCP configuration file**: `mcp.json` - runs servers defined in a standard MCP configuration file
|
||||
|
||||
<Warning>
|
||||
Note: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == "__main__"` block. This means:
|
||||
|
|
@ -158,6 +159,28 @@ To start a local proxy, you can use the following syntax:
|
|||
fastmcp run https://example.com/mcp
|
||||
```
|
||||
|
||||
#### FastMCP Configuration
|
||||
<VersionBadge version="2.11.4" />
|
||||
|
||||
FastMCP supports declarative configuration through `fastmcp.json` files. When you run `fastmcp run` without arguments, it automatically looks for a `fastmcp.json` file in the current directory:
|
||||
|
||||
```bash
|
||||
# Auto-detect fastmcp.json in current directory
|
||||
fastmcp run
|
||||
|
||||
# Or explicitly specify a configuration file
|
||||
fastmcp run my-config.fastmcp.json
|
||||
```
|
||||
|
||||
The configuration file handles dependencies, environment variables, and transport settings. Command-line arguments override configuration file values:
|
||||
|
||||
```bash
|
||||
# Override port from config file
|
||||
fastmcp run fastmcp.json --port 8080
|
||||
```
|
||||
|
||||
See [Server Configuration](/deployment/server-configuration) for detailed documentation on fastmcp.json.
|
||||
|
||||
#### MCP Configuration
|
||||
|
||||
FastMCP can also run servers defined in a standard MCP configuration file. This is useful when you want to run multiple servers from a single file, or when you want to use a client that doesn't support direct connections to remote servers.
|
||||
|
|
@ -179,7 +202,12 @@ fastmcp dev server.py
|
|||
```
|
||||
|
||||
<Tip>
|
||||
This command always runs your server via `uv run` subprocess (never your local environment) to work with the MCP Inspector. All dependencies must be explicitly specified using the `--with` and/or `--with-editable` options, or be available in a uv-managed project.
|
||||
This command always runs your server via `uv run` subprocess (never your local environment) to work with the MCP Inspector. Dependencies can be:
|
||||
- Specified using `--with` and/or `--with-editable` options
|
||||
- Defined in a `fastmcp.json` configuration file
|
||||
- Available in a uv-managed project
|
||||
|
||||
When using `fastmcp.json`, the dev command automatically uses the configured dependencies.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
|
|
@ -214,14 +242,15 @@ This command does not support HTTP testing. To test a server over Streamable HTT
|
|||
|
||||
### Entrypoints
|
||||
|
||||
The `dev` command supports local FastMCP server files only:
|
||||
The `dev` command supports local FastMCP server files and configuration:
|
||||
|
||||
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
|
||||
2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
|
||||
3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
|
||||
4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration (auto-detects in current directory)
|
||||
|
||||
<Warning>
|
||||
The `dev` command **only supports local files** - no URLs, remote servers, or MCP configuration files.
|
||||
The `dev` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.
|
||||
</Warning>
|
||||
|
||||
**Examples**
|
||||
|
|
@ -230,6 +259,12 @@ The `dev` command **only supports local files** - no URLs, remote servers, or MC
|
|||
# Run dev server with editable mode and additional packages
|
||||
fastmcp dev server.py -e . --with pandas --with matplotlib
|
||||
|
||||
# Run dev server with fastmcp.json configuration (auto-detects)
|
||||
fastmcp dev
|
||||
|
||||
# Run dev server with explicit fastmcp.json file
|
||||
fastmcp dev dev.fastmcp.json
|
||||
|
||||
# Run dev server with specific Python version
|
||||
fastmcp dev server.py --python 3.11
|
||||
|
||||
|
|
@ -286,18 +321,19 @@ Note that for security reasons, MCP clients usually run every server in a comple
|
|||
|
||||
### Entrypoints
|
||||
|
||||
The `install` command supports local FastMCP server files only:
|
||||
The `install` command supports local FastMCP server files and configuration:
|
||||
|
||||
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
|
||||
2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
|
||||
3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
|
||||
4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration with dependencies and settings
|
||||
|
||||
<Note>
|
||||
Factory functions are particularly useful for install commands since they allow setup code to run that would otherwise be ignored when the MCP client runs your server.
|
||||
Factory functions are particularly useful for install commands since they allow setup code to run that would otherwise be ignored when the MCP client runs your server. When using fastmcp.json, dependencies are automatically handled.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
The `install` command **only supports local files** - no URLs, remote servers, or MCP configuration files. For remote servers, use your MCP client's native configuration.
|
||||
The `install` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files. For remote servers, use your MCP client's native configuration.
|
||||
</Warning>
|
||||
|
||||
**Examples**
|
||||
|
|
@ -306,6 +342,12 @@ The `install` command **only supports local files** - no URLs, remote servers, o
|
|||
# Auto-detects server object (looks for 'mcp', 'server', or 'app')
|
||||
fastmcp install claude-desktop server.py
|
||||
|
||||
# Install with fastmcp.json configuration (auto-detects)
|
||||
fastmcp install claude-desktop
|
||||
|
||||
# Install with explicit fastmcp.json file
|
||||
fastmcp install claude-desktop my-config.fastmcp.json
|
||||
|
||||
# Uses specific server object
|
||||
fastmcp install claude-desktop server.py:my_server
|
||||
|
||||
|
|
@ -395,14 +437,15 @@ fastmcp inspect server.py
|
|||
|
||||
### Entrypoints
|
||||
|
||||
The `inspect` command supports local FastMCP server files only:
|
||||
The `inspect` command supports local FastMCP server files and configuration:
|
||||
|
||||
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
|
||||
2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
|
||||
3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
|
||||
4. **FastMCP configuration**: `fastmcp.json` - inspects servers defined with FastMCP's declarative configuration
|
||||
|
||||
<Warning>
|
||||
The `inspect` command **only supports local files** - no URLs, remote servers, or MCP configuration files.
|
||||
The `inspect` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.
|
||||
</Warning>
|
||||
|
||||
**Examples**
|
||||
|
|
|
|||
1
docs/schemas/fastmcp_config/latest.json
Symbolic link
1
docs/schemas/fastmcp_config/latest.json
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
v1.json
|
||||
1
docs/schemas/fastmcp_config/v1.json
Symbolic link
1
docs/schemas/fastmcp_config/v1.json
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../src/fastmcp/utilities/fastmcp_config/v1/schema.json
|
||||
|
|
@ -52,9 +52,6 @@ The `FastMCP` constructor accepts several arguments:
|
|||
A list of tools (or functions to convert to tools) to add to the server. In some cases, providing tools programmatically may be more convenient than using the `@mcp.tool` decorator
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="dependencies" type="list[str] | None">
|
||||
Optional server dependencies list with package specifications
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="include_tags" type="set[str] | None">
|
||||
Only expose components with at least one matching tag
|
||||
|
|
@ -202,13 +199,13 @@ if __name__ == "__main__":
|
|||
# This runs the server, defaulting to STDIO transport
|
||||
mcp.run()
|
||||
|
||||
# To use a different transport, e.g., Streamable HTTP:
|
||||
# To use a different transport, e.g., HTTP:
|
||||
# mcp.run(transport="http", host="127.0.0.1", port=9000)
|
||||
```
|
||||
|
||||
FastMCP supports several transport options:
|
||||
- STDIO (default, for local tools)
|
||||
- Streamable HTTP (recommended for web services)
|
||||
- HTTP (recommended for web services, uses Streamable HTTP protocol)
|
||||
- SSE (legacy web transport, deprecated)
|
||||
|
||||
The server can also be run using the FastMCP CLI.
|
||||
|
|
@ -319,7 +316,6 @@ from fastmcp import FastMCP
|
|||
# Configure server-specific settings
|
||||
mcp = FastMCP(
|
||||
name="ConfiguredServer",
|
||||
dependencies=["requests", "pandas>=2.0.0"], # Optional server dependencies
|
||||
include_tags={"public", "api"}, # Only expose these tagged components
|
||||
exclude_tags={"internal", "deprecated"}, # Hide these tagged components
|
||||
on_duplicate_tools="error", # Handle duplicate registrations
|
||||
|
|
|
|||
9
examples/atproto_mcp/fastmcp.json
Normal file
9
examples/atproto_mcp/fastmcp.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "src/atproto_mcp/server.py",
|
||||
"environment": {
|
||||
"dependencies": [
|
||||
"atproto_mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -22,12 +22,7 @@ from atproto_mcp.types import (
|
|||
)
|
||||
from fastmcp import FastMCP
|
||||
|
||||
atproto_mcp = FastMCP(
|
||||
"ATProto MCP Server",
|
||||
dependencies=[
|
||||
"atproto_mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp",
|
||||
],
|
||||
)
|
||||
atproto_mcp = FastMCP("ATProto MCP Server")
|
||||
|
||||
|
||||
# Resources - read-only operations
|
||||
|
|
|
|||
20
examples/fastmcp_config/env_interpolation_example.json
Normal file
20
examples/fastmcp_config/env_interpolation_example.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "src/server.py:app",
|
||||
"environment": {
|
||||
"python": "3.12",
|
||||
"dependencies": ["fastmcp", "httpx", "pandas"]
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "0.0.0.0",
|
||||
"port": 8000,
|
||||
"env": {
|
||||
"API_BASE_URL": "https://api.${ENVIRONMENT}.example.com",
|
||||
"DATABASE_URL": "postgres://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}",
|
||||
"CACHE_PREFIX": "myapp_${ENVIRONMENT}_v1",
|
||||
"LOG_LEVEL": "${LOG_LEVEL}",
|
||||
"FEATURE_FLAGS": "${FEATURE_FLAGS}"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
examples/fastmcp_config/fastmcp.json
Normal file
11
examples/fastmcp_config/fastmcp.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp/v1.json",
|
||||
"entrypoint": "server.py",
|
||||
"environment": {
|
||||
"python": "3.12",
|
||||
"dependencies": ["requests"]
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "stdio"
|
||||
}
|
||||
}
|
||||
30
examples/fastmcp_config/full_example.fastmcp.json
Normal file
30
examples/fastmcp_config/full_example.fastmcp.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {
|
||||
"file": "server.py",
|
||||
"object": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
"python": "3.12",
|
||||
"dependencies": [
|
||||
"requests>=2.31.0",
|
||||
"httpx"
|
||||
],
|
||||
"requirements": null,
|
||||
"project": null,
|
||||
"editable": null
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"path": "/mcp/",
|
||||
"log_level": "INFO",
|
||||
"env": {
|
||||
"DEBUG": "false",
|
||||
"API_TIMEOUT": "30"
|
||||
},
|
||||
"cwd": null,
|
||||
"args": null
|
||||
}
|
||||
}
|
||||
39
examples/fastmcp_config/server.py
Normal file
39
examples/fastmcp_config/server.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
"""Example FastMCP server for demonstrating fastmcp.json configuration."""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Create the FastMCP server instance
|
||||
mcp = FastMCP("Config Example Server")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def echo(text: str) -> str:
|
||||
"""Echo the provided text back to the user."""
|
||||
return f"You said: {text}"
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.resource("config://example")
|
||||
def get_example_config() -> str:
|
||||
"""Return an example configuration."""
|
||||
return """
|
||||
This server is configured using fastmcp.json.
|
||||
|
||||
The configuration file specifies:
|
||||
- Python version
|
||||
- Dependencies
|
||||
- Transport settings
|
||||
- Other runtime options
|
||||
"""
|
||||
|
||||
|
||||
# This allows the server to run with: fastmcp run server.py
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(mcp.run_async())
|
||||
7
examples/fastmcp_config/simple.fastmcp.json
Normal file
7
examples/fastmcp_config/simple.fastmcp.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp/v1.json",
|
||||
"entrypoint": "server.py",
|
||||
"deployment": {
|
||||
"transport": "stdio"
|
||||
}
|
||||
}
|
||||
55
examples/fastmcp_config_demo/README.md
Normal file
55
examples/fastmcp_config_demo/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# FastMCP Configuration Demo
|
||||
|
||||
This example demonstrates the recommended way to configure FastMCP servers using `fastmcp.json`.
|
||||
|
||||
## Migration from Dependencies Parameter
|
||||
|
||||
Previously (deprecated as of FastMCP 2.11.4), you would specify dependencies in the Python code:
|
||||
|
||||
```python
|
||||
mcp = FastMCP("Demo Server", dependencies=["pyautogui", "Pillow"])
|
||||
```
|
||||
|
||||
Now, dependencies are declared in `fastmcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"environment": {
|
||||
"dependencies": ["pyautogui", "Pillow"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
With the configuration file in place, you can run the server in several ways:
|
||||
|
||||
```bash
|
||||
# Auto-detect fastmcp.json in current directory
|
||||
cd examples/fastmcp_config_demo
|
||||
fastmcp run
|
||||
|
||||
# Or specify the config file explicitly
|
||||
fastmcp run examples/fastmcp_config_demo/fastmcp.json
|
||||
|
||||
# Or use development mode with the Inspector UI
|
||||
fastmcp dev examples/fastmcp_config_demo/fastmcp.json
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Single source of truth**: All configuration in one place
|
||||
- **Environment isolation**: Dependencies are installed in an isolated UV environment
|
||||
- **No import-time issues**: Dependencies are installed before the server is imported
|
||||
- **IDE support**: JSON schema provides autocomplete and validation
|
||||
- **Shareable**: Easy to share complete server configuration with others
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
The `fastmcp.json` file supports three main sections:
|
||||
|
||||
1. **entrypoint** (required): The Python file containing your server
|
||||
2. **environment** (optional): Python version and dependencies
|
||||
3. **deployment** (optional): Runtime settings like transport and logging
|
||||
|
||||
See the [full documentation](https://gofastmcp.com/docs/deployment/server-configuration) for more details.
|
||||
15
examples/fastmcp_config_demo/fastmcp.json
Normal file
15
examples/fastmcp_config_demo/fastmcp.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "server.py",
|
||||
"environment": {
|
||||
"python": "3.11",
|
||||
"dependencies": [
|
||||
"pyautogui",
|
||||
"Pillow"
|
||||
]
|
||||
},
|
||||
"deployment": {
|
||||
"transport": "stdio",
|
||||
"log_level": "INFO"
|
||||
}
|
||||
}
|
||||
70
examples/fastmcp_config_demo/server.py
Normal file
70
examples/fastmcp_config_demo/server.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""
|
||||
Example server demonstrating fastmcp.json configuration.
|
||||
|
||||
This server previously would have used the deprecated dependencies parameter:
|
||||
mcp = FastMCP("Demo Server", dependencies=["pyautogui", "Pillow"])
|
||||
|
||||
Now dependencies are declared in fastmcp.json alongside this file.
|
||||
"""
|
||||
|
||||
import io
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.types import Image
|
||||
|
||||
# Create server - dependencies are now in fastmcp.json
|
||||
mcp = FastMCP("Screenshot Demo")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def take_screenshot() -> Image:
|
||||
"""
|
||||
Take a screenshot of the user's screen and return it as an image.
|
||||
|
||||
Use this tool anytime the user wants you to look at something on their screen.
|
||||
"""
|
||||
import pyautogui
|
||||
|
||||
buffer = io.BytesIO()
|
||||
|
||||
# Capture and compress the screenshot to stay under size limits
|
||||
screenshot = pyautogui.screenshot()
|
||||
screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True)
|
||||
|
||||
return Image(data=buffer.getvalue(), format="jpeg")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def analyze_colors() -> dict:
|
||||
"""
|
||||
Analyze the dominant colors in the current screen.
|
||||
|
||||
Returns a dictionary with color statistics from the screen.
|
||||
"""
|
||||
import pyautogui
|
||||
from PIL import Image as PILImage
|
||||
|
||||
screenshot = pyautogui.screenshot()
|
||||
# Convert to smaller size for faster analysis
|
||||
small = screenshot.resize((100, 100), PILImage.Resampling.LANCZOS)
|
||||
|
||||
# Get colors
|
||||
colors = small.getcolors(maxcolors=10000)
|
||||
if not colors:
|
||||
return {"error": "Too many colors to analyze"}
|
||||
|
||||
# Sort by frequency
|
||||
sorted_colors = sorted(colors, key=lambda x: x[0], reverse=True)[:10]
|
||||
|
||||
return {
|
||||
"top_colors": [
|
||||
{"count": count, "rgb": color} for count, color in sorted_colors
|
||||
],
|
||||
"total_pixels": sum(c[0] for c in colors),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(mcp.run_async())
|
||||
12
examples/memory.fastmcp.json
Normal file
12
examples/memory.fastmcp.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "memory.py",
|
||||
"environment": {
|
||||
"dependencies": [
|
||||
"pydantic-ai-slim[openai]",
|
||||
"asyncpg",
|
||||
"numpy",
|
||||
"pgvector"
|
||||
]
|
||||
}
|
||||
}
|
||||
4
examples/mount_example.fastmcp.json
Normal file
4
examples/mount_example.fastmcp.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "mount_example.py"
|
||||
}
|
||||
|
|
@ -54,9 +54,7 @@ async def news_data():
|
|||
|
||||
|
||||
# Main application
|
||||
app = FastMCP(
|
||||
"Main App", dependencies=["fastmcp@git+https://github.com/jlowin/fastmcp.git"]
|
||||
)
|
||||
app = FastMCP("Main App")
|
||||
|
||||
|
||||
@app.tool
|
||||
|
|
|
|||
7
examples/screenshot.fastmcp.json
Normal file
7
examples/screenshot.fastmcp.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "screenshot.py",
|
||||
"environment": {
|
||||
"dependencies": ["pyautogui", "Pillow"]
|
||||
}
|
||||
}
|
||||
9
examples/smart_home/hub.fastmcp.json
Normal file
9
examples/smart_home/hub.fastmcp.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "src/smart_home/hub.py",
|
||||
"environment": {
|
||||
"dependencies": [
|
||||
"smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
|
||||
]
|
||||
}
|
||||
}
|
||||
9
examples/smart_home/lights.fastmcp.json
Normal file
9
examples/smart_home/lights.fastmcp.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "src/smart_home/lights/server.py",
|
||||
"environment": {
|
||||
"dependencies": [
|
||||
"smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ def version(
|
|||
|
||||
@app.command
|
||||
async def dev(
|
||||
server_spec: str,
|
||||
server_spec: str | None = None,
|
||||
*,
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
|
|
@ -202,8 +202,57 @@ async def dev(
|
|||
"""Run an MCP server with the MCP Inspector for development.
|
||||
|
||||
Args:
|
||||
server_spec: Python file to run, optionally with :object suffix
|
||||
server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
|
||||
"""
|
||||
# Auto-detect fastmcp.json if no server_spec provided
|
||||
if server_spec is None:
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
|
||||
config_path = Path("fastmcp.json")
|
||||
if not config_path.exists():
|
||||
# Check if fastmcp.json exists in current directory
|
||||
found_config = FastMCPConfig.find_config()
|
||||
if found_config:
|
||||
config_path = found_config
|
||||
else:
|
||||
logger.error(
|
||||
"No server specification provided and no fastmcp.json found in current directory.\n"
|
||||
"Please specify a server file or create a fastmcp.json configuration."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Load the config to get settings
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
entrypoint = config.get_entrypoint(config_path)
|
||||
|
||||
# Convert entrypoint to string format for dev command
|
||||
if entrypoint.object:
|
||||
server_spec = f"{entrypoint.file}:{entrypoint.object}"
|
||||
else:
|
||||
server_spec = entrypoint.file
|
||||
|
||||
# Merge environment settings with CLI args (CLI takes precedence)
|
||||
if config.environment:
|
||||
merged_env = config.environment.merge_with_cli_args(
|
||||
python=python,
|
||||
with_packages=with_packages,
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
with_editable=with_editable,
|
||||
)
|
||||
python = merged_env["python"]
|
||||
with_packages = merged_env["with_packages"]
|
||||
with_requirements = merged_env["with_requirements"]
|
||||
project = merged_env["project"]
|
||||
with_editable = merged_env["with_editable"]
|
||||
|
||||
# Get server port from deployment config if not specified
|
||||
if config.deployment and config.deployment.port:
|
||||
server_port = server_port or config.deployment.port
|
||||
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
file, server_object = run_module.parse_file_path(server_spec)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -220,8 +269,18 @@ async def dev(
|
|||
|
||||
try:
|
||||
# Import server to get dependencies
|
||||
# TODO: Remove dependencies handling (deprecated in v2.11.4)
|
||||
server: FastMCP = await run_module.import_server(file, server_object)
|
||||
if server.dependencies is not None:
|
||||
if server.dependencies:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
f"Server '{server.name}' uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). "
|
||||
"Please migrate to fastmcp.json configuration file. "
|
||||
"See https://gofastmcp.com/docs/deployment/server-configuration for details.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
with_packages = list(set(with_packages + server.dependencies))
|
||||
|
||||
env_vars = {}
|
||||
|
|
@ -284,7 +343,7 @@ async def dev(
|
|||
|
||||
@app.command
|
||||
async def run(
|
||||
server_spec: str,
|
||||
server_spec: str | None = None,
|
||||
*server_args: str,
|
||||
transport: Annotated[
|
||||
run_module.TransportType | None,
|
||||
|
|
@ -361,18 +420,81 @@ async def run(
|
|||
) -> None:
|
||||
"""Run an MCP server or connect to a remote one.
|
||||
|
||||
The server can be specified in four ways:
|
||||
The server can be specified in several ways:
|
||||
1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
|
||||
2. Import approach: "server.py:app" - imports and runs the specified server object
|
||||
3. URL approach: "http://server-url" - connects to a remote server and creates a proxy
|
||||
4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
|
||||
5. FastMCP config: "fastmcp.json" - runs server using FastMCP configuration
|
||||
6. No argument: looks for fastmcp.json in current directory
|
||||
|
||||
Server arguments can be passed after -- :
|
||||
fastmcp run server.py -- --config config.json --debug
|
||||
|
||||
Args:
|
||||
server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
|
||||
server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
|
||||
"""
|
||||
# Load configuration if needed
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
|
||||
config = None
|
||||
config_path = None
|
||||
|
||||
# Auto-detect fastmcp.json if no server_spec provided
|
||||
if server_spec is None:
|
||||
config_path = Path("fastmcp.json")
|
||||
if not config_path.exists():
|
||||
# Check if fastmcp.json exists in current directory
|
||||
found_config = FastMCPConfig.find_config()
|
||||
if found_config:
|
||||
config_path = found_config
|
||||
else:
|
||||
logger.error(
|
||||
"No server specification provided and no fastmcp.json found in current directory.\n"
|
||||
"Please specify a server file or create a fastmcp.json configuration."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
server_spec = str(config_path)
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
|
||||
# Load config if server_spec is a fastmcp.json file
|
||||
if server_spec.endswith("fastmcp.json"):
|
||||
config_path = Path(server_spec)
|
||||
if config_path.exists():
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
|
||||
# Merge deployment config with CLI values (CLI takes precedence)
|
||||
if config.deployment:
|
||||
merged_deploy = config.deployment.merge_with_cli_args(
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level=log_level,
|
||||
server_args=list(server_args) if server_args else None,
|
||||
)
|
||||
transport = merged_deploy["transport"]
|
||||
host = merged_deploy["host"]
|
||||
port = merged_deploy["port"]
|
||||
path = merged_deploy["path"]
|
||||
log_level = merged_deploy["log_level"]
|
||||
server_args = merged_deploy["server_args"] or ()
|
||||
|
||||
# Merge environment config with CLI values (CLI takes precedence)
|
||||
if config.environment:
|
||||
merged_env = config.environment.merge_with_cli_args(
|
||||
python=python,
|
||||
with_packages=with_packages,
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
)
|
||||
python = merged_env["python"]
|
||||
with_packages = merged_env["with_packages"]
|
||||
with_requirements = merged_env["with_requirements"]
|
||||
project = merged_env["project"]
|
||||
logger.debug(
|
||||
"Running server or client",
|
||||
extra={
|
||||
|
|
@ -386,8 +508,14 @@ async def run(
|
|||
},
|
||||
)
|
||||
|
||||
# If any uv-specific options are provided, use uv run
|
||||
if python or with_packages or with_requirements or project:
|
||||
# Check if we need to use uv run (either from CLI args or config)
|
||||
needs_uv = python or with_packages or with_requirements or project
|
||||
if not needs_uv and config and config.environment:
|
||||
# Check if config's environment needs uv
|
||||
needs_uv = config.environment.needs_uv()
|
||||
|
||||
if needs_uv:
|
||||
# Use uv run subprocess - always use run_with_uv which handles output correctly
|
||||
try:
|
||||
run_module.run_with_uv(
|
||||
server_spec=server_spec,
|
||||
|
|
@ -437,7 +565,7 @@ async def run(
|
|||
|
||||
@app.command
|
||||
async def inspect(
|
||||
server_spec: str,
|
||||
server_spec: str | None = None,
|
||||
*,
|
||||
output: Annotated[
|
||||
Path,
|
||||
|
|
@ -446,6 +574,35 @@ async def inspect(
|
|||
help="Output file path for the JSON report (default: server-info.json)",
|
||||
),
|
||||
] = Path("server-info.json"),
|
||||
python: Annotated[
|
||||
str | None,
|
||||
cyclopts.Parameter(
|
||||
"--python",
|
||||
help="Python version to use (e.g., 3.10, 3.11)",
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
cyclopts.Parameter(
|
||||
"--with",
|
||||
help="Additional packages to install (can be used multiple times)",
|
||||
negative=False,
|
||||
),
|
||||
] = [],
|
||||
project: Annotated[
|
||||
Path | None,
|
||||
cyclopts.Parameter(
|
||||
"--project",
|
||||
help="Run the command within the given project directory",
|
||||
),
|
||||
] = None,
|
||||
with_requirements: Annotated[
|
||||
Path | None,
|
||||
cyclopts.Parameter(
|
||||
"--with-requirements",
|
||||
help="Requirements file to install dependencies from",
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Inspect an MCP server and generate a JSON report.
|
||||
|
||||
|
|
@ -458,10 +615,104 @@ async def inspect(
|
|||
fastmcp inspect server.py -o report.json
|
||||
fastmcp inspect server.py:mcp -o analysis.json
|
||||
fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
|
||||
fastmcp inspect fastmcp.json
|
||||
fastmcp inspect # auto-detect fastmcp.json
|
||||
|
||||
Args:
|
||||
server_spec: Python file to inspect, optionally with :object suffix
|
||||
server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
|
||||
"""
|
||||
# Load configuration if needed
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
|
||||
config = None
|
||||
config_path = None
|
||||
|
||||
# Auto-detect fastmcp.json if no server_spec provided
|
||||
if server_spec is None:
|
||||
config_path = Path("fastmcp.json")
|
||||
if not config_path.exists():
|
||||
# Check if fastmcp.json exists in current directory
|
||||
found_config = FastMCPConfig.find_config()
|
||||
if found_config:
|
||||
config_path = found_config
|
||||
else:
|
||||
logger.error(
|
||||
"No server specification provided and no fastmcp.json found in current directory.\n"
|
||||
"Please specify a server file or create a fastmcp.json configuration."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
server_spec = str(config_path)
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
|
||||
# Load config if server_spec is a fastmcp.json file
|
||||
if server_spec.endswith("fastmcp.json"):
|
||||
config_path = Path(server_spec)
|
||||
if config_path.exists():
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
# Get the actual entrypoint with resolved paths
|
||||
entrypoint = config.get_entrypoint(config_path)
|
||||
|
||||
if entrypoint.object:
|
||||
server_spec = f"{entrypoint.file}:{entrypoint.object}"
|
||||
else:
|
||||
server_spec = entrypoint.file
|
||||
|
||||
# Merge environment settings from config with CLI (CLI takes precedence)
|
||||
if config.environment:
|
||||
merged_env = config.environment.merge_with_cli_args(
|
||||
python=python,
|
||||
with_packages=with_packages,
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
)
|
||||
python = merged_env["python"]
|
||||
with_packages = merged_env["with_packages"]
|
||||
with_requirements = merged_env["with_requirements"]
|
||||
project = merged_env["project"]
|
||||
|
||||
# Check if we need to use uv run
|
||||
needs_uv = python or with_packages or with_requirements or project
|
||||
if not needs_uv and config and config.environment:
|
||||
needs_uv = config.environment.needs_uv()
|
||||
|
||||
if needs_uv:
|
||||
# Build and run uv command
|
||||
if config and config.environment:
|
||||
# Use environment config's run_with_uv method
|
||||
inspect_command = [
|
||||
"fastmcp",
|
||||
"inspect",
|
||||
server_spec,
|
||||
"--output",
|
||||
str(output),
|
||||
]
|
||||
config.environment.run_with_uv(inspect_command)
|
||||
else:
|
||||
# Build an EnvironmentConfig from CLI args for consistency
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
EnvironmentConfig,
|
||||
)
|
||||
|
||||
env_config = EnvironmentConfig(
|
||||
python=python,
|
||||
dependencies=with_packages,
|
||||
requirements=str(with_requirements) if with_requirements else None,
|
||||
project=str(project) if project else None,
|
||||
)
|
||||
|
||||
inspect_command = [
|
||||
"fastmcp",
|
||||
"inspect",
|
||||
server_spec,
|
||||
"--output",
|
||||
str(output),
|
||||
]
|
||||
env_config.run_with_uv(inspect_command)
|
||||
|
||||
# Direct import path (no uv needed)
|
||||
# Parse the server specification
|
||||
file, server_object = run_module.parse_file_path(server_spec)
|
||||
|
||||
|
|
@ -514,6 +765,43 @@ async def inspect(
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command
|
||||
def generate_schema(
|
||||
*,
|
||||
output: Annotated[
|
||||
Path | None,
|
||||
cyclopts.Parameter(
|
||||
name=["--output", "-o"],
|
||||
help="Output file path for the JSON schema",
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Generate JSON schema for fastmcp.json configuration files.
|
||||
|
||||
This generates a JSON schema that can be used by IDEs and validators
|
||||
to provide auto-completion and validation for fastmcp.json files.
|
||||
|
||||
Examples:
|
||||
fastmcp generate-schema
|
||||
fastmcp generate-schema -o schema.json
|
||||
"""
|
||||
import json
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
generate_schema as gen_schema,
|
||||
)
|
||||
|
||||
schema = gen_schema()
|
||||
schema_json = json.dumps(schema, indent=2)
|
||||
|
||||
if output:
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(schema_json)
|
||||
logger.info(f"Schema written to {output}")
|
||||
else:
|
||||
console.print(schema_json)
|
||||
|
||||
|
||||
# Add install subcommands using proper Cyclopts pattern
|
||||
app.command(install_app)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,35 @@ async def process_common_args(
|
|||
env_vars: list[str],
|
||||
env_file: Path | None,
|
||||
) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
|
||||
"""Process common arguments shared by all install commands."""
|
||||
# Parse server spec
|
||||
file, server_object = parse_file_path(server_spec)
|
||||
"""Process common arguments shared by all install commands.
|
||||
|
||||
Handles both fastmcp.json config files and traditional file.py:object syntax.
|
||||
"""
|
||||
# Check if server_spec is a fastmcp.json file
|
||||
if server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name:
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
|
||||
config_path = Path(server_spec).resolve()
|
||||
if not config_path.exists():
|
||||
print(f"[red]Configuration file not found: {config_path}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
# Load config and get entrypoint
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
entrypoint = config.get_entrypoint(config_path)
|
||||
|
||||
# Convert to file and server_object
|
||||
file = Path(entrypoint.file)
|
||||
server_object = entrypoint.object
|
||||
|
||||
# Merge packages from config if not overridden
|
||||
if config.environment and config.environment.dependencies:
|
||||
# Merge with CLI packages (CLI takes precedence)
|
||||
config_packages = config.environment.dependencies or []
|
||||
with_packages = list(set(with_packages + config_packages))
|
||||
else:
|
||||
# Parse traditional server spec
|
||||
file, server_object = parse_file_path(server_spec)
|
||||
|
||||
logger.debug(
|
||||
"Installing server",
|
||||
|
|
@ -59,8 +85,18 @@ async def process_common_args(
|
|||
name = file.stem
|
||||
|
||||
# Get server dependencies if available
|
||||
# TODO: Remove dependencies handling (deprecated in v2.11.4)
|
||||
server_dependencies = getattr(server, "dependencies", []) if server else []
|
||||
if server_dependencies:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Server uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). "
|
||||
"Please migrate to fastmcp.json configuration file. "
|
||||
"See https://gofastmcp.com/docs/deployment/server-configuration for details.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
with_packages = list(set(with_packages + server_dependencies))
|
||||
|
||||
# Process environment variables if provided
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ from typing import Any, Literal
|
|||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
DeploymentConfig,
|
||||
EntrypointConfig,
|
||||
EnvironmentConfig,
|
||||
FastMCPConfig,
|
||||
)
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
logger = get_logger("cli.run")
|
||||
|
|
@ -89,12 +95,13 @@ async def import_server(file: Path, server_or_factory: str | None = None) -> Any
|
|||
for name in ["mcp", "server", "app"]:
|
||||
if hasattr(module, name):
|
||||
obj = getattr(module, name)
|
||||
return await _resolve_server_or_factory(obj, file, name)
|
||||
if isinstance(obj, FastMCP | FastMCP1x):
|
||||
return await _resolve_server_or_factory(obj, file, name)
|
||||
|
||||
logger.error(
|
||||
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",
|
||||
"2. Specify the object name in fastmcp.json or use `file.py:object` syntax as your path.",
|
||||
extra={"file": str(file)},
|
||||
)
|
||||
sys.exit(1)
|
||||
|
|
@ -187,7 +194,7 @@ def run_with_uv(
|
|||
"""Run a MCP server using uv run subprocess.
|
||||
|
||||
Args:
|
||||
server_spec: Python file, object specification (file:obj), or URL
|
||||
server_spec: Python file, object specification (file:obj), config file, or URL
|
||||
python_version: Python version to use (e.g. "3.10")
|
||||
with_packages: Additional packages to install
|
||||
with_requirements: Requirements file to use
|
||||
|
|
@ -199,6 +206,55 @@ def run_with_uv(
|
|||
log_level: Log level
|
||||
show_banner: Whether to show the server banner
|
||||
"""
|
||||
# Check if server_spec is a fastmcp.json file
|
||||
if server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name:
|
||||
config_path = Path(server_spec).resolve() # Get absolute path
|
||||
if config_path.exists():
|
||||
# Load config
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
|
||||
# Get entrypoint with resolved paths
|
||||
entrypoint = config.get_entrypoint(config_path)
|
||||
if entrypoint.object:
|
||||
server_spec = f"{entrypoint.file}:{entrypoint.object}"
|
||||
else:
|
||||
server_spec = entrypoint.file
|
||||
|
||||
# Merge environment config with CLI args
|
||||
# Check if environment has any non-None values
|
||||
if config.environment and any(
|
||||
getattr(config.environment, field, None) is not None
|
||||
for field in EnvironmentConfig.model_fields
|
||||
):
|
||||
merged_env = config.environment.merge_with_cli_args(
|
||||
python=python_version,
|
||||
with_packages=with_packages,
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
)
|
||||
python_version = merged_env["python"]
|
||||
with_packages = merged_env["with_packages"]
|
||||
with_requirements = merged_env["with_requirements"]
|
||||
project = merged_env["project"]
|
||||
|
||||
# Merge deployment config with CLI args
|
||||
# Check if deployment has any non-None values
|
||||
if config.deployment and any(
|
||||
getattr(config.deployment, field, None) is not None
|
||||
for field in DeploymentConfig.model_fields
|
||||
):
|
||||
merged_deploy = config.deployment.merge_with_cli_args(
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level=log_level,
|
||||
)
|
||||
transport = merged_deploy["transport"]
|
||||
host = merged_deploy["host"]
|
||||
port = merged_deploy["port"]
|
||||
path = merged_deploy["path"]
|
||||
log_level = merged_deploy["log_level"]
|
||||
cmd = ["uv", "run"]
|
||||
|
||||
# Add Python version if specified
|
||||
|
|
@ -280,6 +336,48 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
|
|||
return server
|
||||
|
||||
|
||||
def load_fastmcp_config(
|
||||
config_path: Path,
|
||||
) -> tuple[EntrypointConfig, DeploymentConfig | None, EnvironmentConfig | None]:
|
||||
"""Load a FastMCP configuration from a fastmcp.json file.
|
||||
|
||||
Args:
|
||||
config_path: Path to fastmcp.json file
|
||||
|
||||
Returns:
|
||||
Tuple of (entrypoint, deployment config, environment config)
|
||||
"""
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
|
||||
# Apply runtime settings from deployment config
|
||||
if config.deployment:
|
||||
config.deployment.apply_runtime_settings(config_path)
|
||||
|
||||
# Get entrypoint as structured object with resolved paths
|
||||
entrypoint = config.get_entrypoint(config_path)
|
||||
|
||||
# Return None for empty configs (backward compatibility)
|
||||
deployment = (
|
||||
config.deployment
|
||||
if any(
|
||||
getattr(config.deployment, field, None) is not None
|
||||
for field in DeploymentConfig.model_fields
|
||||
)
|
||||
else None
|
||||
)
|
||||
|
||||
environment = (
|
||||
config.environment
|
||||
if any(
|
||||
getattr(config.environment, field, None) is not None
|
||||
for field in EnvironmentConfig.model_fields
|
||||
)
|
||||
else None
|
||||
)
|
||||
|
||||
return entrypoint, deployment, environment
|
||||
|
||||
|
||||
async def import_server_with_args(
|
||||
file: Path,
|
||||
server_or_factory: str | None = None,
|
||||
|
|
@ -320,7 +418,7 @@ async def run_command(
|
|||
"""Run a MCP server or connect to a remote one.
|
||||
|
||||
Args:
|
||||
server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
|
||||
server_spec: Python file, object specification (file:obj), config file, or URL
|
||||
transport: Transport protocol to use
|
||||
host: Host to bind to when using http transport
|
||||
port: Port to bind to when using http transport
|
||||
|
|
@ -334,7 +432,38 @@ async def run_command(
|
|||
# Handle URL case
|
||||
server = create_client_server(server_spec)
|
||||
logger.debug(f"Created client proxy server for {server_spec}")
|
||||
elif (
|
||||
server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name
|
||||
):
|
||||
# Handle fastmcp.json configuration file (matches test_fastmcp.json, my.fastmcp.json, etc)
|
||||
config_path = Path(server_spec)
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_path)
|
||||
|
||||
# Merge deployment config with CLI arguments (CLI takes precedence)
|
||||
if deployment:
|
||||
merged = deployment.merge_with_cli_args(
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level=log_level,
|
||||
server_args=server_args,
|
||||
)
|
||||
transport = merged["transport"]
|
||||
host = merged["host"]
|
||||
port = merged["port"]
|
||||
path = merged["path"]
|
||||
log_level = merged["log_level"]
|
||||
server_args = merged["server_args"]
|
||||
|
||||
# Import the server from the structured entrypoint
|
||||
file_path = Path(entrypoint.file)
|
||||
server = await import_server_with_args(
|
||||
file_path, entrypoint.object, server_args
|
||||
)
|
||||
logger.debug(f'Found server "{server.name}" from config {config_path}')
|
||||
elif server_spec.endswith(".json"):
|
||||
# Handle other JSON files as MCPConfig
|
||||
server = create_mcp_config_server(Path(server_spec))
|
||||
else:
|
||||
# Handle file case
|
||||
|
|
@ -358,8 +487,8 @@ async def run_command(
|
|||
kwargs["port"] = port
|
||||
if path:
|
||||
kwargs["path"] = path
|
||||
if log_level:
|
||||
kwargs["log_level"] = log_level
|
||||
# Note: log_level is not currently supported by run_async
|
||||
# TODO: Add log_level support to server.run_async
|
||||
|
||||
if not show_banner:
|
||||
kwargs["show_banner"] = False
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ from mcp.client.session import (
|
|||
MessageHandlerFnT,
|
||||
SamplingFnT,
|
||||
)
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1Server
|
||||
from mcp.shared._httpx_utils import McpHttpClientFactory
|
||||
from mcp.shared.memory import create_client_server_memory_streams
|
||||
|
|
@ -33,6 +36,7 @@ from fastmcp.client.auth.oauth import OAuth
|
|||
from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
|
||||
from fastmcp.server.dependencies import get_http_headers
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import EnvironmentConfig
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
@ -192,8 +196,6 @@ class SSETransport(ClientTransport):
|
|||
async def connect_session(
|
||||
self, **session_kwargs: Unpack[SessionKwargs]
|
||||
) -> AsyncIterator[ClientSession]:
|
||||
from mcp.client.sse import sse_client
|
||||
|
||||
client_kwargs: dict[str, Any] = {}
|
||||
|
||||
# load headers from an active HTTP request, if available. This will only be true
|
||||
|
|
@ -264,8 +266,6 @@ class StreamableHttpTransport(ClientTransport):
|
|||
async def connect_session(
|
||||
self, **session_kwargs: Unpack[SessionKwargs]
|
||||
) -> AsyncIterator[ClientSession]:
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
|
||||
client_kwargs: dict[str, Any] = {}
|
||||
|
||||
# load headers from an active HTTP request, if available. This will only be true
|
||||
|
|
@ -429,8 +429,6 @@ async def _stdio_transport_connect_task(
|
|||
"""A standalone connection task for a stdio transport. It is not a part of the StdioTransport class
|
||||
to ensure that the connection task does not hold a reference to the Transport object."""
|
||||
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
try:
|
||||
async with contextlib.AsyncExitStack() as stack:
|
||||
try:
|
||||
|
|
@ -598,16 +596,38 @@ class UvStdioTransport(StdioTransport):
|
|||
f"Project directory not found: {project_directory}"
|
||||
)
|
||||
|
||||
# Build uv arguments
|
||||
uv_args: list[str] = ["run"]
|
||||
if project_directory:
|
||||
uv_args.extend(["--directory", str(project_directory)])
|
||||
if python_version:
|
||||
uv_args.extend(["--python", python_version])
|
||||
for pkg in with_packages or []:
|
||||
uv_args.extend(["--with", pkg])
|
||||
if with_requirements:
|
||||
uv_args.extend(["--with-requirements", str(with_requirements)])
|
||||
# Create EnvironmentConfig from provided parameters (internal use)
|
||||
env_config = EnvironmentConfig(
|
||||
python=python_version,
|
||||
dependencies=with_packages,
|
||||
requirements=with_requirements,
|
||||
project=project_directory,
|
||||
editable=None, # Not exposed in this transport
|
||||
)
|
||||
|
||||
# Build uv arguments using the config
|
||||
uv_args: list[str] = []
|
||||
|
||||
# Check if we need any environment setup
|
||||
if env_config.needs_uv():
|
||||
# Use the config to build args, but we need to handle the command differently
|
||||
# since transport has specific needs
|
||||
uv_args = ["run"]
|
||||
|
||||
if python_version:
|
||||
uv_args.extend(["--python", python_version])
|
||||
if project_directory:
|
||||
uv_args.extend(["--directory", str(project_directory)])
|
||||
|
||||
# Note: Don't add fastmcp as dependency here, transport is for general use
|
||||
for pkg in with_packages or []:
|
||||
uv_args.extend(["--with", pkg])
|
||||
if with_requirements:
|
||||
uv_args.extend(["--with-requirements", str(with_requirements)])
|
||||
else:
|
||||
# No environment setup needed
|
||||
uv_args = ["run"]
|
||||
|
||||
if module:
|
||||
uv_args.append("--module")
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ from pydantic import (
|
|||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
ValidationInfo,
|
||||
model_validator,
|
||||
)
|
||||
from typing_extensions import Self, override
|
||||
|
|
@ -239,20 +238,24 @@ class MCPConfig(BaseModel):
|
|||
For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
|
||||
"""
|
||||
|
||||
mcpServers: dict[str, MCPServerTypes]
|
||||
mcpServers: dict[str, MCPServerTypes] = Field(default_factory=dict)
|
||||
|
||||
model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields
|
||||
|
||||
@model_validator(mode="before")
|
||||
def validate_mcp_servers(self, info: ValidationInfo) -> dict[str, Any]:
|
||||
"""Validate the MCP servers."""
|
||||
if not isinstance(self, dict):
|
||||
raise ValueError("MCPConfig format requires a dictionary of servers.")
|
||||
|
||||
if "mcpServers" not in self:
|
||||
self = {"mcpServers": self}
|
||||
|
||||
return self
|
||||
@classmethod
|
||||
def wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]:
|
||||
"""If there's no mcpServers key but there are server configs at root, wrap them."""
|
||||
if "mcpServers" not in values:
|
||||
# Check if any values look like server configs
|
||||
has_servers = any(
|
||||
isinstance(v, dict) and ("command" in v or "url" in v)
|
||||
for v in values.values()
|
||||
)
|
||||
if has_servers:
|
||||
# Move all server-like configs under mcpServers
|
||||
return {"mcpServers": values}
|
||||
return values
|
||||
|
||||
def add_server(self, name: str, server: MCPServerTypes) -> None:
|
||||
"""Add or update a server in the configuration."""
|
||||
|
|
@ -289,7 +292,7 @@ class CanonicalMCPConfig(MCPConfig):
|
|||
The format is designed to be client-agnostic and extensible for future use cases.
|
||||
"""
|
||||
|
||||
mcpServers: dict[str, CanonicalMCPServerTypes]
|
||||
mcpServers: dict[str, CanonicalMCPServerTypes] = Field(default_factory=dict)
|
||||
|
||||
@override
|
||||
def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
|
|
@ -222,7 +223,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
# Set up MCP protocol handlers
|
||||
self._setup_handlers()
|
||||
self.dependencies = dependencies or fastmcp.settings.server_dependencies
|
||||
|
||||
# Handle dependencies with deprecation warning
|
||||
# TODO: Remove dependencies parameter (deprecated in v2.11.4)
|
||||
if dependencies is not None:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
|
||||
"Please specify dependencies in a fastmcp.json configuration file instead:\n"
|
||||
'{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
|
||||
f"{json.dumps(dependencies)}\n }}\n}}\n"
|
||||
"See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.dependencies = (
|
||||
dependencies or fastmcp.settings.server_dependencies
|
||||
) # TODO: Remove (deprecated in v2.11.4)
|
||||
|
||||
self.include_fastmcp_meta = (
|
||||
include_fastmcp_meta
|
||||
|
|
|
|||
21
src/fastmcp/utilities/fastmcp_config/__init__.py
Normal file
21
src/fastmcp/utilities/fastmcp_config/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""FastMCP Configuration module.
|
||||
|
||||
This module provides versioned configuration support for FastMCP servers.
|
||||
The current version is v1, which is re-exported here for convenience.
|
||||
"""
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import (
|
||||
DeploymentConfig,
|
||||
EntrypointConfig,
|
||||
EnvironmentConfig,
|
||||
FastMCPConfig,
|
||||
generate_schema,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"FastMCPConfig",
|
||||
"EntrypointConfig",
|
||||
"EnvironmentConfig",
|
||||
"DeploymentConfig",
|
||||
"generate_schema",
|
||||
]
|
||||
678
src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py
Normal file
678
src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py
Normal file
|
|
@ -0,0 +1,678 @@
|
|||
"""FastMCP Configuration File Support.
|
||||
|
||||
This module provides support for fastmcp.json configuration files that allow
|
||||
users to specify server settings in a declarative format instead of using
|
||||
command-line arguments.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
logger = get_logger("cli.config")
|
||||
|
||||
# JSON Schema for IDE support
|
||||
FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
|
||||
|
||||
|
||||
class EntrypointConfig(BaseModel):
|
||||
"""Configuration for server entrypoint when using object format."""
|
||||
|
||||
file: str = Field(
|
||||
description="Path to Python file containing the server",
|
||||
examples=["server.py", "src/server.py", "app/main.py"],
|
||||
)
|
||||
|
||||
object: str | None = Field(
|
||||
default=None,
|
||||
description="Name of the server object in the file (defaults to searching for mcp/server/app)",
|
||||
examples=["app", "mcp", "server"],
|
||||
)
|
||||
|
||||
repo: str | None = Field(
|
||||
default=None,
|
||||
description="Git repository URL",
|
||||
examples=["https://github.com/user/repo"],
|
||||
)
|
||||
|
||||
|
||||
class EnvironmentConfig(BaseModel):
|
||||
"""Configuration for Python environment setup."""
|
||||
|
||||
python: str | None = Field(
|
||||
default=None,
|
||||
description="Python version constraint",
|
||||
examples=["3.10", "3.11", "3.12"],
|
||||
)
|
||||
|
||||
dependencies: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Python packages to install with PEP 508 specifiers",
|
||||
examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
|
||||
)
|
||||
|
||||
requirements: str | None = Field(
|
||||
default=None,
|
||||
description="Path to requirements.txt file",
|
||||
examples=["requirements.txt", "../requirements/prod.txt"],
|
||||
)
|
||||
|
||||
project: str | None = Field(
|
||||
default=None,
|
||||
description="Path to project directory containing pyproject.toml",
|
||||
examples=[".", "../my-project"],
|
||||
)
|
||||
|
||||
editable: str | None = Field(
|
||||
default=None,
|
||||
description="Directory to install in editable mode",
|
||||
examples=[".", "../my-package"],
|
||||
)
|
||||
|
||||
def build_uv_args(self, command: str | list[str] | None = None) -> list[str]:
|
||||
"""Build uv run arguments from this environment configuration.
|
||||
|
||||
Args:
|
||||
command: Optional command to append (string or list of args)
|
||||
|
||||
Returns:
|
||||
List of arguments for uv run command
|
||||
"""
|
||||
args = ["run"]
|
||||
|
||||
# Add Python version if specified
|
||||
if self.python:
|
||||
args.extend(["--python", self.python])
|
||||
|
||||
# Add project directory if specified
|
||||
if self.project:
|
||||
args.extend(["--project", str(self.project)])
|
||||
|
||||
# Add fastmcp as a base dependency
|
||||
args.extend(["--with", "fastmcp"])
|
||||
|
||||
# Add additional dependencies
|
||||
if self.dependencies:
|
||||
for dep in self.dependencies:
|
||||
args.extend(["--with", dep])
|
||||
|
||||
# Add requirements file
|
||||
if self.requirements:
|
||||
args.extend(["--with-requirements", str(self.requirements)])
|
||||
|
||||
# Add editable package
|
||||
if self.editable:
|
||||
args.extend(["--with-editable", str(self.editable)])
|
||||
|
||||
# Add the command if provided
|
||||
if command:
|
||||
if isinstance(command, str):
|
||||
args.append(command)
|
||||
else:
|
||||
args.extend(command)
|
||||
|
||||
return args
|
||||
|
||||
def run_with_uv(self, command: list[str]) -> None:
|
||||
"""Execute a command using uv run with this environment configuration.
|
||||
|
||||
Args:
|
||||
command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Build the full uv command
|
||||
uv_args = self.build_uv_args(command)
|
||||
cmd = ["uv"] + uv_args
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
# Run without capturing output so it flows through naturally
|
||||
process = subprocess.run(cmd, check=True)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed: {e}")
|
||||
sys.exit(e.returncode)
|
||||
|
||||
def needs_uv(self) -> bool:
|
||||
"""Check if this environment config requires uv to set up.
|
||||
|
||||
Returns:
|
||||
True if any environment settings require uv run
|
||||
"""
|
||||
return bool(
|
||||
self.python
|
||||
or self.dependencies
|
||||
or self.requirements
|
||||
or self.project
|
||||
or self.editable
|
||||
)
|
||||
|
||||
def merge_with_cli_args(
|
||||
self,
|
||||
python: str | None = None,
|
||||
with_packages: list[str] | None = None,
|
||||
with_requirements: Path | None = None,
|
||||
project: Path | None = None,
|
||||
with_editable: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Merge environment config with CLI arguments, with CLI taking precedence.
|
||||
|
||||
For packages, combines both config and CLI packages.
|
||||
For other fields, CLI takes precedence if provided.
|
||||
|
||||
Returns:
|
||||
Dictionary with merged arguments suitable for CLI commands
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
# Merge packages from both sources
|
||||
packages = []
|
||||
if self.dependencies:
|
||||
packages.extend(self.dependencies)
|
||||
if with_packages:
|
||||
packages.extend(with_packages)
|
||||
|
||||
return {
|
||||
"python": python or self.python,
|
||||
"with_packages": packages,
|
||||
"with_requirements": with_requirements
|
||||
or (Path(self.requirements) if self.requirements else None),
|
||||
"project": project or (Path(self.project) if self.project else None),
|
||||
"with_editable": with_editable
|
||||
or (Path(self.editable) if self.editable else None),
|
||||
}
|
||||
|
||||
|
||||
class DeploymentConfig(BaseModel):
|
||||
"""Configuration for server deployment and runtime settings."""
|
||||
|
||||
transport: Literal["stdio", "http", "sse"] | None = Field(
|
||||
default=None,
|
||||
description="Transport protocol to use",
|
||||
)
|
||||
|
||||
host: str | None = Field(
|
||||
default=None,
|
||||
description="Host to bind to when using HTTP transport",
|
||||
examples=["127.0.0.1", "0.0.0.0", "localhost"],
|
||||
)
|
||||
|
||||
port: int | None = Field(
|
||||
default=None,
|
||||
description="Port to bind to when using HTTP transport",
|
||||
examples=[8000, 3000, 5000],
|
||||
)
|
||||
|
||||
path: str | None = Field(
|
||||
default=None,
|
||||
description="URL path for the server endpoint",
|
||||
examples=["/mcp/", "/api/mcp/", "/sse/"],
|
||||
)
|
||||
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = Field(
|
||||
default=None,
|
||||
description="Log level for the server",
|
||||
)
|
||||
|
||||
cwd: str | None = Field(
|
||||
default=None,
|
||||
description="Working directory for the server process",
|
||||
examples=[".", "./src", "/app"],
|
||||
)
|
||||
|
||||
env: dict[str, str] | None = Field(
|
||||
default=None,
|
||||
description="Environment variables to set when running the server",
|
||||
examples=[{"API_KEY": "secret", "DEBUG": "true"}],
|
||||
)
|
||||
|
||||
args: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Arguments to pass to the server (after --)",
|
||||
examples=[["--config", "config.json", "--debug"]],
|
||||
)
|
||||
|
||||
def merge_with_cli_args(
|
||||
self,
|
||||
transport: str | None = None,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
path: str | None = None,
|
||||
log_level: str | None = None,
|
||||
server_args: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Merge deployment config with CLI arguments, with CLI taking precedence.
|
||||
|
||||
Returns:
|
||||
Dictionary with merged arguments suitable for CLI commands
|
||||
"""
|
||||
return {
|
||||
"transport": transport or self.transport,
|
||||
"host": host or self.host,
|
||||
"port": port or self.port,
|
||||
"path": path or self.path,
|
||||
"log_level": log_level or self.log_level,
|
||||
"server_args": server_args if server_args is not None else self.args,
|
||||
}
|
||||
|
||||
def apply_runtime_settings(self, config_path: Path | None = None) -> None:
|
||||
"""Apply runtime settings like environment variables and working directory.
|
||||
|
||||
Args:
|
||||
config_path: Path to config file for resolving relative paths
|
||||
|
||||
Environment variables support interpolation with ${VAR_NAME} syntax.
|
||||
For example: "API_URL": "https://api.${ENVIRONMENT}.example.com"
|
||||
will substitute the value of the ENVIRONMENT variable at runtime.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Set environment variables with interpolation support
|
||||
if self.env:
|
||||
for key, value in self.env.items():
|
||||
# Interpolate environment variables in the value
|
||||
interpolated_value = self._interpolate_env_vars(value)
|
||||
os.environ[key] = interpolated_value
|
||||
|
||||
# Change working directory
|
||||
if self.cwd:
|
||||
cwd_path = Path(self.cwd)
|
||||
if not cwd_path.is_absolute() and config_path:
|
||||
cwd_path = (config_path.parent / cwd_path).resolve()
|
||||
os.chdir(cwd_path)
|
||||
|
||||
def _interpolate_env_vars(self, value: str) -> str:
|
||||
"""Interpolate environment variables in a string.
|
||||
|
||||
Replaces ${VAR_NAME} with the value of VAR_NAME from the environment.
|
||||
If the variable is not set, the placeholder is left unchanged.
|
||||
|
||||
Args:
|
||||
value: String potentially containing ${VAR_NAME} placeholders
|
||||
|
||||
Returns:
|
||||
String with environment variables interpolated
|
||||
"""
|
||||
|
||||
def replace_var(match: re.Match) -> str:
|
||||
var_name = match.group(1)
|
||||
# Return the environment variable value if it exists, otherwise keep the placeholder
|
||||
return os.environ.get(var_name, match.group(0))
|
||||
|
||||
# Match ${VAR_NAME} pattern and replace with environment variable values
|
||||
return re.sub(r"\$\{([^}]+)\}", replace_var, value)
|
||||
|
||||
|
||||
class FastMCPConfig(BaseModel):
|
||||
"""Configuration for a FastMCP server.
|
||||
|
||||
This configuration file allows you to specify all settings needed to run
|
||||
a FastMCP server in a declarative format.
|
||||
"""
|
||||
|
||||
# Schema field for IDE support
|
||||
schema_: str | None = Field(
|
||||
default="https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
alias="$schema",
|
||||
description="JSON schema for IDE support and validation",
|
||||
)
|
||||
|
||||
# Server entrypoint - supports both string and object format
|
||||
entrypoint: EntrypointConfig = Field(
|
||||
description="Server entrypoint as a string (file or file:object) or object with file/object/repo",
|
||||
examples=[
|
||||
"server.py",
|
||||
"server.py:app",
|
||||
{"file": "src/server.py", "object": "app"},
|
||||
],
|
||||
)
|
||||
|
||||
# Environment configuration
|
||||
environment: EnvironmentConfig = Field(
|
||||
default_factory=lambda: EnvironmentConfig(),
|
||||
description="Python environment setup configuration",
|
||||
)
|
||||
|
||||
# Deployment configuration
|
||||
deployment: DeploymentConfig = Field(
|
||||
default_factory=lambda: DeploymentConfig(),
|
||||
description="Server deployment and runtime settings",
|
||||
)
|
||||
|
||||
# purely for static type checkers to avoid issues with providng str entrypoint
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, *, entrypoint: str | dict | EntrypointConfig, **data
|
||||
) -> None: ...
|
||||
@overload
|
||||
def __init__(
|
||||
self, *, environment: dict | EnvironmentConfig, **data
|
||||
) -> None: ...
|
||||
@overload
|
||||
def __init__(self, *, deployment: dict | DeploymentConfig, **data) -> None: ...
|
||||
def __init__(self, **data) -> None: ...
|
||||
|
||||
@field_validator("entrypoint", mode="before")
|
||||
@classmethod
|
||||
def validate_entrypoint(cls, v: str | EntrypointConfig) -> EntrypointConfig:
|
||||
"""Validate and convert entrypoint to proper format.
|
||||
|
||||
Supports:
|
||||
- String format: "server.py" or "server.py:object"
|
||||
- Object format: {"file": "server.py", "object": "app"}
|
||||
- EntrypointConfig instance (passed through)
|
||||
|
||||
The string format with :object syntax is automatically parsed into
|
||||
the object format for consistency.
|
||||
"""
|
||||
if isinstance(v, EntrypointConfig):
|
||||
# Already an EntrypointConfig instance, return as-is
|
||||
return v
|
||||
elif isinstance(v, dict):
|
||||
return EntrypointConfig(**v)
|
||||
elif isinstance(v, str):
|
||||
# Parse file.py:object syntax into object format if present
|
||||
if ":" in v:
|
||||
# Check if it's a Windows path (e.g., C:\...)
|
||||
has_windows_drive = len(v) > 1 and v[1] == ":"
|
||||
|
||||
# Only split if colon is not part of Windows drive
|
||||
if ":" in (v[2:] if has_windows_drive else v):
|
||||
file, obj = v.rsplit(":", 1)
|
||||
return EntrypointConfig(file=file, object=obj)
|
||||
else:
|
||||
return EntrypointConfig(file=v)
|
||||
|
||||
raise ValueError("entrypoint must be a string, EntrypointConfig instance")
|
||||
|
||||
@field_validator("environment", mode="before")
|
||||
@classmethod
|
||||
def validate_environment(cls, v: dict | EnvironmentConfig) -> EnvironmentConfig:
|
||||
"""Validate and convert environment to EnvironmentConfig.
|
||||
|
||||
Accepts:
|
||||
- EnvironmentConfig instance
|
||||
- dict that can be converted to EnvironmentConfig
|
||||
"""
|
||||
if isinstance(v, EnvironmentConfig):
|
||||
return v
|
||||
elif isinstance(v, dict):
|
||||
return EnvironmentConfig(**v) # type: ignore[arg-type]
|
||||
else:
|
||||
raise ValueError("environment must be a dict, EnvironmentConfig instance")
|
||||
|
||||
@field_validator("deployment", mode="before")
|
||||
@classmethod
|
||||
def validate_deployment(cls, v: dict | DeploymentConfig) -> DeploymentConfig:
|
||||
"""Validate and convert deployment to DeploymentConfig.
|
||||
|
||||
Accepts:
|
||||
- DeploymentConfig instance
|
||||
- dict that can be converted to DeploymentConfig
|
||||
|
||||
"""
|
||||
if isinstance(v, DeploymentConfig):
|
||||
return v
|
||||
elif isinstance(v, dict):
|
||||
return DeploymentConfig(**v) # type: ignore[arg-type]
|
||||
else:
|
||||
raise ValueError("deployment must be a dict, DeploymentConfig instance")
|
||||
|
||||
def get_entrypoint(self, config_path: Path | None = None) -> EntrypointConfig:
|
||||
"""Get the entrypoint as a structured object with resolved paths.
|
||||
|
||||
Args:
|
||||
config_path: Path to config file for resolving relative paths
|
||||
|
||||
Returns:
|
||||
EntrypointConfig object with file, object, and repo fields.
|
||||
If config_path is provided, relative file paths are resolved
|
||||
relative to the config file location.
|
||||
"""
|
||||
if isinstance(self.entrypoint, str):
|
||||
# Parse string format into structured object
|
||||
if ":" in self.entrypoint:
|
||||
file, obj = self.entrypoint.rsplit(":", 1)
|
||||
entrypoint = EntrypointConfig(file=file, object=obj)
|
||||
else:
|
||||
entrypoint = EntrypointConfig(file=self.entrypoint)
|
||||
else:
|
||||
# Already an EntrypointConfig
|
||||
entrypoint = self.entrypoint
|
||||
|
||||
# Resolve relative paths if config_path provided
|
||||
if config_path:
|
||||
file_path = Path(entrypoint.file)
|
||||
if not file_path.is_absolute():
|
||||
resolved_path = (config_path.parent / file_path).resolve()
|
||||
# Create new EntrypointConfig with resolved path
|
||||
entrypoint = EntrypointConfig(
|
||||
file=str(resolved_path),
|
||||
object=entrypoint.object,
|
||||
repo=entrypoint.repo,
|
||||
)
|
||||
|
||||
return entrypoint
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, file_path: Path) -> FastMCPConfig:
|
||||
"""Load configuration from a JSON file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the configuration file
|
||||
|
||||
Returns:
|
||||
FastMCPConfig instance
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file doesn't exist
|
||||
json.JSONDecodeError: If the file is not valid JSON
|
||||
pydantic.ValidationError: If the configuration is invalid
|
||||
"""
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"Configuration file not found: {file_path}")
|
||||
|
||||
with file_path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
return cls.model_validate(data)
|
||||
|
||||
@classmethod
|
||||
def from_cli_args(
|
||||
cls,
|
||||
entrypoint: str,
|
||||
transport: Literal["stdio", "http", "sse", "streamable-http"] | None = None,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
path: str | None = None,
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
| None = None,
|
||||
python: str | None = None,
|
||||
dependencies: list[str] | None = None,
|
||||
requirements: str | None = None,
|
||||
project: str | None = None,
|
||||
editable: str | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
args: list[str] | None = None,
|
||||
) -> FastMCPConfig:
|
||||
"""Create a config from CLI arguments.
|
||||
|
||||
This allows us to have a single code path where everything
|
||||
goes through a config object.
|
||||
|
||||
Args:
|
||||
entrypoint: Server entrypoint (file or file:object)
|
||||
transport: Transport protocol
|
||||
host: Host for HTTP transport
|
||||
port: Port for HTTP transport
|
||||
path: URL path for server
|
||||
log_level: Logging level
|
||||
python: Python version
|
||||
dependencies: Python packages to install
|
||||
requirements: Path to requirements file
|
||||
project: Path to project directory
|
||||
editable: Path to install in editable mode
|
||||
env: Environment variables
|
||||
cwd: Working directory
|
||||
args: Server arguments
|
||||
|
||||
Returns:
|
||||
FastMCPConfig instance
|
||||
"""
|
||||
# Build environment config if any env args provided
|
||||
environment = None
|
||||
if any([python, dependencies, requirements, project, editable]):
|
||||
environment = EnvironmentConfig(
|
||||
python=python,
|
||||
dependencies=dependencies,
|
||||
requirements=requirements,
|
||||
project=project,
|
||||
editable=editable,
|
||||
)
|
||||
|
||||
# Build deployment config if any deployment args provided
|
||||
deployment = None
|
||||
if any([transport, host, port, path, log_level, env, cwd, args]):
|
||||
# Convert streamable-http to http for backward compatibility
|
||||
if transport == "streamable-http":
|
||||
transport = "http" # type: ignore[assignment]
|
||||
deployment = DeploymentConfig(
|
||||
transport=transport, # type: ignore[arg-type]
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level=log_level,
|
||||
env=env,
|
||||
cwd=cwd,
|
||||
args=args,
|
||||
)
|
||||
|
||||
return cls(
|
||||
entrypoint=entrypoint,
|
||||
environment=environment,
|
||||
deployment=deployment,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def find_config(cls, start_path: Path | None = None) -> Path | None:
|
||||
"""Find a fastmcp.json file in the specified directory.
|
||||
|
||||
Args:
|
||||
start_path: Directory to look in (defaults to current directory)
|
||||
|
||||
Returns:
|
||||
Path to the configuration file, or None if not found
|
||||
"""
|
||||
if start_path is None:
|
||||
start_path = Path.cwd()
|
||||
|
||||
config_path = start_path / "fastmcp.json"
|
||||
if config_path.exists():
|
||||
logger.debug(f"Found configuration file: {config_path}")
|
||||
return config_path
|
||||
|
||||
return None
|
||||
|
||||
async def load_server(self, config_path: Path | None = None) -> Any:
|
||||
"""Load the server from the configuration.
|
||||
|
||||
This handles environment setup, working directory changes,
|
||||
and imports the server module.
|
||||
|
||||
Args:
|
||||
config_path: Path to the config file (for resolving relative paths)
|
||||
|
||||
Returns:
|
||||
The imported server object
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Set environment variables if specified
|
||||
if self.deployment and self.deployment.env:
|
||||
for key, value in self.deployment.env.items():
|
||||
os.environ[key] = value
|
||||
|
||||
# Change working directory if specified
|
||||
if self.deployment and self.deployment.cwd:
|
||||
cwd_path = Path(self.deployment.cwd)
|
||||
if not cwd_path.is_absolute():
|
||||
# If config_path provided, resolve relative to it
|
||||
if config_path:
|
||||
cwd_path = (config_path.parent / cwd_path).resolve()
|
||||
else:
|
||||
cwd_path = cwd_path.resolve()
|
||||
os.chdir(cwd_path)
|
||||
|
||||
# Get structured entrypoint with resolved paths
|
||||
entrypoint = self.get_entrypoint(config_path)
|
||||
|
||||
# Import the server
|
||||
from fastmcp.cli.run import import_server_with_args
|
||||
|
||||
file_path = Path(entrypoint.file)
|
||||
server_args = self.deployment.args if self.deployment else None
|
||||
|
||||
return await import_server_with_args(file_path, entrypoint.object, server_args)
|
||||
|
||||
async def run_server(self, **kwargs: Any) -> None:
|
||||
"""Load and run the server with this configuration.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional arguments to pass to server.run_async()
|
||||
These override config settings
|
||||
"""
|
||||
server = await self.load_server()
|
||||
|
||||
# Build run arguments from config
|
||||
run_args = {}
|
||||
if self.deployment:
|
||||
if self.deployment.transport:
|
||||
run_args["transport"] = self.deployment.transport
|
||||
if self.deployment.host:
|
||||
run_args["host"] = self.deployment.host
|
||||
if self.deployment.port:
|
||||
run_args["port"] = self.deployment.port
|
||||
if self.deployment.path:
|
||||
run_args["path"] = self.deployment.path
|
||||
# Note: log_level not currently supported by run_async
|
||||
|
||||
# Override with any provided kwargs
|
||||
run_args.update(kwargs)
|
||||
|
||||
# Run the server
|
||||
await server.run_async(**run_args)
|
||||
|
||||
|
||||
def generate_schema() -> dict[str, Any]:
|
||||
"""Generate JSON schema for fastmcp.json files.
|
||||
|
||||
This is used to create the schema file that IDEs can use for
|
||||
validation and auto-completion.
|
||||
|
||||
Returns:
|
||||
JSON schema as a dictionary
|
||||
"""
|
||||
schema = FastMCPConfig.model_json_schema()
|
||||
|
||||
# Add some metadata
|
||||
schema["$id"] = FASTMCP_JSON_SCHEMA
|
||||
schema["title"] = "FastMCP Configuration"
|
||||
schema["description"] = "Configuration file for FastMCP servers"
|
||||
|
||||
return schema
|
||||
361
src/fastmcp/utilities/fastmcp_config/v1/schema.json
Normal file
361
src/fastmcp/utilities/fastmcp_config/v1/schema.json
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
{
|
||||
"$defs": {
|
||||
"DeploymentConfig": {
|
||||
"description": "Configuration for server deployment and runtime settings.",
|
||||
"properties": {
|
||||
"transport": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"stdio",
|
||||
"http",
|
||||
"sse"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Transport protocol to use",
|
||||
"title": "Transport"
|
||||
},
|
||||
"host": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Host to bind to when using HTTP transport",
|
||||
"examples": [
|
||||
"127.0.0.1",
|
||||
"0.0.0.0",
|
||||
"localhost"
|
||||
],
|
||||
"title": "Host"
|
||||
},
|
||||
"port": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Port to bind to when using HTTP transport",
|
||||
"examples": [
|
||||
8000,
|
||||
3000,
|
||||
5000
|
||||
],
|
||||
"title": "Port"
|
||||
},
|
||||
"path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "URL path for the server endpoint",
|
||||
"examples": [
|
||||
"/mcp/",
|
||||
"/api/mcp/",
|
||||
"/sse/"
|
||||
],
|
||||
"title": "Path"
|
||||
},
|
||||
"log_level": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"WARNING",
|
||||
"ERROR",
|
||||
"CRITICAL"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Log level for the server",
|
||||
"title": "Log Level"
|
||||
},
|
||||
"cwd": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Working directory for the server process",
|
||||
"examples": [
|
||||
".",
|
||||
"./src",
|
||||
"/app"
|
||||
],
|
||||
"title": "Cwd"
|
||||
},
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Environment variables to set when running the server",
|
||||
"examples": [
|
||||
{
|
||||
"API_KEY": "secret",
|
||||
"DEBUG": "true"
|
||||
}
|
||||
],
|
||||
"title": "Env"
|
||||
},
|
||||
"args": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Arguments to pass to the server (after --)",
|
||||
"examples": [
|
||||
[
|
||||
"--config",
|
||||
"config.json",
|
||||
"--debug"
|
||||
]
|
||||
],
|
||||
"title": "Args"
|
||||
}
|
||||
},
|
||||
"title": "DeploymentConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"EntrypointConfig": {
|
||||
"description": "Configuration for server entrypoint when using object format.",
|
||||
"properties": {
|
||||
"file": {
|
||||
"description": "Path to Python file containing the server",
|
||||
"examples": [
|
||||
"server.py",
|
||||
"src/server.py",
|
||||
"app/main.py"
|
||||
],
|
||||
"title": "File",
|
||||
"type": "string"
|
||||
},
|
||||
"object": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Name of the server object in the file (defaults to searching for mcp/server/app)",
|
||||
"examples": [
|
||||
"app",
|
||||
"mcp",
|
||||
"server"
|
||||
],
|
||||
"title": "Object"
|
||||
},
|
||||
"repo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Git repository URL",
|
||||
"examples": [
|
||||
"https://github.com/user/repo"
|
||||
],
|
||||
"title": "Repo"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file"
|
||||
],
|
||||
"title": "EntrypointConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"EnvironmentConfig": {
|
||||
"description": "Configuration for Python environment setup.",
|
||||
"properties": {
|
||||
"python": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Python version constraint",
|
||||
"examples": [
|
||||
"3.10",
|
||||
"3.11",
|
||||
"3.12"
|
||||
],
|
||||
"title": "Python"
|
||||
},
|
||||
"dependencies": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Python packages to install with PEP 508 specifiers",
|
||||
"examples": [
|
||||
[
|
||||
"fastmcp>=2.0,<3",
|
||||
"httpx",
|
||||
"pandas>=2.0"
|
||||
]
|
||||
],
|
||||
"title": "Dependencies"
|
||||
},
|
||||
"requirements": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Path to requirements.txt file",
|
||||
"examples": [
|
||||
"requirements.txt",
|
||||
"../requirements/prod.txt"
|
||||
],
|
||||
"title": "Requirements"
|
||||
},
|
||||
"project": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Path to project directory containing pyproject.toml",
|
||||
"examples": [
|
||||
".",
|
||||
"../my-project"
|
||||
],
|
||||
"title": "Project"
|
||||
},
|
||||
"editable": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Directory to install in editable mode",
|
||||
"examples": [
|
||||
".",
|
||||
"../my-package"
|
||||
],
|
||||
"title": "Editable"
|
||||
}
|
||||
},
|
||||
"title": "EnvironmentConfig",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "Configuration file for FastMCP servers",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"description": "JSON schema for IDE support and validation",
|
||||
"title": "$Schema"
|
||||
},
|
||||
"entrypoint": {
|
||||
"$ref": "#/$defs/EntrypointConfig",
|
||||
"description": "Server entrypoint as a string (file or file:object) or object with file/object/repo",
|
||||
"examples": [
|
||||
"server.py",
|
||||
"server.py:app",
|
||||
{
|
||||
"file": "src/server.py",
|
||||
"object": "app"
|
||||
}
|
||||
]
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/$defs/EnvironmentConfig",
|
||||
"description": "Python environment setup configuration"
|
||||
},
|
||||
"deployment": {
|
||||
"$ref": "#/$defs/DeploymentConfig",
|
||||
"description": "Server deployment and runtime settings"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entrypoint"
|
||||
],
|
||||
"title": "FastMCP Configuration",
|
||||
"type": "object",
|
||||
"$id": "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
|
||||
}
|
||||
513
tests/cli/test_config.py
Normal file
513
tests/cli/test_config.py
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
"""Tests for FastMCP configuration file support with nested structure."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
DeploymentConfig,
|
||||
EntrypointConfig,
|
||||
EnvironmentConfig,
|
||||
FastMCPConfig,
|
||||
)
|
||||
|
||||
|
||||
class TestEntrypointConfig:
|
||||
"""Test EntrypointConfig class."""
|
||||
|
||||
def test_string_entrypoint(self):
|
||||
"""Test that string entrypoint is converted to EntrypointConfig."""
|
||||
config = FastMCPConfig(entrypoint="server.py")
|
||||
# With the new validator, this should be converted to EntrypointConfig
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "server.py"
|
||||
assert config.entrypoint.object is None
|
||||
|
||||
# get_entrypoint should return the same object
|
||||
entrypoint = config.get_entrypoint()
|
||||
assert isinstance(entrypoint, EntrypointConfig)
|
||||
assert entrypoint.file == "server.py"
|
||||
assert entrypoint.object is None
|
||||
|
||||
def test_string_entrypoint_with_object(self):
|
||||
"""Test string entrypoint with :object syntax."""
|
||||
config = FastMCPConfig(entrypoint="server.py:app")
|
||||
# With the new validator, this should be converted to EntrypointConfig
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "server.py"
|
||||
assert config.entrypoint.object == "app"
|
||||
|
||||
# get_entrypoint should return the same object
|
||||
entrypoint = config.get_entrypoint()
|
||||
assert isinstance(entrypoint, EntrypointConfig)
|
||||
assert entrypoint.file == "server.py"
|
||||
assert entrypoint.object == "app"
|
||||
|
||||
def test_object_entrypoint(self):
|
||||
"""Test EntrypointConfig object format."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint=EntrypointConfig(file="src/server.py", object="mcp")
|
||||
)
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "src/server.py"
|
||||
assert config.entrypoint.object == "mcp"
|
||||
|
||||
def test_get_entrypoint_path_resolution(self, tmp_path):
|
||||
"""Test that get_entrypoint resolves paths relative to config file."""
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir()
|
||||
server_dir = tmp_path / "src"
|
||||
server_dir.mkdir()
|
||||
server_file = server_dir / "server.py"
|
||||
server_file.write_text("# server")
|
||||
|
||||
config = FastMCPConfig(entrypoint="../src/server.py")
|
||||
entrypoint = config.get_entrypoint(config_dir / "fastmcp.json")
|
||||
|
||||
# Should resolve to absolute path
|
||||
assert Path(entrypoint.file).is_absolute()
|
||||
assert Path(entrypoint.file) == server_file.resolve()
|
||||
|
||||
|
||||
class TestEnvironmentConfig:
|
||||
"""Test EnvironmentConfig class."""
|
||||
|
||||
def test_environment_config_fields(self):
|
||||
"""Test all EnvironmentConfig fields."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={
|
||||
"python": "3.12",
|
||||
"dependencies": ["requests", "numpy>=2.0"],
|
||||
"requirements": "requirements.txt",
|
||||
"project": ".",
|
||||
"editable": "../my-package",
|
||||
},
|
||||
)
|
||||
|
||||
env = config.environment
|
||||
assert env.python == "3.12"
|
||||
assert env.dependencies == ["requests", "numpy>=2.0"]
|
||||
assert env.requirements == "requirements.txt"
|
||||
assert env.project == "."
|
||||
assert env.editable == "../my-package"
|
||||
|
||||
def test_needs_uv(self):
|
||||
"""Test needs_uv() method."""
|
||||
# No environment config - doesn't need UV
|
||||
config = FastMCPConfig(entrypoint="server.py")
|
||||
assert not config.environment.needs_uv()
|
||||
|
||||
# Empty environment - doesn't need UV
|
||||
config = FastMCPConfig(entrypoint="server.py", environment={})
|
||||
assert not config.environment.needs_uv()
|
||||
|
||||
# With dependencies - needs UV
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py", environment={"dependencies": ["requests"]}
|
||||
)
|
||||
assert config.environment.needs_uv()
|
||||
|
||||
# With Python version - needs UV
|
||||
config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
|
||||
assert config.environment.needs_uv()
|
||||
|
||||
def test_build_uv_args(self):
|
||||
"""Test build_uv_args() method."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={
|
||||
"python": "3.12",
|
||||
"dependencies": ["requests", "numpy"],
|
||||
"requirements": "requirements.txt",
|
||||
"project": ".",
|
||||
},
|
||||
)
|
||||
|
||||
args = config.environment.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
|
||||
assert args[0] == "run"
|
||||
assert "--python" in args
|
||||
assert "3.12" in args
|
||||
assert "--project" in args
|
||||
assert "--with" in args
|
||||
assert "fastmcp" in args
|
||||
assert "requests" in args
|
||||
assert "numpy" in args
|
||||
assert "--with-requirements" in args
|
||||
assert "requirements.txt" in args
|
||||
assert "fastmcp" in args[-3:]
|
||||
assert "run" in args[-2:]
|
||||
assert "server.py" in args[-1:]
|
||||
|
||||
def test_merge_with_cli_args(self):
|
||||
"""Test merge_with_cli_args() method."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={
|
||||
"python": "3.11",
|
||||
"dependencies": ["requests"],
|
||||
},
|
||||
)
|
||||
|
||||
# CLI args should take precedence
|
||||
merged = config.environment.merge_with_cli_args(
|
||||
python="3.12", # Override
|
||||
with_packages=["numpy"], # Add to dependencies
|
||||
with_requirements=None,
|
||||
project=None,
|
||||
)
|
||||
|
||||
assert merged["python"] == "3.12" # CLI override
|
||||
assert set(merged["with_packages"]) == {"requests", "numpy"} # Merged
|
||||
assert merged["with_requirements"] is None
|
||||
assert merged["project"] is None
|
||||
|
||||
def test_run_with_uv(self):
|
||||
"""Test run_with_uv() subprocess execution."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py", environment={"dependencies": ["requests"]}
|
||||
)
|
||||
|
||||
# run_with_uv calls sys.exit, so we expect SystemExit
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
# This will fail because we're running exit(1)
|
||||
# but it tests that the subprocess is called correctly
|
||||
config.environment.run_with_uv(["python", "-c", "exit(1)"])
|
||||
|
||||
# Check that it exited with code 1
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
|
||||
class TestDeploymentConfig:
|
||||
"""Test DeploymentConfig class."""
|
||||
|
||||
def test_deployment_config_fields(self):
|
||||
"""Test all DeploymentConfig fields."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={
|
||||
"transport": "http",
|
||||
"host": "0.0.0.0",
|
||||
"port": 8000,
|
||||
"path": "/api/",
|
||||
"log_level": "DEBUG",
|
||||
"env": {"API_KEY": "secret"},
|
||||
"cwd": "./work",
|
||||
"args": ["--debug"],
|
||||
},
|
||||
)
|
||||
|
||||
deploy = config.deployment
|
||||
assert deploy.transport == "http"
|
||||
assert deploy.host == "0.0.0.0"
|
||||
assert deploy.port == 8000
|
||||
assert deploy.path == "/api/"
|
||||
assert deploy.log_level == "DEBUG"
|
||||
assert deploy.env == {"API_KEY": "secret"}
|
||||
assert deploy.cwd == "./work"
|
||||
assert deploy.args == ["--debug"]
|
||||
|
||||
def test_merge_with_cli_args(self):
|
||||
"""Test DeploymentConfig merge_with_cli_args() method."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={
|
||||
"transport": "stdio",
|
||||
"port": 3000,
|
||||
"log_level": "INFO",
|
||||
},
|
||||
)
|
||||
|
||||
# CLI args should take precedence
|
||||
merged = config.deployment.merge_with_cli_args(
|
||||
transport="http", # Override
|
||||
host="localhost", # New value
|
||||
port=None, # Keep config value
|
||||
path=None,
|
||||
log_level="DEBUG", # Override
|
||||
server_args=["--test"],
|
||||
)
|
||||
|
||||
assert merged["transport"] == "http" # CLI override
|
||||
assert merged["host"] == "localhost" # CLI value
|
||||
assert merged["port"] == 3000 # Config value (CLI was None)
|
||||
assert merged["log_level"] == "DEBUG" # CLI override
|
||||
assert merged["server_args"] == ["--test"] # CLI value
|
||||
|
||||
def test_apply_runtime_settings(self, tmp_path):
|
||||
"""Test apply_runtime_settings() method."""
|
||||
import os
|
||||
|
||||
# Create config with env vars and cwd
|
||||
work_dir = tmp_path / "work"
|
||||
work_dir.mkdir()
|
||||
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={
|
||||
"env": {"TEST_VAR": "test_value"},
|
||||
"cwd": "work",
|
||||
},
|
||||
)
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
original_env = os.environ.get("TEST_VAR")
|
||||
|
||||
try:
|
||||
config.deployment.apply_runtime_settings(tmp_path / "fastmcp.json")
|
||||
|
||||
# Check environment variable was set
|
||||
assert os.environ["TEST_VAR"] == "test_value"
|
||||
|
||||
# Check working directory was changed
|
||||
assert Path.cwd() == work_dir.resolve()
|
||||
|
||||
finally:
|
||||
# Restore original state
|
||||
os.chdir(original_cwd)
|
||||
if original_env is None:
|
||||
os.environ.pop("TEST_VAR", None)
|
||||
else:
|
||||
os.environ["TEST_VAR"] = original_env
|
||||
|
||||
def test_env_var_interpolation(self, tmp_path):
|
||||
"""Test environment variable interpolation in deployment env."""
|
||||
import os
|
||||
|
||||
# Set up test environment variables
|
||||
os.environ["BASE_URL"] = "example.com"
|
||||
os.environ["ENV_NAME"] = "production"
|
||||
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={
|
||||
"env": {
|
||||
"API_URL": "https://api.${BASE_URL}/v1",
|
||||
"DATABASE": "postgres://${ENV_NAME}.db",
|
||||
"PREFIXED": "MY_${ENV_NAME}_SERVER",
|
||||
"MISSING": "value_${NONEXISTENT}_here",
|
||||
"STATIC": "no_interpolation",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
original_values = {
|
||||
key: os.environ.get(key)
|
||||
for key in ["API_URL", "DATABASE", "PREFIXED", "MISSING", "STATIC"]
|
||||
}
|
||||
|
||||
try:
|
||||
config.deployment.apply_runtime_settings()
|
||||
|
||||
# Check interpolated values
|
||||
assert os.environ["API_URL"] == "https://api.example.com/v1"
|
||||
assert os.environ["DATABASE"] == "postgres://production.db"
|
||||
assert os.environ["PREFIXED"] == "MY_production_SERVER"
|
||||
# Missing variables should keep the placeholder
|
||||
assert os.environ["MISSING"] == "value_${NONEXISTENT}_here"
|
||||
# Static values should remain unchanged
|
||||
assert os.environ["STATIC"] == "no_interpolation"
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
os.environ.pop("BASE_URL", None)
|
||||
os.environ.pop("ENV_NAME", None)
|
||||
for key, value in original_values.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
class TestFastMCPConfig:
|
||||
"""Test FastMCPConfig root configuration."""
|
||||
|
||||
def test_minimal_config(self):
|
||||
"""Test creating a config with only required fields."""
|
||||
config = FastMCPConfig(entrypoint="server.py")
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "server.py"
|
||||
assert config.entrypoint.object is None
|
||||
# Environment and deployment are now always present but empty
|
||||
assert isinstance(config.environment, EnvironmentConfig)
|
||||
assert isinstance(config.deployment, DeploymentConfig)
|
||||
# Check they have no values set
|
||||
assert not config.environment.needs_uv()
|
||||
assert all(
|
||||
getattr(config.deployment, field, None) is None
|
||||
for field in DeploymentConfig.model_fields
|
||||
)
|
||||
|
||||
def test_nested_structure(self):
|
||||
"""Test the nested configuration structure."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={
|
||||
"python": "3.12",
|
||||
"dependencies": ["fastmcp"],
|
||||
},
|
||||
deployment={
|
||||
"transport": "stdio",
|
||||
"log_level": "INFO",
|
||||
},
|
||||
)
|
||||
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "server.py"
|
||||
assert config.entrypoint.object is None
|
||||
assert isinstance(config.environment, EnvironmentConfig)
|
||||
assert isinstance(config.deployment, DeploymentConfig)
|
||||
|
||||
def test_from_file(self, tmp_path):
|
||||
"""Test loading config from JSON file with nested structure."""
|
||||
config_data = {
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": {"file": "src/server.py", "object": "app"},
|
||||
"environment": {"python": "3.12", "dependencies": ["requests"]},
|
||||
"deployment": {"transport": "http", "port": 8000},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
|
||||
# When loaded from JSON with object format, it becomes EntrypointConfig
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "src/server.py"
|
||||
assert config.entrypoint.object == "app"
|
||||
assert config.environment.python == "3.12"
|
||||
assert config.environment.dependencies == ["requests"]
|
||||
assert config.deployment.transport == "http"
|
||||
assert config.deployment.port == 8000
|
||||
|
||||
def test_from_file_with_string_entrypoint(self, tmp_path):
|
||||
"""Test loading config with string entrypoint."""
|
||||
config_data = {
|
||||
"entrypoint": "server.py:mcp",
|
||||
"environment": {"dependencies": ["fastmcp"]},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
# String entrypoint with : should be converted to EntrypointConfig
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "server.py"
|
||||
assert config.entrypoint.object == "mcp"
|
||||
|
||||
# get_entrypoint should return the same
|
||||
entrypoint = config.get_entrypoint()
|
||||
assert entrypoint.file == "server.py"
|
||||
assert entrypoint.object == "mcp"
|
||||
|
||||
def test_string_entrypoint_with_object_and_environment(self, tmp_path):
|
||||
"""Test that file.py:object syntax works with environment config."""
|
||||
config_data = {
|
||||
"entrypoint": "src/server.py:app",
|
||||
"environment": {"python": "3.12", "dependencies": ["fastmcp", "requests"]},
|
||||
"deployment": {"transport": "http", "port": 8000},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
|
||||
# Should be parsed into EntrypointConfig
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "src/server.py"
|
||||
assert config.entrypoint.object == "app"
|
||||
|
||||
# Environment config should still work
|
||||
assert config.environment.python == "3.12"
|
||||
assert config.environment.dependencies == ["fastmcp", "requests"]
|
||||
|
||||
# Deployment config should still work
|
||||
assert config.deployment.transport == "http"
|
||||
assert config.deployment.port == 8000
|
||||
|
||||
def test_find_config_in_current_dir(self, tmp_path):
|
||||
"""Test finding config in current directory."""
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
found = FastMCPConfig.find_config()
|
||||
assert found == config_file
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def test_find_config_not_in_parent_dir(self, tmp_path):
|
||||
"""Test that config is NOT found in parent directory."""
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
|
||||
|
||||
subdir = tmp_path / "subdir"
|
||||
subdir.mkdir()
|
||||
|
||||
# Should NOT find config in parent directory
|
||||
found = FastMCPConfig.find_config(subdir)
|
||||
assert found is None
|
||||
|
||||
def test_find_config_in_specified_dir(self, tmp_path):
|
||||
"""Test finding config in the specified directory."""
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
|
||||
|
||||
# Should find config when looking in the directory that contains it
|
||||
found = FastMCPConfig.find_config(tmp_path)
|
||||
assert found == config_file
|
||||
|
||||
def test_find_config_not_found(self, tmp_path):
|
||||
"""Test when config is not found."""
|
||||
found = FastMCPConfig.find_config(tmp_path)
|
||||
assert found is None
|
||||
|
||||
def test_invalid_transport(self, tmp_path):
|
||||
"""Test loading config with invalid transport value."""
|
||||
config_data = {
|
||||
"entrypoint": "server.py",
|
||||
"deployment": {"transport": "invalid_transport"},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
FastMCPConfig.from_file(config_file)
|
||||
|
||||
def test_optional_sections(self):
|
||||
"""Test that all config sections are optional except entrypoint."""
|
||||
# Only entrypoint is required
|
||||
config = FastMCPConfig(entrypoint="server.py")
|
||||
assert isinstance(config.entrypoint, EntrypointConfig)
|
||||
assert config.entrypoint.file == "server.py"
|
||||
# Environment and deployment are now always present but may be empty
|
||||
assert isinstance(config.environment, EnvironmentConfig)
|
||||
assert isinstance(config.deployment, DeploymentConfig)
|
||||
|
||||
# Only environment with values
|
||||
config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
|
||||
assert config.environment.python == "3.12"
|
||||
assert isinstance(config.deployment, DeploymentConfig)
|
||||
assert all(
|
||||
getattr(config.deployment, field, None) is None
|
||||
for field in DeploymentConfig.model_fields
|
||||
)
|
||||
|
||||
# Only deployment with values
|
||||
config = FastMCPConfig(entrypoint="server.py", deployment={"transport": "http"})
|
||||
assert isinstance(config.environment, EnvironmentConfig)
|
||||
assert all(
|
||||
getattr(config.environment, field, None) is None
|
||||
for field in EnvironmentConfig.model_fields
|
||||
)
|
||||
assert config.deployment.transport == "http"
|
||||
355
tests/cli/test_fastmcp_config_integration.py
Normal file
355
tests/cli/test_fastmcp_config_integration.py
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
"""Integration tests for fastmcp.json configuration system."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server_with_config(tmp_path):
|
||||
"""Create a complete server setup with fastmcp.json config."""
|
||||
# Create server file
|
||||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Config Test Server")
|
||||
|
||||
@mcp.tool
|
||||
def hello(name: str = "World") -> str:
|
||||
'''Say hello to someone'''
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@mcp.resource("resource://greeting")
|
||||
def get_greeting() -> str:
|
||||
'''Get a greeting message'''
|
||||
return "Welcome to FastMCP!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(mcp.run_async())
|
||||
""")
|
||||
|
||||
# Create config file
|
||||
config_data = {
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "server.py",
|
||||
"environment": {
|
||||
"python": sys.version.split()[0], # Use current Python version
|
||||
"dependencies": ["fastmcp"],
|
||||
},
|
||||
"deployment": {"transport": "stdio", "log_level": "INFO"},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data, indent=2))
|
||||
|
||||
return tmp_path
|
||||
|
||||
|
||||
class TestConfigFileDetection:
|
||||
"""Test configuration file detection patterns."""
|
||||
|
||||
def test_detect_standard_fastmcp_json(self, tmp_path):
|
||||
"""Test detection of standard fastmcp.json file."""
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
|
||||
|
||||
# Should be detected as fastmcp config
|
||||
assert "fastmcp.json" in config_file.name
|
||||
assert config_file.name.endswith("fastmcp.json")
|
||||
|
||||
def test_detect_prefixed_fastmcp_json(self, tmp_path):
|
||||
"""Test detection of prefixed fastmcp.json files."""
|
||||
config_file = tmp_path / "my.fastmcp.json"
|
||||
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
|
||||
|
||||
# Should be detected as fastmcp config
|
||||
assert "fastmcp.json" in config_file.name
|
||||
|
||||
def test_detect_test_fastmcp_json(self, tmp_path):
|
||||
"""Test detection of test_fastmcp.json file."""
|
||||
config_file = tmp_path / "test_fastmcp.json"
|
||||
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
|
||||
|
||||
# Should be detected as fastmcp config
|
||||
assert "fastmcp.json" in config_file.name
|
||||
|
||||
|
||||
class TestConfigWithClient:
|
||||
"""Test fastmcp.json configuration with client connections."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_server_with_client(self, server_with_config):
|
||||
"""Test that a server loaded from config works with a client."""
|
||||
# Load the config
|
||||
config_file = server_with_config / "fastmcp.json"
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
|
||||
# Import the server using the entrypoint
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
entrypoint = config.get_entrypoint(config_file)
|
||||
spec = importlib.util.spec_from_file_location("test_server", entrypoint.file)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load module from {entrypoint.file}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["test_server"] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
server = module.mcp
|
||||
|
||||
# Connect client to server
|
||||
async with Client(server) as client:
|
||||
# Test tool
|
||||
result = await client.call_tool("hello", {"name": "FastMCP"})
|
||||
assert result.data == "Hello, FastMCP!" # Use .data for string result
|
||||
|
||||
# Test resource
|
||||
results = await client.read_resource("resource://greeting")
|
||||
assert len(results) == 1
|
||||
# Resource results should have text content
|
||||
assert hasattr(results[0], "text") or hasattr(results[0], "contents")
|
||||
# Get the text content from the resource
|
||||
text = getattr(results[0], "text", None) or getattr(
|
||||
results[0], "contents", ""
|
||||
)
|
||||
assert "Welcome to FastMCP!" in str(text)
|
||||
|
||||
|
||||
class TestEnvironmentExecution:
|
||||
"""Test environment configuration execution paths."""
|
||||
|
||||
def test_needs_uv_with_dependencies(self):
|
||||
"""Test that environment with dependencies needs UV."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={"dependencies": ["requests", "numpy"]}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert config.environment is not None
|
||||
assert config.environment.needs_uv()
|
||||
|
||||
def test_needs_uv_with_python_version(self):
|
||||
"""Test that environment with Python version needs UV."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={"python": "3.12"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert config.environment is not None
|
||||
assert config.environment.needs_uv()
|
||||
|
||||
def test_no_uv_needed_without_environment(self):
|
||||
"""Test that no UV is needed without environment config."""
|
||||
config = FastMCPConfig(entrypoint="server.py")
|
||||
|
||||
# Environment is now always present but may be empty
|
||||
assert config.environment is not None
|
||||
assert not config.environment.needs_uv()
|
||||
|
||||
def test_no_uv_needed_with_empty_environment(self):
|
||||
"""Test that no UV is needed with empty environment config."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert config.environment is not None
|
||||
assert not config.environment.needs_uv()
|
||||
|
||||
|
||||
class TestCLIArgumentMerging:
|
||||
"""Test CLI argument merging with config values."""
|
||||
|
||||
def test_cli_overrides_environment(self):
|
||||
"""Test that CLI args override environment config."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={"python": "3.11", "dependencies": ["requests"]}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert config.environment is not None
|
||||
merged = config.environment.merge_with_cli_args(
|
||||
python="3.12", # Override Python version
|
||||
with_packages=["numpy"], # Add package
|
||||
with_requirements=None,
|
||||
project=None,
|
||||
)
|
||||
|
||||
assert merged["python"] == "3.12" # CLI wins
|
||||
assert "requests" in merged["with_packages"] # From config
|
||||
assert "numpy" in merged["with_packages"] # From CLI
|
||||
|
||||
def test_cli_overrides_deployment(self):
|
||||
"""Test that CLI args override deployment config."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"transport": "stdio", "port": 3000, "log_level": "INFO"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert config.deployment is not None
|
||||
merged = config.deployment.merge_with_cli_args(
|
||||
transport="http", # Override transport
|
||||
host="localhost", # New value
|
||||
port=8080, # Override port
|
||||
path=None,
|
||||
log_level="DEBUG", # Override log level
|
||||
server_args=None,
|
||||
)
|
||||
|
||||
assert merged["transport"] == "http" # CLI wins
|
||||
assert merged["host"] == "localhost" # CLI value
|
||||
assert merged["port"] == 8080 # CLI wins
|
||||
assert merged["log_level"] == "DEBUG" # CLI wins
|
||||
|
||||
def test_config_values_when_cli_is_none(self):
|
||||
"""Test that config values are used when CLI args are None."""
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"transport": "http", "port": 3000}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert config.deployment is not None
|
||||
merged = config.deployment.merge_with_cli_args(
|
||||
transport=None, # Use config
|
||||
host=None, # No value
|
||||
port=None, # Use config
|
||||
path=None,
|
||||
log_level=None,
|
||||
server_args=None,
|
||||
)
|
||||
|
||||
assert merged["transport"] == "http" # From config
|
||||
assert merged["port"] == 3000 # From config
|
||||
assert merged["host"] is None # No value provided
|
||||
|
||||
|
||||
class TestPathResolution:
|
||||
"""Test path resolution in configurations."""
|
||||
|
||||
def test_entrypoint_path_resolution(self, tmp_path):
|
||||
"""Test that entrypoint paths are resolved relative to config."""
|
||||
# Create nested directory structure
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir()
|
||||
src_dir = tmp_path / "src"
|
||||
src_dir.mkdir()
|
||||
|
||||
# Server is in src, config is in config
|
||||
server_file = src_dir / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
config = FastMCPConfig(entrypoint="../src/server.py")
|
||||
|
||||
# Get entrypoint resolved relative to config location
|
||||
config_file = config_dir / "fastmcp.json"
|
||||
entrypoint = config.get_entrypoint(config_file)
|
||||
|
||||
# Should resolve to absolute path of server file
|
||||
assert Path(entrypoint.file) == server_file.resolve()
|
||||
|
||||
def test_cwd_path_resolution(self, tmp_path):
|
||||
"""Test that working directory is resolved relative to config."""
|
||||
import os
|
||||
|
||||
# Create directory structure
|
||||
work_dir = tmp_path / "work"
|
||||
work_dir.mkdir()
|
||||
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"cwd": "work"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
# Apply runtime settings relative to config location
|
||||
assert config.deployment is not None
|
||||
config.deployment.apply_runtime_settings(tmp_path / "fastmcp.json")
|
||||
|
||||
# Should change to work directory
|
||||
assert Path.cwd() == work_dir.resolve()
|
||||
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def test_requirements_path_resolution(self, tmp_path):
|
||||
"""Test that requirements path is resolved correctly."""
|
||||
# Create requirements file
|
||||
reqs_file = tmp_path / "requirements.txt"
|
||||
reqs_file.write_text("fastmcp>=2.0")
|
||||
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
environment={"requirements": "requirements.txt"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
# Build UV args
|
||||
assert config.environment is not None
|
||||
uv_args = config.environment.build_uv_args(["fastmcp", "run"])
|
||||
|
||||
# Should include requirements file
|
||||
assert "--with-requirements" in uv_args
|
||||
req_idx = uv_args.index("--with-requirements") + 1
|
||||
assert uv_args[req_idx] == "requirements.txt"
|
||||
|
||||
|
||||
class TestConfigValidation:
|
||||
"""Test configuration validation."""
|
||||
|
||||
def test_invalid_transport_rejected(self):
|
||||
"""Test that invalid transport values are rejected."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"transport": "invalid_transport"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def test_streamable_http_transport_rejected(self):
|
||||
"""Test that streamable-http transport is rejected in fastmcp.json config."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"transport": "streamable-http"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def test_invalid_log_level_rejected(self):
|
||||
"""Test that invalid log level values are rejected."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"log_level": "INVALID"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def test_missing_entrypoint_rejected(self):
|
||||
"""Test that config without entrypoint is rejected."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig() # type: ignore[call-arg]
|
||||
|
||||
def test_valid_transport_values(self):
|
||||
"""Test that all valid transport values are accepted."""
|
||||
for transport in ["stdio", "http", "sse"]:
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"transport": transport}, # type: ignore[arg-type]
|
||||
)
|
||||
assert config.deployment is not None
|
||||
assert config.deployment.transport == transport
|
||||
|
||||
def test_valid_log_levels(self):
|
||||
"""Test that all valid log levels are accepted."""
|
||||
for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
||||
config = FastMCPConfig(
|
||||
entrypoint="server.py",
|
||||
deployment={"log_level": level}, # type: ignore[arg-type]
|
||||
)
|
||||
assert config.deployment is not None
|
||||
assert config.deployment.log_level == level
|
||||
163
tests/cli/test_fastmcp_config_schema.py
Normal file
163
tests/cli/test_fastmcp_config_schema.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"""Test that the JSON schema file matches the Pydantic model."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import generate_schema
|
||||
|
||||
|
||||
def test_schema_file_matches_pydantic_model():
|
||||
"""Test that the schema.json file matches what the Pydantic model generates."""
|
||||
# Path to the schema file
|
||||
schema_file = (
|
||||
Path(__file__).parent.parent.parent
|
||||
/ "src"
|
||||
/ "fastmcp"
|
||||
/ "utilities"
|
||||
/ "fastmcp_config"
|
||||
/ "v1"
|
||||
/ "schema.json"
|
||||
)
|
||||
|
||||
# Load the schema file
|
||||
with open(schema_file) as f:
|
||||
file_schema = json.load(f)
|
||||
|
||||
# Generate schema from Pydantic model
|
||||
generated_schema = generate_schema()
|
||||
|
||||
# They should be identical
|
||||
assert file_schema == generated_schema, (
|
||||
"The schema.json file does not match the Pydantic model schema. "
|
||||
"Please regenerate the schema file by running:\n"
|
||||
'uv run python -c "from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import generate_schema; '
|
||||
'import json; print(json.dumps(generate_schema(), indent=2))" > '
|
||||
f"{schema_file}"
|
||||
)
|
||||
|
||||
|
||||
def test_schema_has_correct_id():
|
||||
"""Test that the schema has the correct $id field."""
|
||||
generated_schema = generate_schema()
|
||||
|
||||
assert "$id" in generated_schema
|
||||
assert (
|
||||
generated_schema["$id"]
|
||||
== "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
|
||||
)
|
||||
|
||||
|
||||
def test_schema_has_required_fields():
|
||||
"""Test that the schema specifies the required fields correctly."""
|
||||
generated_schema = generate_schema()
|
||||
|
||||
# Check that entrypoint is required
|
||||
assert "required" in generated_schema
|
||||
assert "entrypoint" in generated_schema["required"]
|
||||
|
||||
# Check that entrypoint is in properties
|
||||
assert "properties" in generated_schema
|
||||
assert "entrypoint" in generated_schema["properties"]
|
||||
|
||||
|
||||
def test_schema_nested_structure():
|
||||
"""Test that the schema has the correct nested structure."""
|
||||
generated_schema = generate_schema()
|
||||
|
||||
properties = generated_schema["properties"]
|
||||
|
||||
# Check environment section
|
||||
assert "environment" in properties
|
||||
env_schema = properties["environment"]
|
||||
if "properties" in env_schema:
|
||||
env_props = env_schema["properties"]
|
||||
assert "python" in env_props
|
||||
assert "dependencies" in env_props
|
||||
assert "requirements" in env_props
|
||||
assert "project" in env_props
|
||||
assert "editable" in env_props
|
||||
|
||||
# Check deployment section
|
||||
assert "deployment" in properties
|
||||
deploy_schema = properties["deployment"]
|
||||
if "properties" in deploy_schema:
|
||||
deploy_props = deploy_schema["properties"]
|
||||
assert "transport" in deploy_props
|
||||
assert "host" in deploy_props
|
||||
assert "port" in deploy_props
|
||||
assert "log_level" in deploy_props
|
||||
assert "env" in deploy_props
|
||||
assert "cwd" in deploy_props
|
||||
assert "args" in deploy_props
|
||||
|
||||
|
||||
def test_schema_transport_enum():
|
||||
"""Test that transport field has correct enum values."""
|
||||
generated_schema = generate_schema()
|
||||
|
||||
# Navigate to transport field
|
||||
deploy_schema = generated_schema["properties"]["deployment"]
|
||||
|
||||
# Handle both direct properties and anyOf cases
|
||||
if "anyOf" in deploy_schema:
|
||||
# Find the object type in anyOf
|
||||
for option in deploy_schema["anyOf"]:
|
||||
if option.get("type") == "object" and "properties" in option:
|
||||
transport_schema = option["properties"].get("transport", {})
|
||||
if "anyOf" in transport_schema:
|
||||
# Look for enum in anyOf options
|
||||
for trans_option in transport_schema["anyOf"]:
|
||||
if "enum" in trans_option:
|
||||
valid_transports = trans_option["enum"]
|
||||
assert "stdio" in valid_transports
|
||||
assert "http" in valid_transports
|
||||
assert "sse" in valid_transports
|
||||
break
|
||||
elif "properties" in deploy_schema:
|
||||
transport_schema = deploy_schema["properties"].get("transport", {})
|
||||
if "anyOf" in transport_schema:
|
||||
for option in transport_schema["anyOf"]:
|
||||
if "enum" in option:
|
||||
valid_transports = option["enum"]
|
||||
assert "stdio" in valid_transports
|
||||
assert "http" in valid_transports
|
||||
assert "sse" in valid_transports
|
||||
break
|
||||
|
||||
|
||||
def test_schema_log_level_enum():
|
||||
"""Test that log_level field has correct enum values."""
|
||||
generated_schema = generate_schema()
|
||||
|
||||
# Navigate to log_level field
|
||||
deploy_schema = generated_schema["properties"]["deployment"]
|
||||
|
||||
# Handle both direct properties and anyOf cases
|
||||
if "anyOf" in deploy_schema:
|
||||
# Find the object type in anyOf
|
||||
for option in deploy_schema["anyOf"]:
|
||||
if option.get("type") == "object" and "properties" in option:
|
||||
log_level_schema = option["properties"].get("log_level", {})
|
||||
if "anyOf" in log_level_schema:
|
||||
# Look for enum in anyOf options
|
||||
for level_option in log_level_schema["anyOf"]:
|
||||
if "enum" in level_option:
|
||||
valid_levels = level_option["enum"]
|
||||
assert "DEBUG" in valid_levels
|
||||
assert "INFO" in valid_levels
|
||||
assert "WARNING" in valid_levels
|
||||
assert "ERROR" in valid_levels
|
||||
assert "CRITICAL" in valid_levels
|
||||
break
|
||||
elif "properties" in deploy_schema:
|
||||
log_level_schema = deploy_schema["properties"].get("log_level", {})
|
||||
if "anyOf" in log_level_schema:
|
||||
for option in log_level_schema["anyOf"]:
|
||||
if "enum" in option:
|
||||
valid_levels = option["enum"]
|
||||
assert "DEBUG" in valid_levels
|
||||
assert "INFO" in valid_levels
|
||||
assert "WARNING" in valid_levels
|
||||
assert "ERROR" in valid_levels
|
||||
assert "CRITICAL" in valid_levels
|
||||
break
|
||||
298
tests/cli/test_run_config.py
Normal file
298
tests/cli/test_run_config.py
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
"""Integration tests for FastMCP configuration with run command."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp.cli.run import load_fastmcp_config
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
DeploymentConfig,
|
||||
EntrypointConfig,
|
||||
EnvironmentConfig,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_config(tmp_path):
|
||||
"""Create a sample fastmcp.json configuration file with nested structure."""
|
||||
config_data = {
|
||||
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
|
||||
"entrypoint": "server.py",
|
||||
"environment": {"python": "3.11", "dependencies": ["requests"]},
|
||||
"deployment": {"transport": "stdio", "env": {"TEST_VAR": "test_value"}},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data, indent=2))
|
||||
|
||||
# Create a simple server file
|
||||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
@mcp.tool
|
||||
def test_tool(message: str) -> str:
|
||||
return f"Echo: {message}"
|
||||
""")
|
||||
|
||||
return config_file
|
||||
|
||||
|
||||
def test_load_fastmcp_config(sample_config, monkeypatch):
|
||||
"""Test loading configuration and returning config subsets."""
|
||||
|
||||
# Capture environment changes
|
||||
original_env = dict(os.environ)
|
||||
|
||||
try:
|
||||
entrypoint, deployment, environment = load_fastmcp_config(sample_config)
|
||||
|
||||
# Check that we got the right types
|
||||
assert isinstance(entrypoint, EntrypointConfig)
|
||||
assert isinstance(deployment, DeploymentConfig)
|
||||
assert isinstance(environment, EnvironmentConfig)
|
||||
|
||||
# Check entrypoint
|
||||
assert entrypoint.file.endswith("server.py")
|
||||
assert Path(entrypoint.file).is_absolute()
|
||||
assert entrypoint.object is None
|
||||
|
||||
# Check environment config
|
||||
assert environment.python == "3.11"
|
||||
assert environment.dependencies == ["requests"]
|
||||
|
||||
# Check deployment config
|
||||
assert deployment.transport == "stdio"
|
||||
assert deployment.env == {"TEST_VAR": "test_value"}
|
||||
|
||||
# Check that environment variables were applied
|
||||
assert os.environ.get("TEST_VAR") == "test_value"
|
||||
|
||||
finally:
|
||||
# Restore original environment
|
||||
os.environ.clear()
|
||||
os.environ.update(original_env)
|
||||
|
||||
|
||||
def test_load_config_with_object_entrypoint(tmp_path):
|
||||
"""Test loading config with object-format entrypoint."""
|
||||
config_data = {
|
||||
"entrypoint": {"file": "src/server.py", "object": "app"},
|
||||
"deployment": {"transport": "http", "port": 8000},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Create the server file in subdirectory
|
||||
src_dir = tmp_path / "src"
|
||||
src_dir.mkdir()
|
||||
server_file = src_dir / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_file)
|
||||
|
||||
# Check entrypoint resolution
|
||||
assert entrypoint.file == str(server_file.resolve())
|
||||
assert entrypoint.object == "app"
|
||||
|
||||
# Check deployment
|
||||
assert deployment is not None
|
||||
assert deployment.transport == "http"
|
||||
assert deployment.port == 8000
|
||||
|
||||
# No environment config
|
||||
assert environment is None
|
||||
|
||||
|
||||
def test_load_config_with_cwd(tmp_path):
|
||||
"""Test that DeploymentConfig applies working directory change."""
|
||||
|
||||
# Create a subdirectory
|
||||
subdir = tmp_path / "subdir"
|
||||
subdir.mkdir()
|
||||
|
||||
config_data = {"entrypoint": "server.py", "deployment": {"cwd": "subdir"}}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Create server file in subdirectory
|
||||
server_file = subdir / "server.py"
|
||||
server_file.write_text("# Test server")
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_file)
|
||||
|
||||
# Check that working directory was changed
|
||||
assert Path.cwd() == subdir.resolve()
|
||||
|
||||
finally:
|
||||
# Restore original working directory
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
def test_load_config_with_relative_cwd(tmp_path):
|
||||
"""Test configuration with relative working directory."""
|
||||
|
||||
# Create nested subdirectories
|
||||
subdir1 = tmp_path / "dir1"
|
||||
subdir2 = subdir1 / "dir2"
|
||||
subdir2.mkdir(parents=True)
|
||||
|
||||
config_data = {
|
||||
"entrypoint": "server.py",
|
||||
"deployment": {
|
||||
"cwd": "../" # Relative to config file location
|
||||
},
|
||||
}
|
||||
|
||||
config_file = subdir2 / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Create server file in parent directory
|
||||
server_file = subdir1 / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_file)
|
||||
|
||||
# Should change to parent directory of config file
|
||||
assert Path.cwd() == subdir1.resolve()
|
||||
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
def test_load_minimal_config(tmp_path):
|
||||
"""Test loading minimal configuration with only entrypoint."""
|
||||
config_data = {"entrypoint": "server.py"}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Create server file
|
||||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_file)
|
||||
|
||||
# Check we got entrypoint
|
||||
assert isinstance(entrypoint, EntrypointConfig)
|
||||
assert entrypoint.file == str(server_file.resolve())
|
||||
|
||||
# No deployment or environment
|
||||
assert deployment is None
|
||||
assert environment is None
|
||||
|
||||
|
||||
def test_load_config_with_server_args(tmp_path):
|
||||
"""Test configuration with server arguments."""
|
||||
config_data = {
|
||||
"entrypoint": "server.py",
|
||||
"deployment": {"args": ["--debug", "--config", "custom.json"]},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Create server file
|
||||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_file)
|
||||
|
||||
assert deployment is not None
|
||||
assert deployment.args == ["--debug", "--config", "custom.json"]
|
||||
|
||||
|
||||
def test_config_subset_independence(tmp_path):
|
||||
"""Test that config subsets can be used independently."""
|
||||
config_data = {
|
||||
"entrypoint": "server.py",
|
||||
"environment": {"python": "3.12", "dependencies": ["pandas"]},
|
||||
"deployment": {"transport": "http", "host": "0.0.0.0", "port": 3000},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Create server file
|
||||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_file)
|
||||
|
||||
# Each subset should be independently usable
|
||||
assert entrypoint.file == str(server_file.resolve())
|
||||
assert entrypoint.object is None
|
||||
|
||||
assert environment is not None
|
||||
assert environment.python == "3.12"
|
||||
assert environment.dependencies == ["pandas"]
|
||||
assert environment.needs_uv() # Has dependencies
|
||||
|
||||
assert deployment is not None
|
||||
assert deployment.transport == "http"
|
||||
assert deployment.host == "0.0.0.0"
|
||||
assert deployment.port == 3000
|
||||
|
||||
# Can merge deployment config with CLI args
|
||||
merged = deployment.merge_with_cli_args(
|
||||
transport=None, # Keep config value
|
||||
host="localhost", # Override
|
||||
port=8080, # Override
|
||||
path="/api", # New value
|
||||
log_level=None,
|
||||
server_args=None,
|
||||
)
|
||||
|
||||
assert merged["transport"] == "http" # Kept from config
|
||||
assert merged["host"] == "localhost" # CLI override
|
||||
assert merged["port"] == 8080 # CLI override
|
||||
assert merged["path"] == "/api" # CLI value
|
||||
|
||||
|
||||
def test_environment_config_path_resolution(tmp_path):
|
||||
"""Test that paths in environment config are resolved correctly."""
|
||||
# Create requirements file
|
||||
reqs_file = tmp_path / "requirements.txt"
|
||||
reqs_file.write_text("fastmcp>=2.0")
|
||||
|
||||
config_data = {
|
||||
"entrypoint": "server.py",
|
||||
"environment": {
|
||||
"requirements": "requirements.txt",
|
||||
"project": ".",
|
||||
"editable": "../other-project",
|
||||
},
|
||||
}
|
||||
|
||||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
# Create server file
|
||||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
entrypoint, deployment, environment = load_fastmcp_config(config_file)
|
||||
|
||||
# Check that UV args are built with resolved paths
|
||||
assert environment is not None
|
||||
uv_args = environment.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
|
||||
assert "--with-requirements" in uv_args
|
||||
assert "--project" in uv_args
|
||||
# Path should be resolved relative to config file
|
||||
req_idx = uv_args.index("--with-requirements") + 1
|
||||
assert (
|
||||
Path(uv_args[req_idx]).is_absolute() or uv_args[req_idx] == "requirements.txt"
|
||||
)
|
||||
30
tests/deprecated/test_dependencies.py
Normal file
30
tests/deprecated/test_dependencies.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""Tests for deprecated dependencies parameter.
|
||||
|
||||
This entire file can be deleted when the dependencies parameter is removed (deprecated in v2.11.4).
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
||||
def test_dependencies_parameter_deprecated():
|
||||
"""Test that using the dependencies parameter raises a deprecation warning."""
|
||||
|
||||
with pytest.warns(DeprecationWarning, match="deprecated as of FastMCP 2.11.4"):
|
||||
server = FastMCP("Test Server", dependencies=["pandas", "numpy"])
|
||||
|
||||
# Should still work for backward compatibility
|
||||
assert server.dependencies == ["pandas", "numpy"]
|
||||
|
||||
|
||||
def test_no_warning_without_dependencies():
|
||||
"""Test that no warning is raised when dependencies are not used."""
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error") # Turn warnings into errors
|
||||
server = FastMCP("Test Server") # Should not raise
|
||||
|
||||
assert server.dependencies == [] # Should use default empty list
|
||||
Loading…
Add table
Add a link
Reference in a new issue