feat: introduce fastmcp.json configuration system (#1517)

This commit is contained in:
Jeremiah Lowin 2025-08-19 16:06:04 -04:00 committed by GitHub
commit 2d3d5392f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3917 additions and 108 deletions

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

View file

@ -110,6 +110,7 @@
"icon": "rocket",
"pages": [
"deployment/running-server",
"deployment/server-configuration",
"deployment/testing",
"deployment/self-hosted",
"deployment/fastmcp-cloud"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
v1.json

View file

@ -0,0 +1 @@
../../../src/fastmcp/utilities/fastmcp_config/v1/schema.json

View file

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

View 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"
]
}
}

View file

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

View 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}"
}
}
}

View 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"
}
}

View 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
}
}

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

View file

@ -0,0 +1,7 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp/v1.json",
"entrypoint": "server.py",
"deployment": {
"transport": "stdio"
}
}

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

View 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"
}
}

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

View 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"
]
}
}

View file

@ -0,0 +1,4 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": "mount_example.py"
}

View file

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

View file

@ -0,0 +1,7 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": "screenshot.py",
"environment": {
"dependencies": ["pyautogui", "Pillow"]
}
}

View 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"
]
}
}

View 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"
]
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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",
]

View 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

View 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
View 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"

View 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

View 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

View 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"
)

View 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