Implement typed source system for FastMCP configuration (#1607)

This commit is contained in:
Jeremiah Lowin 2025-08-24 16:32:37 -04:00 committed by GitHub
commit 1f3f82d245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1656 additions and 1602 deletions

6
.gitignore vendored
View file

@ -64,7 +64,13 @@ dmypy.json
# Claude worktree management
.claude-wt/worktrees
# Agents
/PLAN.md
/TODO.md
/STATUS.md
# Common FastMCP test files
/test.py
/server.py
/client.py
/test.json

View file

@ -1,6 +1,6 @@
{
"$defs": {
"DeploymentConfig": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
@ -159,64 +159,10 @@
"title": "Args"
}
},
"title": "DeploymentConfig",
"title": "Deployment",
"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": {
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
@ -312,7 +258,42 @@
"title": "Editable"
}
},
"title": "EnvironmentConfig",
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
@ -327,35 +308,41 @@
"type": "null"
}
],
"default": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/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",
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
"server.py",
"server.py:app",
{
"file": "src/server.py",
"object": "app"
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/EnvironmentConfig",
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/DeploymentConfig",
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": [
"entrypoint"
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
}

View file

@ -1,6 +1,6 @@
{
"$defs": {
"DeploymentConfig": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
@ -159,64 +159,10 @@
"title": "Args"
}
},
"title": "DeploymentConfig",
"title": "Deployment",
"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": {
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
@ -312,7 +258,42 @@
"title": "Editable"
}
},
"title": "EnvironmentConfig",
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
@ -327,35 +308,41 @@
"type": "null"
}
],
"default": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/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",
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
"server.py",
"server.py:app",
{
"file": "src/server.py",
"object": "app"
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/EnvironmentConfig",
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/DeploymentConfig",
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": [
"entrypoint"
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
}

View file

@ -1,16 +1,18 @@
---
title: Server Configuration with fastmcp.json
sidebarTitle: Server Configuration
description: Use fastmcp.json for declarative server configuration
title: "Project Configuration"
sidebarTitle: "Project Configuration"
description: Use fastmcp.json for portable, declarative project configuration
icon: file-code
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="2.11.4" />
<VersionBadge version="2.12.0" />
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.
The `fastmcp.json` file is designed to be a portable description of your server configuration that can be shared across environments and teams. When running from a `fastmcp.json` file, you can override any configuration values using CLI 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.
@ -27,16 +29,45 @@ 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
## File Structure
The `fastmcp.json` configuration answers three fundamental questions about your server:
- **Source** = WHERE does your server code live?
- **Environment** = WHAT environment setup does it require?
- **Deployment** = HOW should the server run?
This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
// WHERE: Location of your server code
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
// WHAT: Python environment and dependencies
},
"deployment": {
// HOW: Runtime configuration
}
}
```
Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
### 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/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"source": {
"path": "server.py",
"entrypoint": "mcp"
}
}
```
@ -47,69 +78,61 @@ Two schema URLs are available:
Modern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.
## File Structure
### Source Configuration
The `fastmcp.json` file has three main sections, each controlling a different aspect of your server:
The source configuration determines **WHERE** your server code lives. It tells FastMCP how to find and load your server, whether it's a local Python file, a remote repository, or hosted in the cloud. This section is required and forms the foundation of your configuration.
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/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:
<Card icon="code" title="Source">
<ParamField body="source" type="object" required>
The server source configuration that determines where your server code lives.
**Object format** (recommended): Explicit file and object specification
```json
"entrypoint": {
"file": "src/server.py",
"object": "mcp"
}
```
<ParamField body="type" type="string" default="filesystem">
The source type identifier that determines which implementation to use. Currently supports `"filesystem"` for local files. Future releases will add support for `"git"` and `"cloud"` source types.
</ParamField>
**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 title="FileSystemSource">
When `type` is `"filesystem"` (or omitted), the source points to a local Python file containing your FastMCP server:
<ParamField body="path" type="string" required>
Path to the Python file containing your FastMCP server.
</ParamField>
<ParamField body="entrypoint" type="string">
Name of the server instance or factory function within the module:
- Can be a FastMCP server instance (e.g., `mcp = FastMCP("MyServer")`)
- Can be a function with no arguments that returns a FastMCP server
- If not specified, FastMCP searches for common names: `mcp`, `server`, or `app`
</ParamField>
**Example:**
```json
"source": {
"type": "filesystem",
"path": "src/server.py",
"entrypoint": "mcp"
}
```
Note: File paths are resolved relative to the configuration file's location.
</Expandable>
</ParamField>
</Card>
### Environment
<Note>
**Future Source Types**
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.
Future releases will support additional source types:
- **Git repositories** (`type: "git"`) for loading server code directly from version control
- **FastMCP Cloud** (`type: "cloud"`) for hosted servers with automatic scaling and management
</Note>
<Card icon="code" title="Environment Configuration">
### Environment Configuration
The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment using `uv`'s powerful dependency management. This section ensures your server runs with the exact Python version and dependencies it requires, creating isolated, reproducible environments across different systems.
These settings leverage standard `uv` arguments for environment creation. When any environment field is specified, FastMCP automatically creates an isolated environment before running your server. This build-time configuration happens once when the server starts, not during runtime execution.
<Card icon="code" title="Environment Fields">
<ParamField body="environment" type="object">
Optional Python environment configuration. When any field is specified, FastMCP automatically creates an isolated environment using `uv`.
@ -143,9 +166,9 @@ The environment section configures Python dependencies and version requirements.
</ParamField>
<ParamField body="editable" type="string">
Path to a package to install in editable/development mode.
Path to a package to install in editable/development mode. Useful for local development when you want changes to be reflected immediately.
```json
"editable": "./my-package"
"editable": "."
```
</ParamField>
</Expandable>
@ -157,11 +180,15 @@ When environment configuration is provided, FastMCP:
2. Installs the specified dependencies
3. Runs your server in this clean environment
### Deployment
This build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects.
The deployment section controls runtime configuration including transport protocol, networking, logging, and environment variables.
### Deployment Configuration
<Card icon="code" title="Deployment Configuration">
The deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels.
Environment variables are included in this section because they're runtime configuration that affects how your server behaves when it executes, not how its environment is built. The deployment configuration is applied every time your server starts, controlling its operational characteristics.
<Card icon="code" title="Deployment Fields">
<ParamField body="deployment" type="object">
Optional runtime configuration for the server.
@ -220,243 +247,7 @@ The deployment section controls runtime configuration including transport protoc
</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/public/schemas/fastmcp.json/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/public/schemas/fastmcp.json/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/public/schemas/fastmcp.json/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/public/schemas/fastmcp.json/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/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
},
"deployment": {
"transport": "http",
"log_level": "DEBUG"
}
}
```
**prod.fastmcp.json**:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/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
#### 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:
@ -499,6 +290,241 @@ This feature is particularly useful for:
- Building dynamic URLs and connection strings
- Creating environment-specific prefixes or suffixes
## Usage with CLI Commands
FastMCP automatically detects and uses a file specifically named `fastmcp.json` in the current directory, making server execution simple and consistent. Files with FastMCP configuration format but different names are not auto-detected and must be specified explicitly:
```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
# Skip environment setup when already in a uv environment
fastmcp run fastmcp.json --skip-env
```
### Environment Setup Control
By default, FastMCP uses `uv` to create an isolated environment based on your configuration. The `--skip-env` flag allows you to skip this automatic environment setup:
```bash
fastmcp run fastmcp.json --skip-env
```
**When to use `--skip-env`:**
- You're already in an activated virtual environment with all dependencies installed
- You're inside a Docker container with pre-installed dependencies
- You're in a uv-managed environment and want to prevent infinite recursion
- You want to test the server without environment setup for debugging purposes
This flag is particularly useful in CI/CD pipelines, Docker containers, or when you're managing the Python environment yourself.
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.
### 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
### 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/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
}
}
```
This configuration explicitly specifies the server entrypoint (`mcp`), making it clear which server instance or factory function to use. 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/public/schemas/fastmcp.json/v1.json",
// WHERE does the server live?
"source": {
"path": "src/server.py",
"entrypoint": "app"
},
// WHAT dependencies does it need?
"environment": {
"python": "3.12",
"dependencies": ["fastmcp[dev]"],
"editable": "."
},
// HOW should it run?
"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/public/schemas/fastmcp.json/v1.json",
// WHERE does the server live?
"source": {
"path": "app/main.py",
"entrypoint": "mcp_server"
},
// WHAT dependencies does it need?
"environment": {
"python": "3.11",
"requirements": "requirements/production.txt",
"project": "."
},
// HOW should it run?
"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/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "analysis_server.py",
"entrypoint": "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/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"deployment": {
"transport": "http",
"log_level": "DEBUG"
}
}
```
**prod.fastmcp.json**:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "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>
## 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:
@ -516,9 +542,9 @@ uv run --with pandas --with requests \
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -86,9 +86,9 @@ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -103,9 +103,9 @@ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -104,9 +104,9 @@ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -179,9 +179,9 @@ You can also use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "matplotlib", "seaborn"]

View file

@ -46,6 +46,7 @@ By default, this command runs the server directly in your current Python environ
| Path | `--path` | Path to bind to when using http transport (default: `/mcp/` or `/sse/` for SSE) |
| Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
| No Banner | `--no-banner` | Disable the startup banner display |
| No Environment | `--skip-env` | Skip environment setup with uv (use when already in a uv environment) |
| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |
| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |
| Project Directory | `--project` | Run the command within the given project directory |
@ -57,8 +58,8 @@ By default, this command runs the server directly in your current Python environ
The `fastmcp run` command supports the following entrypoints:
1. **[Inferred server instance](#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](#explicit-server-object)**: `server.py:custom_name` - imports and uses the specified server object
1. **[Inferred server instance](#inferred-server-instance)**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **[Explicit server entrypoint](#explicit-server-entrypoint)**: `server.py:custom_name` - imports and uses the specified server entrypoint
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. **[FastMCP configuration file](#fastmcp-configuration)**: `fastmcp.json` - runs servers using FastMCP's declarative configuration format (auto-detects files in current directory)
@ -68,7 +69,7 @@ The `fastmcp run` command supports the following entrypoints:
Note: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == "__main__"` block. This means:
- Any setup code in `__main__` will NOT run
- Server configuration in `__main__` is bypassed
- `fastmcp run` finds your server object/factory and runs it with its own transport settings
- `fastmcp run` finds your server entrypoint/factory and runs it with its own transport settings
If you need setup code to run, use the **factory pattern** instead.
</Warning>
@ -91,9 +92,9 @@ You can run it with:
fastmcp run server.py
```
#### Explicit Server Object
#### Explicit Server Entrypoint
If your server is stored as a variable with a custom name, or you want to be explicit about which server to run, you can use the following syntax to load a specific server object:
If your server is stored as a variable with a custom name, or you want to be explicit about which server to run, you can use the following syntax to load a specific server entrypoint:
```bash
fastmcp run server.py:custom_name
@ -122,7 +123,7 @@ fastmcp run server.py:custom_name
Since `fastmcp run` ignores the `if __name__ == "__main__"` block, you can use a factory function to run setup code before your server starts. Factory functions are called without any arguments and must return a FastMCP server instance. Both sync and async factory functions are supported.
The syntax for using a factory function is the same as for an explicit server object: `fastmcp run server.py:factory_fn`. FastMCP will automatically detect that you have identified a function rather than a server Instance
The syntax for using a factory function is the same as for an explicit server entrypoint: `fastmcp run server.py:factory_fn`. FastMCP will automatically detect that you have identified a function rather than a server Instance
For example, if you have a file called `server.py` with the following content:
@ -177,8 +178,19 @@ The configuration file handles dependencies, environment variables, and transpor
```bash
# Override port from config file
fastmcp run fastmcp.json --port 8080
# Skip environment setup when already in a uv environment
fastmcp run fastmcp.json --skip-env
```
<Note>
The `--skip-env` flag is useful when:
- You're already in an activated virtual environment
- You're inside a Docker container with pre-installed dependencies
- You're in a uv-managed environment (prevents infinite recursion)
- You want to test the server without environment setup
</Note>
See [Server Configuration](/deployment/server-configuration) for detailed documentation on fastmcp.json.
#### MCP Configuration
@ -244,8 +256,8 @@ This command does not support HTTP testing. To test a server over Streamable HTT
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
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint
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)
@ -323,8 +335,8 @@ Note that for security reasons, MCP clients usually run every server in a comple
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
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint
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
@ -339,7 +351,7 @@ The `install` command **only supports local files and fastmcp.json** - no URLs,
**Examples**
```bash
# Auto-detects server object (looks for 'mcp', 'server', or 'app')
# Auto-detects server entrypoint (looks for 'mcp', 'server', or 'app')
fastmcp install claude-desktop server.py
# Install with fastmcp.json configuration (auto-detects)
@ -348,7 +360,7 @@ fastmcp install claude-desktop
# Install with explicit fastmcp.json file
fastmcp install claude-desktop my-config.fastmcp.json
# Uses specific server object
# Uses specific server entrypoint
fastmcp install claude-desktop server.py:my_server
# With custom name and dependencies
@ -439,8 +451,8 @@ fastmcp inspect server.py
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
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint
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
@ -451,10 +463,10 @@ The `inspect` command **only supports local files and fastmcp.json** - no URLs,
**Examples**
```bash
# Auto-detect server object
# Auto-detect server entrypoint
fastmcp inspect server.py
# Specify server object
# Specify server entrypoint
fastmcp inspect server.py:my_server
# Custom output location

View file

@ -1,12 +1,16 @@
{
"$defs": {
"DeploymentConfig": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
"anyOf": [
{
"enum": ["stdio", "http", "sse"],
"enum": [
"stdio",
"http",
"sse"
],
"type": "string"
},
{
@ -28,7 +32,11 @@
],
"default": null,
"description": "Host to bind to when using HTTP transport",
"examples": ["127.0.0.1", "0.0.0.0", "localhost"],
"examples": [
"127.0.0.1",
"0.0.0.0",
"localhost"
],
"title": "Host"
},
"port": {
@ -42,7 +50,11 @@
],
"default": null,
"description": "Port to bind to when using HTTP transport",
"examples": [8000, 3000, 5000],
"examples": [
8000,
3000,
5000
],
"title": "Port"
},
"path": {
@ -56,13 +68,23 @@
],
"default": null,
"description": "URL path for the server endpoint",
"examples": ["/mcp/", "/api/mcp/", "/sse/"],
"examples": [
"/mcp/",
"/api/mcp/",
"/sse/"
],
"title": "Path"
},
"log_level": {
"anyOf": [
{
"enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
"enum": [
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL"
],
"type": "string"
},
{
@ -84,7 +106,11 @@
],
"default": null,
"description": "Working directory for the server process",
"examples": [".", "./src", "/app"],
"examples": [
".",
"./src",
"/app"
],
"title": "Cwd"
},
"env": {
@ -123,56 +149,20 @@
],
"default": null,
"description": "Arguments to pass to the server (after --)",
"examples": [["--config", "config.json", "--debug"]],
"examples": [
[
"--config",
"config.json",
"--debug"
]
],
"title": "Args"
}
},
"title": "DeploymentConfig",
"title": "Deployment",
"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": {
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
@ -186,7 +176,11 @@
],
"default": null,
"description": "Python version constraint",
"examples": ["3.10", "3.11", "3.12"],
"examples": [
"3.10",
"3.11",
"3.12"
],
"title": "Python"
},
"dependencies": {
@ -203,7 +197,13 @@
],
"default": null,
"description": "Python packages to install with PEP 508 specifiers",
"examples": [["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
"examples": [
[
"fastmcp>=2.0,<3",
"httpx",
"pandas>=2.0"
]
],
"title": "Dependencies"
},
"requirements": {
@ -217,7 +217,10 @@
],
"default": null,
"description": "Path to requirements.txt file",
"examples": ["requirements.txt", "../requirements/prod.txt"],
"examples": [
"requirements.txt",
"../requirements/prod.txt"
],
"title": "Requirements"
},
"project": {
@ -231,7 +234,10 @@
],
"default": null,
"description": "Path to project directory containing pyproject.toml",
"examples": [".", "../my-project"],
"examples": [
".",
"../my-project"
],
"title": "Project"
},
"editable": {
@ -245,11 +251,49 @@
],
"default": null,
"description": "Directory to install in editable mode",
"examples": [".", "../my-package"],
"examples": [
".",
"../my-package"
],
"title": "Editable"
}
},
"title": "EnvironmentConfig",
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
@ -268,28 +312,36 @@
"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",
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
"server.py",
"server.py:app",
{
"file": "src/server.py",
"object": "app"
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/EnvironmentConfig",
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/DeploymentConfig",
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": ["entrypoint"],
"required": [
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"

View file

@ -1,12 +1,16 @@
{
"$defs": {
"DeploymentConfig": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
"anyOf": [
{
"enum": ["stdio", "http", "sse"],
"enum": [
"stdio",
"http",
"sse"
],
"type": "string"
},
{
@ -28,7 +32,11 @@
],
"default": null,
"description": "Host to bind to when using HTTP transport",
"examples": ["127.0.0.1", "0.0.0.0", "localhost"],
"examples": [
"127.0.0.1",
"0.0.0.0",
"localhost"
],
"title": "Host"
},
"port": {
@ -42,7 +50,11 @@
],
"default": null,
"description": "Port to bind to when using HTTP transport",
"examples": [8000, 3000, 5000],
"examples": [
8000,
3000,
5000
],
"title": "Port"
},
"path": {
@ -56,13 +68,23 @@
],
"default": null,
"description": "URL path for the server endpoint",
"examples": ["/mcp/", "/api/mcp/", "/sse/"],
"examples": [
"/mcp/",
"/api/mcp/",
"/sse/"
],
"title": "Path"
},
"log_level": {
"anyOf": [
{
"enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
"enum": [
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL"
],
"type": "string"
},
{
@ -84,7 +106,11 @@
],
"default": null,
"description": "Working directory for the server process",
"examples": [".", "./src", "/app"],
"examples": [
".",
"./src",
"/app"
],
"title": "Cwd"
},
"env": {
@ -123,56 +149,20 @@
],
"default": null,
"description": "Arguments to pass to the server (after --)",
"examples": [["--config", "config.json", "--debug"]],
"examples": [
[
"--config",
"config.json",
"--debug"
]
],
"title": "Args"
}
},
"title": "DeploymentConfig",
"title": "Deployment",
"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": {
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
@ -186,7 +176,11 @@
],
"default": null,
"description": "Python version constraint",
"examples": ["3.10", "3.11", "3.12"],
"examples": [
"3.10",
"3.11",
"3.12"
],
"title": "Python"
},
"dependencies": {
@ -203,7 +197,13 @@
],
"default": null,
"description": "Python packages to install with PEP 508 specifiers",
"examples": [["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
"examples": [
[
"fastmcp>=2.0,<3",
"httpx",
"pandas>=2.0"
]
],
"title": "Dependencies"
},
"requirements": {
@ -217,7 +217,10 @@
],
"default": null,
"description": "Path to requirements.txt file",
"examples": ["requirements.txt", "../requirements/prod.txt"],
"examples": [
"requirements.txt",
"../requirements/prod.txt"
],
"title": "Requirements"
},
"project": {
@ -231,7 +234,10 @@
],
"default": null,
"description": "Path to project directory containing pyproject.toml",
"examples": [".", "../my-project"],
"examples": [
".",
"../my-project"
],
"title": "Project"
},
"editable": {
@ -245,11 +251,49 @@
],
"default": null,
"description": "Directory to install in editable mode",
"examples": [".", "../my-package"],
"examples": [
".",
"../my-package"
],
"title": "Editable"
}
},
"title": "EnvironmentConfig",
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
@ -268,28 +312,36 @@
"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",
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
"server.py",
"server.py:app",
{
"file": "src/server.py",
"object": "app"
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/EnvironmentConfig",
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/DeploymentConfig",
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": ["entrypoint"],
"required": [
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"

View file

@ -1,6 +1,8 @@
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "src/atproto_mcp/server.py",
"source": {
"path": "src/atproto_mcp/server.py"
},
"environment": {
"dependencies": [
"atproto_mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp"

View file

@ -1,6 +1,8 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp/v1.json",
"entrypoint": "server.py",
"source": {
"path": "server.py"
},
"environment": {
"python": "3.12",
"dependencies": ["requests"]

View file

@ -1,8 +1,8 @@
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"python": "3.12",

View file

@ -1,6 +1,8 @@
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "server.py",
"source": {
"path": "server.py"
},
"environment": {
"python": "3.11",
"dependencies": ["pyautogui", "Pillow"]

View file

@ -6,7 +6,7 @@ import sys
from pathlib import Path
from typing import Any
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
@ -90,11 +90,20 @@ def update_claude_config(
else:
env_vars = existing_env
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Build uv run command using Environment.build_uv_args()
env_config = Environment(
dependencies=deduplicated_packages,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Convert file path to absolute before adding to command
# Split off any :object suffix first

View file

@ -2,6 +2,7 @@
import importlib.metadata
import importlib.util
import json
import os
import platform
import subprocess
@ -11,7 +12,7 @@ from typing import Annotated, Literal
import cyclopts
import pyperclip
from pydantic import TypeAdapter
from pydantic import TypeAdapter, ValidationError
from rich.console import Console
from rich.table import Table
@ -19,9 +20,9 @@ import fastmcp
from fastmcp.cli import run as run_module
from fastmcp.cli.install import install_app
from fastmcp.server.server import FastMCP
from fastmcp.utilities.cli import build_uv_command
from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import get_cached_typeadapter
logger = get_logger("cli")
console = Console()
@ -58,10 +59,6 @@ def _parse_env_var(env_var: str) -> tuple[str, str]:
return key.strip(), value.strip()
# The _build_uv_command function has been moved to cli/utils.py
# and is now imported as build_uv_command
@app.command
def version(
*,
@ -198,18 +195,27 @@ async def dev(
# 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 = python or config.environment.python
project = project or (
Path(config.environment.project) if config.environment.project else None
)
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"]
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
with_editable = with_editable or (
Path(config.environment.editable)
if config.environment.editable
else None
)
# Merge packages from both sources
if config.environment.dependencies:
packages = list(config.environment.dependencies)
if with_packages:
packages.extend(with_packages)
with_packages = packages
# Get server port from deployment config if not specified
if config.deployment and config.deployment.port:
@ -265,14 +271,17 @@ async def dev(
if inspector_version:
inspector_cmd += f"@{inspector_version}"
uv_cmd = build_uv_command(
server_spec,
with_editable=with_editable,
with_packages=with_packages,
python_version=python,
with_requirements=with_requirements,
project=project,
# Create Environment object from CLI args
from fastmcp.utilities.fastmcp_config import Environment
env_config = Environment(
python=python,
dependencies=with_packages if with_packages else None,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
uv_cmd = ["uv"] + env_config.build_uv_args(["fastmcp", "run", server_spec])
# Add --no-banner flag for dev command
uv_cmd.append("--no-banner")
@ -382,6 +391,14 @@ async def run(
help="Requirements file to install dependencies from",
),
] = None,
skip_env: Annotated[
bool,
cyclopts.Parameter(
"--skip-env",
help="Skip environment setup with uv (use when already in a uv environment)",
negative="",
),
] = False,
) -> None:
"""Run an MCP server or connect to a remote one.
@ -406,6 +423,7 @@ async def run(
config = None
config_path = None
editable = None # Initialize editable variable
# Auto-detect fastmcp.json if no server_spec provided
if server_spec is None:
@ -425,41 +443,69 @@ async def run(
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"):
# Load config if server_spec is a .json file
if server_spec.endswith(".json"):
config_path = Path(server_spec)
if config_path.exists():
config = FastMCPConfig.from_file(config_path)
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
try:
with open(config_path) as f:
data = json.load(f)
# 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 ()
# Check if it's an MCPConfig first (has canonical mcpServers key)
if "mcpServers" in data:
# It's an MCPConfig, we don't process these in the run command
# They should be handled through different code paths
config = None
else:
# Try to parse as FastMCPConfig
try:
adapter = get_cached_typeadapter(FastMCPConfig)
config = adapter.validate_python(data)
# 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"]
# Merge deployment config with CLI values (CLI takes precedence)
if config.deployment:
transport = transport or config.deployment.transport
host = host or config.deployment.host
port = port or config.deployment.port
path = path or config.deployment.path
log_level = log_level or config.deployment.log_level
server_args = (
tuple(server_args)
if server_args
else tuple(config.deployment.args or ())
)
# Merge environment config with CLI values (CLI takes precedence)
if config.environment:
python = python or config.environment.python
project = project or (
Path(config.environment.project)
if config.environment.project
else None
)
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
# Extract editable from config (no CLI override for this)
editable = config.environment.editable
# Merge packages from both sources
if config.environment.dependencies:
packages = list(config.environment.dependencies)
if with_packages:
packages.extend(with_packages)
with_packages = packages
except ValidationError:
# Not a valid FastMCPConfig, treat as regular server spec
config = None
except (json.JSONDecodeError, FileNotFoundError):
# Not a valid JSON file, treat as regular server spec
config = None
else:
config = None
logger.debug(
"Running server or client",
extra={
@ -474,8 +520,11 @@ async def run(
)
# 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:
# Skip if --skip-env flag is set (we're already in a uv environment)
needs_uv = not skip_env and (
python or with_packages or with_requirements or project or editable
)
if not skip_env and not needs_uv and config and config.environment:
# Check if config's environment needs uv
needs_uv = config.environment.needs_uv()
@ -494,6 +543,7 @@ async def run(
path=path,
log_level=log_level,
show_banner=not no_banner,
editable=editable,
)
except Exception as e:
logger.error(
@ -612,31 +662,64 @@ async def inspect(
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"):
# Load config if server_spec is a .json file
if server_spec.endswith(".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)
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
try:
with open(config_path) as f:
data = json.load(f)
if entrypoint.object:
server_spec = f"{entrypoint.file}:{entrypoint.object}"
else:
server_spec = entrypoint.file
# Check which type of config it is based on required fields
try:
if "source" in data:
# It's a FastMCPConfig - validate and use it
adapter = get_cached_typeadapter(FastMCPConfig)
config = adapter.validate_python(data)
# Get the actual entrypoint with resolved paths
entrypoint = config.get_entrypoint(config_path)
# 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"]
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:
python = python or config.environment.python
project = project or (
Path(config.environment.project)
if config.environment.project
else None
)
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
# Merge packages from both sources
if config.environment.dependencies:
packages = list(config.environment.dependencies)
if with_packages:
packages.extend(with_packages)
with_packages = packages
elif "mcpServers" in data:
# It's an MCPConfig, we don't process these in the run command
# They should be handled through different code paths
config = None
else:
# Not a recognized config format, treat as regular server spec
config = None
except ValidationError:
# Not a valid config, treat as regular server spec
config = None
except (json.JSONDecodeError, FileNotFoundError):
# Not a valid JSON file, treat as regular server spec
config = None
else:
config = None
# Check if we need to use uv run
needs_uv = python or with_packages or with_requirements or project
@ -656,12 +739,12 @@ async def inspect(
]
config.environment.run_with_uv(inspect_command)
else:
# Build an EnvironmentConfig from CLI args for consistency
# Build an Environment from CLI args for consistency
from fastmcp.utilities.fastmcp_config import (
EnvironmentConfig,
Environment,
)
env_config = EnvironmentConfig(
env_config = Environment(
python=python,
dependencies=with_packages,
requirements=str(with_requirements) if with_requirements else None,

View file

@ -9,7 +9,7 @@ from typing import Annotated
import cyclopts
from rich import print
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -107,14 +107,23 @@ def install_claude_code(
)
return False
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Build uv run command using Environment.build_uv_args()
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:

View file

@ -9,7 +9,7 @@ import cyclopts
from rich import print
from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -73,14 +73,22 @@ def install_claude_desktop(
config_file = config_dir / "claude_desktop_config.json"
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:

View file

@ -10,7 +10,7 @@ import cyclopts
from rich import print
from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -107,14 +107,22 @@ def install_cursor_workspace(
config_file = cursor_dir / "mcp.json"
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=str(with_editable.resolve()) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:
@ -178,14 +186,23 @@ def install_cursor(
Returns:
True if installation was successful, False otherwise
"""
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=str(with_editable.resolve()) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:

View file

@ -9,7 +9,7 @@ import cyclopts
import pyperclip
from rich import print
from fastmcp.utilities.cli import build_uv_run_args
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -48,14 +48,22 @@ def install_mcp_json(
True if generation was successful, False otherwise
"""
try:
# Build uv run command using centralized function
args = build_uv_run_args(
with_editable=with_editable,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:

View file

@ -1,13 +1,16 @@
"""Shared utilities for install commands."""
import json
import sys
from pathlib import Path
from dotenv import dotenv_values
from pydantic import ValidationError
from rich import print
from fastmcp.cli.run import import_server, parse_file_path
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import get_cached_typeadapter
logger = get_logger(__name__)
@ -34,28 +37,46 @@ async def process_common_args(
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
# Check if server_spec is a .json file
if server_spec.endswith(".json"):
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)
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
try:
with open(config_path) as f:
data = json.load(f)
# Convert to file and server_object
file = Path(entrypoint.file)
server_object = entrypoint.object
# Check if it's an MCPConfig first (has canonical mcpServers key)
from fastmcp.utilities.fastmcp_config import FastMCPConfig
# 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))
if "mcpServers" in data:
# It's an MCPConfig, treat as regular server spec
file, server_object = parse_file_path(server_spec)
else:
# Try to parse as FastMCPConfig
try:
adapter = get_cached_typeadapter(FastMCPConfig)
config = adapter.validate_python(data)
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))
except ValidationError:
# Not a valid FastMCPConfig, treat as regular server spec
file, server_object = parse_file_path(server_spec)
except (json.JSONDecodeError, FileNotFoundError):
# Not a valid JSON file, treat as regular server spec
file, server_object = parse_file_path(server_spec)
else:
# Parse traditional server spec
file, server_object = parse_file_path(server_spec)

View file

@ -11,16 +11,16 @@ from pathlib import Path
from typing import Any, Literal
from mcp.server.fastmcp import FastMCP as FastMCP1x
from pydantic import ValidationError
from fastmcp.server.server import FastMCP
from fastmcp.utilities.cli import build_uv_command
from fastmcp.utilities.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
Environment,
FastMCPConfig,
FileSystemSource,
)
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import get_cached_typeadapter
logger = get_logger("cli.run")
@ -102,7 +102,7 @@ async def import_server(file: Path, server_or_factory: str | None = None) -> Any
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 in fastmcp.json or use `file.py:object` syntax as your path.",
"2. Specify the entrypoint name in fastmcp.json or use `file.py:object` syntax as your path.",
extra={"file": str(file)},
)
sys.exit(1)
@ -191,6 +191,7 @@ def run_with_uv(
path: str | None = None,
log_level: LogLevelType | None = None,
show_banner: bool = True,
editable: str | None = None,
) -> None:
"""Run a MCP server using uv run subprocess.
@ -207,62 +208,82 @@ 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:
# Check if server_spec is a .json file
if server_spec.endswith(".json"):
config_path = Path(server_spec).resolve() # Get absolute path
if config_path.exists():
# Load config
config = FastMCPConfig.from_file(config_path)
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
try:
with open(config_path) as f:
data = json.load(f)
# 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
# Check if it's an MCPConfig first (has canonical mcpServers key)
if "mcpServers" in data:
# It's an MCPConfig, we don't process it here - just pass through
pass
else:
# Try to parse as FastMCPConfig
try:
adapter = get_cached_typeadapter(FastMCPConfig)
config: FastMCPConfig = adapter.validate_python(data)
# 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"]
# Apply deployment settings
if config.deployment:
config.deployment.apply_runtime_settings(config_path)
# 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"]
# Build uv command using centralized function
cmd = build_uv_command(
server_spec,
with_packages=with_packages,
python_version=python_version,
with_requirements=with_requirements,
project=project,
# Merge environment config with CLI args (CLI takes precedence)
if config.environment:
# Use CLI values if provided, otherwise fall back to config
python_version = python_version or config.environment.python
project = project or (
Path(config.environment.project)
if config.environment.project
else None
)
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
editable = editable or config.environment.editable
# Merge packages from both sources
# Only merge if with_packages doesn't already contain them
# (they may have been merged already in CLI)
if config.environment.dependencies and not with_packages:
with_packages = list(config.environment.dependencies)
# Merge deployment config with CLI args (CLI takes precedence)
if config.deployment:
transport = transport or config.deployment.transport
host = host or config.deployment.host
port = port or config.deployment.port
path = path or config.deployment.path
log_level = log_level or config.deployment.log_level
except ValidationError:
# Not a valid FastMCPConfig, just pass through
pass
except (json.JSONDecodeError, FileNotFoundError):
# Not a valid JSON file, just pass through
pass
# Build uv command using Environment.build_uv_args()
env_config = Environment(
python=python_version,
dependencies=with_packages if with_packages else None,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=editable,
)
# IMPORTANT: We add --skip-env to prevent infinite recursion.
# When this function executes `uv run ... fastmcp run server.py`, the inner
# `fastmcp run` command will be executed inside the uv environment we're creating.
# Without --skip-env, that inner command would detect it needs uv (due to the same
# CLI args) and try to spawn ANOTHER uv subprocess, creating infinite recursion.
# The --skip-env flag tells the inner fastmcp: "skip environment setup, we're already
# inside the uv environment that was just created for us."
cmd = ["uv"] + env_config.build_uv_args(
["fastmcp", "run", server_spec, "--skip-env"]
)
# Add transport options
@ -320,16 +341,14 @@ 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]:
def load_fastmcp_config(config_path: Path) -> FastMCPConfig:
"""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)
FastMCPConfig object
"""
config = FastMCPConfig.from_file(config_path)
@ -337,29 +356,7 @@ def load_fastmcp_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
return config
async def import_server_with_args(
@ -416,44 +413,56 @@ 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))
# Load JSON and check which type of config it is
config_path = Path(server_spec)
with open(config_path) as f:
data = json.load(f)
# Check if it's an MCPConfig first (has canonical mcpServers key)
if "mcpServers" in data:
# It's an MCP config
server = create_mcp_config_server(config_path)
else:
# Try to parse as FastMCPConfig
adapter = get_cached_typeadapter(FastMCPConfig)
adapter.validate_python(data) # Validate but don't need to store
# It's a FastMCP config - load it properly with runtime settings
config = load_fastmcp_config(config_path)
# Merge deployment config with CLI arguments (CLI takes precedence)
if config.deployment:
transport = transport or config.deployment.transport
host = host or config.deployment.host
port = port or config.deployment.port
path = path or config.deployment.path
log_level = log_level or config.deployment.log_level
server_args = (
server_args if server_args is not None else config.deployment.args
)
# Load the server using the source
server = await config.source.load_server(config_path, server_args)
logger.debug(f'Found server "{server.name}" from config {config_path}')
else:
# Handle file case
file, server_or_factory = parse_file_path(server_spec)
server = await import_server_with_args(file, server_or_factory, server_args)
logger.debug(f'Found server "{server.name}" in {file}')
# Handle file case - parse into FileSystemSource immediately
if ":" in server_spec:
# Check if it's a Windows path (e.g., C:\...)
has_windows_drive = len(server_spec) > 1 and server_spec[1] == ":"
# Only split if colon is not part of Windows drive
if ":" in (server_spec[2:] if has_windows_drive else server_spec):
file_str, obj = server_spec.rsplit(":", 1)
source = FileSystemSource(path=file_str, object=obj)
else:
source = FileSystemSource(path=server_spec)
else:
source = FileSystemSource(path=server_spec)
# Create a temporary config with just the source
config = FastMCPConfig(source=source)
server = await config.source.load_server(None, server_args)
logger.debug(f'Found server "{server.name}" in {source.path}')
# Run the server

View file

@ -36,7 +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.fastmcp_config.v1.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
@ -596,8 +596,8 @@ class UvStdioTransport(StdioTransport):
f"Project directory not found: {project_directory}"
)
# Create EnvironmentConfig from provided parameters (internal use)
env_config = EnvironmentConfig(
# Create Environment from provided parameters (internal use)
env_config = Environment(
python=python_version,
dependencies=with_packages,
requirements=with_requirements,

View file

@ -1,7 +1,6 @@
from __future__ import annotations
from importlib.metadata import version
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from rich.align import Align
@ -110,119 +109,3 @@ def log_server_banner(
console = Console(stderr=True)
console.print(Group("\n", panel, "\n"))
def build_uv_command(
server_spec: str,
*,
with_editable: Path | None = None,
with_packages: list[str] | None = None,
python_version: str | None = None,
with_requirements: Path | None = None,
project: Path | None = None,
) -> list[str]:
"""Build a uv run command for running a FastMCP server.
This centralized function ensures consistent path resolution and command building
across all CLI commands.
Args:
server_spec: Server specification (file path, optionally with :object)
with_editable: Directory to install in editable mode
with_packages: Additional packages to install
python_version: Python version to use (e.g., "3.10", "3.11")
with_requirements: Requirements file to install from
project: Project directory to run within
Returns:
List of command arguments for subprocess execution
"""
cmd = ["uv", "run"]
# Add Python version if specified
if python_version:
cmd.extend(["--python", python_version])
# Add project if specified - resolve to absolute path
if project:
cmd.extend(["--project", str(project.expanduser().resolve())])
# Always include fastmcp
cmd.extend(["--with", "fastmcp"])
# Add additional packages
if with_packages:
# Deduplicate and sort packages for consistency
packages = set(pkg for pkg in with_packages if pkg)
for pkg in sorted(packages):
cmd.extend(["--with", pkg])
# Add editable directory - resolve to absolute path
if with_editable:
cmd.extend(["--with-editable", str(with_editable.expanduser().resolve())])
# Add requirements file - resolve to absolute path
if with_requirements:
cmd.extend(
["--with-requirements", str(with_requirements.expanduser().resolve())]
)
# Add fastmcp run command
cmd.extend(["fastmcp", "run", server_spec])
return cmd
def build_uv_run_args(
*,
with_editable: Path | None = None,
with_packages: list[str] | None = None,
python_version: str | None = None,
with_requirements: Path | None = None,
project: Path | None = None,
) -> list[str]:
"""Build just the uv run arguments without the server spec.
This is useful for install commands that need to build the args array
without the full command structure.
Args:
with_editable: Directory to install in editable mode
with_packages: Additional packages to install (fastmcp will be added automatically)
python_version: Python version to use
with_requirements: Requirements file to install from
project: Project directory to run within
Returns:
List of arguments starting with "run"
"""
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified - resolve to absolute path
if project:
args.extend(["--project", str(project.expanduser().resolve())])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
# Add editable directory - resolve to absolute path
if with_editable:
args.extend(["--with-editable", str(with_editable.expanduser().resolve())])
# Add requirements file - resolve to absolute path
if with_requirements:
args.extend(
["--with-requirements", str(with_requirements.expanduser().resolve())]
)
return args

View file

@ -5,17 +5,19 @@ The current version is v1, which is re-exported here for convenience.
"""
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
BaseSource,
Deployment,
Environment,
FastMCPConfig,
FileSystemSource,
generate_schema,
)
__all__ = [
"BaseSource",
"Deployment",
"Environment",
"FastMCPConfig",
"EntrypointConfig",
"EnvironmentConfig",
"DeploymentConfig",
"FileSystemSource",
"generate_schema",
]

View file

@ -10,6 +10,7 @@ from __future__ import annotations
import json
import os
import re
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, overload
@ -23,28 +24,61 @@ logger = get_logger("cli.config")
FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
class EntrypointConfig(BaseModel):
"""Configuration for server entrypoint when using object format."""
class BaseSource(BaseModel, ABC):
"""Abstract base class for all source types."""
file: str = Field(
description="Path to Python file containing the server",
examples=["server.py", "src/server.py", "app/main.py"],
)
type: str = Field(description="Source type identifier")
object: str | None = Field(
async def prepare(self, config_path: Path | None = None) -> Path | None:
"""Prepare the source (download, clone, install, etc).
Returns:
Path to prepared source directory, or None if no preparation needed.
This path may contain a nested fastmcp.json for configuration chaining.
"""
# Default implementation for sources that don't need preparation
return None
@abstractmethod
async def load_server(
self, config_path: Path | None = None, server_args: list[str] | None = None
) -> Any:
"""Load and return the FastMCP server instance.
Must be called after prepare() if the source requires preparation.
"""
...
class FileSystemSource(BaseSource):
"""Source for local Python files."""
type: Literal["filesystem"] = Field(default="filesystem", description="Source type")
path: str = Field(description="Path to Python file containing the server")
entrypoint: 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"],
description="Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
)
repo: str | None = Field(
default=None,
description="Git repository URL",
examples=["https://github.com/user/repo"],
)
async def load_server(
self, config_path: Path | None = None, server_args: list[str] | None = None
) -> Any:
"""Load server from filesystem."""
from fastmcp.cli.run import import_server_with_args
# Resolve relative paths if config_path provided
file_path = Path(self.path)
if not file_path.is_absolute() and config_path:
file_path = (config_path.parent / file_path).resolve()
return await import_server_with_args(file_path, self.entrypoint, server_args)
class EnvironmentConfig(BaseModel):
# Type alias for source union (will expand with GitSource, etc in future)
SourceType = FileSystemSource
class Environment(BaseModel):
"""Configuration for Python environment setup."""
python: str | None = Field(
@ -99,10 +133,11 @@ class EnvironmentConfig(BaseModel):
# Add fastmcp as a base dependency
args.extend(["--with", "fastmcp"])
# Add additional dependencies
# Add additional dependencies (skip fastmcp if already added)
if self.dependencies:
for dep in self.dependencies:
args.extend(["--with", dep])
if dep != "fastmcp": # Skip fastmcp since we already added it
args.extend(["--with", dep])
# Add requirements file
if self.requirements:
@ -150,51 +185,18 @@ class EnvironmentConfig(BaseModel):
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
return any(
[
self.python is not None,
self.dependencies is not None,
self.requirements is not None,
self.project is not None,
self.editable is not None,
]
)
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):
class Deployment(BaseModel):
"""Configuration for server deployment and runtime settings."""
transport: Literal["stdio", "http", "sse"] | None = Field(
@ -243,29 +245,6 @@ class DeploymentConfig(BaseModel):
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.
@ -329,144 +308,94 @@ class FastMCPConfig(BaseModel):
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",
# Server source - defines where and how to load the server
source: SourceType = Field(
description="Source configuration for the server",
examples=[
"server.py",
"server.py:app",
{"file": "src/server.py", "object": "app"},
{"path": "server.py"},
{"path": "server.py", "entrypoint": "app"},
{"type": "filesystem", "path": "src/server.py", "entrypoint": "mcp"},
],
)
# Environment configuration
environment: EnvironmentConfig = Field(
default_factory=lambda: EnvironmentConfig(),
environment: Environment = Field(
default_factory=lambda: Environment(),
description="Python environment setup configuration",
)
# Deployment configuration
deployment: DeploymentConfig = Field(
default_factory=lambda: DeploymentConfig(),
deployment: Deployment = Field(
default_factory=lambda: Deployment(),
description="Server deployment and runtime settings",
)
# purely for static type checkers to avoid issues with providng str entrypoint
# purely for static type checkers to avoid issues with providing dict source
if TYPE_CHECKING:
@overload
def __init__(
self, *, entrypoint: str | dict | EntrypointConfig, **data
) -> None: ...
def __init__(self, *, source: dict | FileSystemSource, **data) -> None: ...
@overload
def __init__(
self, *, environment: dict | EnvironmentConfig, **data
) -> None: ...
def __init__(self, *, environment: dict | Environment, **data) -> None: ...
@overload
def __init__(self, *, deployment: dict | DeploymentConfig, **data) -> None: ...
def __init__(self, *, deployment: dict | Deployment, **data) -> None: ...
def __init__(self, **data) -> None: ...
@field_validator("entrypoint", mode="before")
@field_validator("source", mode="before")
@classmethod
def validate_entrypoint(cls, v: str | EntrypointConfig) -> EntrypointConfig:
"""Validate and convert entrypoint to proper format.
def validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource:
"""Validate and convert source to proper format.
Supports:
- String format: "server.py" or "server.py:object"
- Object format: {"file": "server.py", "object": "app"}
- EntrypointConfig instance (passed through)
- Dict format: {"path": "server.py", "entrypoint": "app"}
- FileSystemSource instance (passed through)
The string format with :object syntax is automatically parsed into
the object format for consistency.
No string parsing happens here - that's only at CLI boundaries.
FastMCPConfig works only with properly typed objects.
"""
if isinstance(v, EntrypointConfig):
# Already an EntrypointConfig instance, return as-is
if isinstance(v, FileSystemSource):
# Already a FileSystemSource 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")
# Dict can have type field or not (filesystem is default)
if "type" not in v:
v["type"] = "filesystem"
return FileSystemSource(**v)
else:
raise ValueError("source must be a dict or FileSystemSource instance")
@field_validator("environment", mode="before")
@classmethod
def validate_environment(cls, v: dict | EnvironmentConfig) -> EnvironmentConfig:
"""Validate and convert environment to EnvironmentConfig.
def validate_environment(cls, v: dict | Environment) -> Environment:
"""Validate and convert environment to Environment.
Accepts:
- EnvironmentConfig instance
- dict that can be converted to EnvironmentConfig
- Environment instance
- dict that can be converted to Environment
"""
if isinstance(v, EnvironmentConfig):
if isinstance(v, Environment):
return v
elif isinstance(v, dict):
return EnvironmentConfig(**v) # type: ignore[arg-type]
return Environment(**v) # type: ignore[arg-type]
else:
raise ValueError("environment must be a dict, EnvironmentConfig instance")
raise ValueError("environment must be a dict, Environment instance")
@field_validator("deployment", mode="before")
@classmethod
def validate_deployment(cls, v: dict | DeploymentConfig) -> DeploymentConfig:
"""Validate and convert deployment to DeploymentConfig.
def validate_deployment(cls, v: dict | Deployment) -> Deployment:
"""Validate and convert deployment to Deployment.
Accepts:
- DeploymentConfig instance
- dict that can be converted to DeploymentConfig
- Deployment instance
- dict that can be converted to Deployment
"""
if isinstance(v, DeploymentConfig):
if isinstance(v, Deployment):
return v
elif isinstance(v, dict):
return DeploymentConfig(**v) # type: ignore[arg-type]
return Deployment(**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
raise ValueError("deployment must be a dict, Deployment instance")
@classmethod
def from_file(cls, file_path: Path) -> FastMCPConfig:
@ -494,7 +423,7 @@ class FastMCPConfig(BaseModel):
@classmethod
def from_cli_args(
cls,
entrypoint: str,
source: FileSystemSource,
transport: Literal["stdio", "http", "sse", "streamable-http"] | None = None,
host: str | None = None,
port: int | None = None,
@ -516,7 +445,7 @@ class FastMCPConfig(BaseModel):
goes through a config object.
Args:
entrypoint: Server entrypoint (file or file:object)
source: Server source (FileSystemSource instance)
transport: Transport protocol
host: Host for HTTP transport
port: Port for HTTP transport
@ -537,7 +466,7 @@ class FastMCPConfig(BaseModel):
# Build environment config if any env args provided
environment = None
if any([python, dependencies, requirements, project, editable]):
environment = EnvironmentConfig(
environment = Environment(
python=python,
dependencies=dependencies,
requirements=requirements,
@ -551,7 +480,7 @@ class FastMCPConfig(BaseModel):
# Convert streamable-http to http for backward compatibility
if transport == "streamable-http":
transport = "http" # type: ignore[assignment]
deployment = DeploymentConfig(
deployment = Deployment(
transport=transport, # type: ignore[arg-type]
host=host,
port=port,
@ -563,7 +492,7 @@ class FastMCPConfig(BaseModel):
)
return cls(
entrypoint=entrypoint,
source=source,
environment=environment,
deployment=deployment,
)
@ -588,14 +517,17 @@ class FastMCPConfig(BaseModel):
return None
async def load_server(self, config_path: Path | None = None) -> Any:
async def load_server(
self, config_path: Path | None = None, server_args: list[str] | None = None
) -> Any:
"""Load the server from the configuration.
This handles environment setup, working directory changes,
and imports the server module.
and delegates to the source's load_server method.
Args:
config_path: Path to the config file (for resolving relative paths)
server_args: Optional arguments to pass to the server
Returns:
The imported server object
@ -619,16 +551,12 @@ class FastMCPConfig(BaseModel):
cwd_path = cwd_path.resolve()
os.chdir(cwd_path)
# Get structured entrypoint with resolved paths
entrypoint = self.get_entrypoint(config_path)
# Use server_args from deployment if not provided
if server_args is None and self.deployment:
server_args = self.deployment.args
# 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)
# Delegate to the source's load_server method
return await self.source.load_server(config_path, server_args)
async def run_server(self, **kwargs: Any) -> None:
"""Load and run the server with this configuration.
@ -659,14 +587,19 @@ class FastMCPConfig(BaseModel):
await server.run_async(**run_args)
def generate_schema() -> dict[str, Any]:
def generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None:
"""Generate JSON schema for fastmcp.json files.
This is used to create the schema file that IDEs can use for
validation and auto-completion.
Args:
output_path: Optional path to write the schema to. If provided,
writes the schema and returns None. If not provided,
returns the schema as a dictionary.
Returns:
JSON schema as a dictionary
JSON schema as a dictionary if output_path is None, otherwise None
"""
schema = FastMCPConfig.model_json_schema()
@ -675,4 +608,14 @@ def generate_schema() -> dict[str, Any]:
schema["title"] = "FastMCP Configuration"
schema["description"] = "Configuration file for FastMCP servers"
if output_path:
import json
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
with open(output, "w") as f:
json.dump(schema, f, indent=2)
f.write("\n") # Add trailing newline
return None
return schema

View file

@ -1,12 +1,16 @@
{
"$defs": {
"DeploymentConfig": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
"anyOf": [
{
"enum": ["stdio", "http", "sse"],
"enum": [
"stdio",
"http",
"sse"
],
"type": "string"
},
{
@ -28,7 +32,11 @@
],
"default": null,
"description": "Host to bind to when using HTTP transport",
"examples": ["127.0.0.1", "0.0.0.0", "localhost"],
"examples": [
"127.0.0.1",
"0.0.0.0",
"localhost"
],
"title": "Host"
},
"port": {
@ -42,7 +50,11 @@
],
"default": null,
"description": "Port to bind to when using HTTP transport",
"examples": [8000, 3000, 5000],
"examples": [
8000,
3000,
5000
],
"title": "Port"
},
"path": {
@ -56,13 +68,23 @@
],
"default": null,
"description": "URL path for the server endpoint",
"examples": ["/mcp/", "/api/mcp/", "/sse/"],
"examples": [
"/mcp/",
"/api/mcp/",
"/sse/"
],
"title": "Path"
},
"log_level": {
"anyOf": [
{
"enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
"enum": [
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL"
],
"type": "string"
},
{
@ -84,7 +106,11 @@
],
"default": null,
"description": "Working directory for the server process",
"examples": [".", "./src", "/app"],
"examples": [
".",
"./src",
"/app"
],
"title": "Cwd"
},
"env": {
@ -123,56 +149,20 @@
],
"default": null,
"description": "Arguments to pass to the server (after --)",
"examples": [["--config", "config.json", "--debug"]],
"examples": [
[
"--config",
"config.json",
"--debug"
]
],
"title": "Args"
}
},
"title": "DeploymentConfig",
"title": "Deployment",
"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": {
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
@ -186,7 +176,11 @@
],
"default": null,
"description": "Python version constraint",
"examples": ["3.10", "3.11", "3.12"],
"examples": [
"3.10",
"3.11",
"3.12"
],
"title": "Python"
},
"dependencies": {
@ -203,7 +197,13 @@
],
"default": null,
"description": "Python packages to install with PEP 508 specifiers",
"examples": [["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
"examples": [
[
"fastmcp>=2.0,<3",
"httpx",
"pandas>=2.0"
]
],
"title": "Dependencies"
},
"requirements": {
@ -217,7 +217,10 @@
],
"default": null,
"description": "Path to requirements.txt file",
"examples": ["requirements.txt", "../requirements/prod.txt"],
"examples": [
"requirements.txt",
"../requirements/prod.txt"
],
"title": "Requirements"
},
"project": {
@ -231,7 +234,10 @@
],
"default": null,
"description": "Path to project directory containing pyproject.toml",
"examples": [".", "../my-project"],
"examples": [
".",
"../my-project"
],
"title": "Project"
},
"editable": {
@ -245,11 +251,49 @@
],
"default": null,
"description": "Directory to install in editable mode",
"examples": [".", "../my-package"],
"examples": [
".",
"../my-package"
],
"title": "Editable"
}
},
"title": "EnvironmentConfig",
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
@ -268,28 +312,36 @@
"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",
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
"server.py",
"server.py:app",
{
"file": "src/server.py",
"object": "app"
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/EnvironmentConfig",
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/DeploymentConfig",
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": ["entrypoint"],
"required": [
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"

17
test.fastmcp.json Normal file
View file

@ -0,0 +1,17 @@
{
"source": {
"path": "test.py",
"object": "mcp"
},
"environment": {
"python": "3.10",
"dependencies": ["fastmcp", "httpx", "pandas", "httpx23"]
},
"deployment": {
"transport": "http",
"host": "127.0.0.1",
"port": 1234,
"path": "/mcp",
"log_level": "INFO"
}
}

View file

@ -8,77 +8,52 @@ import pytest
from pydantic import ValidationError
from fastmcp.utilities.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
Deployment,
Environment,
FastMCPConfig,
FileSystemSource,
)
class TestEntrypointConfig:
"""Test EntrypointConfig class."""
class TestFileSystemSource:
"""Test FileSystemSource 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
def test_dict_source_minimal(self):
"""Test that dict source is converted to FileSystemSource."""
config = FastMCPConfig(source={"path": "server.py"})
# Dict is converted to FileSystemSource
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
assert config.source.type == "filesystem"
# 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_dict_source_with_entrypoint(self):
"""Test dict source with entrypoint field."""
config = FastMCPConfig(source={"path": "server.py", "entrypoint": "app"})
# Dict with entrypoint is converted to FileSystemSource
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint == "app"
assert config.source.type == "filesystem"
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."""
def test_filesystem_source_entrypoint(self):
"""Test FileSystemSource entrypoint format."""
config = FastMCPConfig(
entrypoint=EntrypointConfig(file="src/server.py", object="mcp")
source=FileSystemSource(path="src/server.py", entrypoint="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()
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "mcp"
assert config.source.type == "filesystem"
class TestEnvironmentConfig:
"""Test EnvironmentConfig class."""
class TestEnvironment:
"""Test Environment class."""
def test_environment_config_fields(self):
"""Test all EnvironmentConfig fields."""
"""Test all Environment fields."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["requests", "numpy>=2.0"],
@ -98,27 +73,29 @@ class TestEnvironmentConfig:
def test_needs_uv(self):
"""Test needs_uv() method."""
# No environment config - doesn't need UV
config = FastMCPConfig(entrypoint="server.py")
config = FastMCPConfig(source={"path": "server.py"})
assert not config.environment.needs_uv()
# Empty environment - doesn't need UV
config = FastMCPConfig(entrypoint="server.py", environment={})
config = FastMCPConfig(source={"path": "server.py"}, environment={})
assert not config.environment.needs_uv()
# With dependencies - needs UV
config = FastMCPConfig(
entrypoint="server.py", environment={"dependencies": ["requests"]}
source={"path": "server.py"}, environment={"dependencies": ["requests"]}
)
assert config.environment.needs_uv()
# With Python version - needs UV
config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
config = FastMCPConfig(
source={"path": "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",
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["requests", "numpy"],
@ -143,33 +120,10 @@ class TestEnvironmentConfig:
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"]}
source={"path": "server.py"}, environment={"dependencies": ["requests"]}
)
# run_with_uv calls sys.exit, so we expect SystemExit
@ -182,13 +136,13 @@ class TestEnvironmentConfig:
assert exc_info.value.code == 1
class TestDeploymentConfig:
"""Test DeploymentConfig class."""
class TestDeployment:
"""Test Deployment class."""
def test_deployment_config_fields(self):
"""Test all DeploymentConfig fields."""
"""Test all Deployment fields."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={
"transport": "http",
"host": "0.0.0.0",
@ -211,33 +165,6 @@ class TestDeploymentConfig:
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
@ -247,7 +174,7 @@ class TestDeploymentConfig:
work_dir.mkdir()
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={
"env": {"TEST_VAR": "test_value"},
"cwd": "work",
@ -283,7 +210,7 @@ class TestDeploymentConfig:
os.environ["ENV_NAME"] = "production"
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={
"env": {
"API_URL": "https://api.${BASE_URL}/v1",
@ -328,24 +255,24 @@ class TestFastMCPConfig:
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
config = FastMCPConfig(source={"path": "server.py"})
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
# Environment and deployment are now always present but empty
assert isinstance(config.environment, EnvironmentConfig)
assert isinstance(config.deployment, DeploymentConfig)
assert isinstance(config.environment, Environment)
assert isinstance(config.deployment, Deployment)
# 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
for field in Deployment.model_fields
)
def test_nested_structure(self):
"""Test the nested configuration structure."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["fastmcp"],
@ -356,17 +283,17 @@ class TestFastMCPConfig:
},
)
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)
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
assert isinstance(config.environment, Environment)
assert isinstance(config.deployment, Deployment)
def test_from_file(self, tmp_path):
"""Test loading config from JSON file with nested structure."""
config_data = {
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": {"file": "src/server.py", "object": "app"},
"source": {"path": "src/server.py", "entrypoint": "app"},
"environment": {"python": "3.12", "dependencies": ["requests"]},
"deployment": {"transport": "http", "port": 8000},
}
@ -376,19 +303,19 @@ class TestFastMCPConfig:
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"
# When loaded from JSON with entrypoint format, it becomes EntrypointConfig
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "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."""
"""Test loading config with dict source format."""
config_data = {
"entrypoint": "server.py:mcp",
"source": {"path": "server.py", "entrypoint": "mcp"},
"environment": {"dependencies": ["fastmcp"]},
}
@ -397,19 +324,14 @@ class TestFastMCPConfig:
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"
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint == "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."""
def test_string_entrypoint_with_entrypoint_and_environment(self, tmp_path):
"""Test that file.py:entrypoint syntax works with environment config."""
config_data = {
"entrypoint": "src/server.py:app",
"source": {"path": "src/server.py", "entrypoint": "app"},
"environment": {"python": "3.12", "dependencies": ["fastmcp", "requests"]},
"deployment": {"transport": "http", "port": 8000},
}
@ -420,9 +342,9 @@ class TestFastMCPConfig:
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"
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "app"
# Environment config should still work
assert config.environment.python == "3.12"
@ -435,7 +357,7 @@ class TestFastMCPConfig:
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"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
original_cwd = os.getcwd()
try:
@ -448,7 +370,7 @@ class TestFastMCPConfig:
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"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
subdir = tmp_path / "subdir"
subdir.mkdir()
@ -460,7 +382,7 @@ class TestFastMCPConfig:
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"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should find config when looking in the directory that contains it
found = FastMCPConfig.find_config(tmp_path)
@ -474,7 +396,7 @@ class TestFastMCPConfig:
def test_invalid_transport(self, tmp_path):
"""Test loading config with invalid transport value."""
config_data = {
"entrypoint": "server.py",
"source": {"path": "server.py"},
"deployment": {"transport": "invalid_transport"},
}
@ -485,29 +407,33 @@ class TestFastMCPConfig:
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"
"""Test that all config sections are optional except source."""
# Only source is required
config = FastMCPConfig(source={"path": "server.py"})
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
# Environment and deployment are now always present but may be empty
assert isinstance(config.environment, EnvironmentConfig)
assert isinstance(config.deployment, DeploymentConfig)
assert isinstance(config.environment, Environment)
assert isinstance(config.deployment, Deployment)
# Only environment with values
config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
config = FastMCPConfig(
source={"path": "server.py"}, environment={"python": "3.12"}
)
assert config.environment.python == "3.12"
assert isinstance(config.deployment, DeploymentConfig)
assert isinstance(config.deployment, Deployment)
assert all(
getattr(config.deployment, field, None) is None
for field in DeploymentConfig.model_fields
for field in Deployment.model_fields
)
# Only deployment with values
config = FastMCPConfig(entrypoint="server.py", deployment={"transport": "http"})
assert isinstance(config.environment, EnvironmentConfig)
config = FastMCPConfig(
source={"path": "server.py"}, deployment={"transport": "http"}
)
assert isinstance(config.environment, Environment)
assert all(
getattr(config.environment, field, None) is None
for field in EnvironmentConfig.model_fields
for field in Environment.model_fields
)
assert config.deployment.transport == "http"

View file

@ -239,11 +239,14 @@ class TestInstallCursor:
"""Test cursor installation with editable package."""
mock_open_deeplink.return_value = True
# Use an absolute path that works on all platforms
editable_path = Path.cwd() / "local" / "package"
result = install_cursor(
file=Path("/path/to/server.py"),
server_object="custom_app",
name="test-server",
with_editable=Path("/local/package"),
with_editable=editable_path,
)
assert result is True

View file

@ -38,7 +38,7 @@ if __name__ == "__main__":
# Create config file
config_data = {
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "server.py",
"source": {"path": "server.py"},
"environment": {
"python": sys.version.split()[0], # Use current Python version
"dependencies": ["fastmcp"],
@ -58,7 +58,7 @@ class TestConfigFileDetection:
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"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should be detected as fastmcp config
assert "fastmcp.json" in config_file.name
@ -67,7 +67,7 @@ class TestConfigFileDetection:
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"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should be detected as fastmcp config
assert "fastmcp.json" in config_file.name
@ -75,7 +75,7 @@ class TestConfigFileDetection:
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"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should be detected as fastmcp config
assert "fastmcp.json" in config_file.name
@ -91,14 +91,18 @@ class TestConfigWithClient:
config_file = server_with_config / "fastmcp.json"
config = FastMCPConfig.from_file(config_file)
# Import the server using the entrypoint
# Import the server using the source
import importlib.util
import sys
entrypoint = config.get_entrypoint(config_file)
spec = importlib.util.spec_from_file_location("test_server", entrypoint.file)
# Resolve the path from the source
source_path = Path(config.source.path)
if not source_path.is_absolute():
source_path = (config_file.parent / source_path).resolve()
spec = importlib.util.spec_from_file_location("test_server", str(source_path))
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load module from {entrypoint.file}")
raise RuntimeError(f"Could not load module from {source_path}")
module = importlib.util.module_from_spec(spec)
sys.modules["test_server"] = module
spec.loader.exec_module(module)
@ -129,7 +133,7 @@ class TestEnvironmentExecution:
def test_needs_uv_with_dependencies(self):
"""Test that environment with dependencies needs UV."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={"dependencies": ["requests", "numpy"]}, # type: ignore[arg-type]
)
@ -139,7 +143,7 @@ class TestEnvironmentExecution:
def test_needs_uv_with_python_version(self):
"""Test that environment with Python version needs UV."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={"python": "3.12"}, # type: ignore[arg-type]
)
@ -148,7 +152,7 @@ class TestEnvironmentExecution:
def test_no_uv_needed_without_environment(self):
"""Test that no UV is needed without environment config."""
config = FastMCPConfig(entrypoint="server.py")
config = FastMCPConfig(source={"path": "server.py"})
# Environment is now always present but may be empty
assert config.environment is not None
@ -157,7 +161,7 @@ class TestEnvironmentExecution:
def test_no_uv_needed_with_empty_environment(self):
"""Test that no UV is needed with empty environment config."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={}, # type: ignore[arg-type]
)
@ -165,77 +169,11 @@ class TestEnvironmentExecution:
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."""
def test_source_path_resolution(self, tmp_path):
"""Test that source paths are resolved relative to config."""
# Create nested directory structure
config_dir = tmp_path / "config"
config_dir.mkdir()
@ -246,14 +184,11 @@ class TestPathResolution:
server_file = src_dir / "server.py"
server_file.write_text("# Server")
config = FastMCPConfig(entrypoint="../src/server.py")
config = FastMCPConfig(source={"path": "../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()
# The source path is resolved during load_server
# For now, just check that the source is created correctly
assert config.source.path == "../src/server.py"
def test_cwd_path_resolution(self, tmp_path):
"""Test that working directory is resolved relative to config."""
@ -264,7 +199,7 @@ class TestPathResolution:
work_dir.mkdir()
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"cwd": "work"}, # type: ignore[arg-type]
)
@ -288,7 +223,7 @@ class TestPathResolution:
reqs_file.write_text("fastmcp>=2.0")
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={"requirements": "requirements.txt"}, # type: ignore[arg-type]
)
@ -309,7 +244,7 @@ class TestConfigValidation:
"""Test that invalid transport values are rejected."""
with pytest.raises(ValueError):
FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"transport": "invalid_transport"}, # type: ignore[arg-type]
)
@ -317,7 +252,7 @@ class TestConfigValidation:
"""Test that streamable-http transport is rejected in fastmcp.json config."""
with pytest.raises(ValueError):
FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"transport": "streamable-http"}, # type: ignore[arg-type]
)
@ -325,12 +260,12 @@ class TestConfigValidation:
"""Test that invalid log level values are rejected."""
with pytest.raises(ValueError):
FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"log_level": "INVALID"}, # type: ignore[arg-type]
)
def test_missing_entrypoint_rejected(self):
"""Test that config without entrypoint is rejected."""
def test_missing_source_rejected(self):
"""Test that config without source is rejected."""
with pytest.raises(ValueError):
FastMCPConfig() # type: ignore[call-arg]
@ -338,7 +273,7 @@ class TestConfigValidation:
"""Test that all valid transport values are accepted."""
for transport in ["stdio", "http", "sse"]:
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"transport": transport}, # type: ignore[arg-type]
)
assert config.deployment is not None
@ -348,7 +283,7 @@ class TestConfigValidation:
"""Test that all valid log levels are accepted."""
for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"log_level": level}, # type: ignore[arg-type]
)
assert config.deployment is not None

View file

@ -40,6 +40,7 @@ def test_schema_has_correct_id():
"""Test that the schema has the correct $id field."""
generated_schema = generate_schema()
assert generated_schema is not None
assert "$id" in generated_schema
assert (
generated_schema["$id"]
@ -51,19 +52,21 @@ 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 generated_schema is not None
# Check that source is required
assert "required" in generated_schema
assert "entrypoint" in generated_schema["required"]
assert "source" in generated_schema["required"]
# Check that entrypoint is in properties
# Check that source is in properties
assert "properties" in generated_schema
assert "entrypoint" in generated_schema["properties"]
assert "source" in generated_schema["properties"]
def test_schema_nested_structure():
"""Test that the schema has the correct nested structure."""
generated_schema = generate_schema()
assert generated_schema is not None
properties = generated_schema["properties"]
# Check environment section
@ -95,6 +98,7 @@ def test_schema_transport_enum():
"""Test that transport field has correct enum values."""
generated_schema = generate_schema()
assert generated_schema is not None
# Navigate to transport field
deploy_schema = generated_schema["properties"]["deployment"]
@ -129,6 +133,7 @@ def test_schema_log_level_enum():
"""Test that log_level field has correct enum values."""
generated_schema = generate_schema()
assert generated_schema is not None
# Navigate to log_level field
deploy_schema = generated_schema["properties"]["deployment"]

View file

@ -8,9 +8,10 @@ import pytest
from fastmcp.cli.run import load_fastmcp_config
from fastmcp.utilities.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
Deployment,
Environment,
FastMCPConfig,
FileSystemSource,
)
@ -19,7 +20,7 @@ def sample_config(tmp_path):
"""Create a sample fastmcp.json configuration file with nested structure."""
config_data = {
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "server.py",
"source": {"path": "server.py"},
"environment": {"python": "3.11", "dependencies": ["requests"]},
"deployment": {"transport": "stdio", "env": {"TEST_VAR": "test_value"}},
}
@ -49,25 +50,25 @@ def test_load_fastmcp_config(sample_config, monkeypatch):
original_env = dict(os.environ)
try:
entrypoint, deployment, environment = load_fastmcp_config(sample_config)
config = load_fastmcp_config(sample_config)
# Check that we got the right types
assert isinstance(entrypoint, EntrypointConfig)
assert isinstance(deployment, DeploymentConfig)
assert isinstance(environment, EnvironmentConfig)
assert isinstance(config, FastMCPConfig)
assert isinstance(config.source, FileSystemSource)
assert isinstance(config.deployment, Deployment)
assert isinstance(config.environment, Environment)
# Check entrypoint
assert entrypoint.file.endswith("server.py")
assert Path(entrypoint.file).is_absolute()
assert entrypoint.object is None
# Check source - path is not resolved yet, only during load_server
assert config.source.path == "server.py"
assert config.source.entrypoint is None
# Check environment config
assert environment.python == "3.11"
assert environment.dependencies == ["requests"]
assert config.environment.python == "3.11"
assert config.environment.dependencies == ["requests"]
# Check deployment config
assert deployment.transport == "stdio"
assert deployment.env == {"TEST_VAR": "test_value"}
assert config.deployment.transport == "stdio"
assert config.deployment.env == {"TEST_VAR": "test_value"}
# Check that environment variables were applied
assert os.environ.get("TEST_VAR") == "test_value"
@ -78,10 +79,10 @@ def test_load_fastmcp_config(sample_config, monkeypatch):
os.environ.update(original_env)
def test_load_config_with_object_entrypoint(tmp_path):
"""Test loading config with object-format entrypoint."""
def test_load_config_with_entrypoint_source(tmp_path):
"""Test loading config with entrypoint-format source."""
config_data = {
"entrypoint": {"file": "src/server.py", "object": "app"},
"source": {"path": "src/server.py", "entrypoint": "app"},
"deployment": {"transport": "http", "port": 8000},
}
@ -94,29 +95,25 @@ def test_load_config_with_object_entrypoint(tmp_path):
server_file = src_dir / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
config = load_fastmcp_config(config_file)
# Check entrypoint resolution
assert entrypoint.file == str(server_file.resolve())
assert entrypoint.object == "app"
# Check source - path is not resolved yet, only during load_server
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "app"
# Check deployment
assert deployment is not None
assert deployment.transport == "http"
assert deployment.port == 8000
# No environment config
assert environment is None
assert config.deployment.transport == "http"
assert config.deployment.port == 8000
def test_load_config_with_cwd(tmp_path):
"""Test that DeploymentConfig applies working directory change."""
"""Test that Deployment applies working directory change."""
# Create a subdirectory
subdir = tmp_path / "subdir"
subdir.mkdir()
config_data = {"entrypoint": "server.py", "deployment": {"cwd": "subdir"}}
config_data = {"source": {"path": "server.py"}, "deployment": {"cwd": "subdir"}}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
@ -128,7 +125,7 @@ def test_load_config_with_cwd(tmp_path):
original_cwd = os.getcwd()
try:
entrypoint, deployment, environment = load_fastmcp_config(config_file)
config = load_fastmcp_config(config_file) # noqa: F841
# Check that working directory was changed
assert Path.cwd() == subdir.resolve()
@ -147,7 +144,7 @@ def test_load_config_with_relative_cwd(tmp_path):
subdir2.mkdir(parents=True)
config_data = {
"entrypoint": "server.py",
"source": {"path": "server.py"},
"deployment": {
"cwd": "../" # Relative to config file location
},
@ -163,7 +160,7 @@ def test_load_config_with_relative_cwd(tmp_path):
original_cwd = os.getcwd()
try:
entrypoint, deployment, environment = load_fastmcp_config(config_file)
config = load_fastmcp_config(config_file) # noqa: F841
# Should change to parent directory of config file
assert Path.cwd() == subdir1.resolve()
@ -173,8 +170,8 @@ def test_load_config_with_relative_cwd(tmp_path):
def test_load_minimal_config(tmp_path):
"""Test loading minimal configuration with only entrypoint."""
config_data = {"entrypoint": "server.py"}
"""Test loading minimal configuration with only source."""
config_data = {"source": {"path": "server.py"}}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
@ -183,21 +180,17 @@ def test_load_minimal_config(tmp_path):
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
config = 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
# Check we got source - path is not resolved yet, only during load_server
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
def test_load_config_with_server_args(tmp_path):
"""Test configuration with server arguments."""
config_data = {
"entrypoint": "server.py",
"source": {"path": "server.py"},
"deployment": {"args": ["--debug", "--config", "custom.json"]},
}
@ -208,16 +201,15 @@ def test_load_config_with_server_args(tmp_path):
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
config = load_fastmcp_config(config_file)
assert deployment is not None
assert deployment.args == ["--debug", "--config", "custom.json"]
assert config.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",
"source": {"path": "server.py"},
"environment": {"python": "3.12", "dependencies": ["pandas"]},
"deployment": {"transport": "http", "host": "0.0.0.0", "port": 3000},
}
@ -229,36 +221,20 @@ def test_config_subset_independence(tmp_path):
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
config = load_fastmcp_config(config_file)
# Each subset should be independently usable
assert entrypoint.file == str(server_file.resolve())
assert entrypoint.object is None
# Path is not resolved yet, only during load_server
assert config.source.path == "server.py"
assert config.source.entrypoint is None
assert environment is not None
assert environment.python == "3.12"
assert environment.dependencies == ["pandas"]
assert environment.needs_uv() # Has dependencies
assert config.environment.python == "3.12"
assert config.environment.dependencies == ["pandas"]
assert config.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
assert config.deployment.transport == "http"
assert config.deployment.host == "0.0.0.0"
assert config.deployment.port == 3000
def test_environment_config_path_resolution(tmp_path):
@ -268,7 +244,7 @@ def test_environment_config_path_resolution(tmp_path):
reqs_file.write_text("fastmcp>=2.0")
config_data = {
"entrypoint": "server.py",
"source": {"path": "server.py"},
"environment": {
"requirements": "requirements.txt",
"project": ".",
@ -283,11 +259,10 @@ def test_environment_config_path_resolution(tmp_path):
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
config = 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"])
uv_args = config.environment.build_uv_args(["fastmcp", "run", "server.py"])
assert "--with-requirements" in uv_args
assert "--project" in uv_args

View file

@ -26,7 +26,16 @@ class TestRunWithUv:
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
expected = ["uv", "run", "--with", "fastmcp", "fastmcp", "run", "server.py"]
expected = [
"uv",
"run",
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
"--skip-env",
]
assert cmd == expected
@patch("subprocess.run")
@ -50,6 +59,7 @@ class TestRunWithUv:
"fastmcp",
"run",
"server.py",
"--skip-env",
]
assert cmd == expected
@ -57,7 +67,8 @@ class TestRunWithUv:
def test_run_with_uv_project(self, mock_run):
"""Test run_with_uv with project directory."""
mock_run.return_value = Mock(returncode=0)
project_path = Path("/my/project")
# Use an absolute path that works on all platforms
project_path = Path.cwd() / "my" / "project"
with pytest.raises(SystemExit) as exc_info:
run_with_uv("server.py", project=project_path)
@ -70,7 +81,14 @@ class TestRunWithUv:
# Check that the project path is absolute
assert Path(cmd[3]).is_absolute()
# Check the rest of the command
assert cmd[4:] == ["--with", "fastmcp", "fastmcp", "run", "server.py"]
assert cmd[4:] == [
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
"--skip-env",
]
@patch("subprocess.run")
def test_run_with_uv_with_packages(self, mock_run):
@ -89,12 +107,13 @@ class TestRunWithUv:
"--with",
"fastmcp",
"--with",
"numpy", # sorted alphabetically
"pandas", # original order preserved
"--with",
"pandas", # sorted alphabetically
"numpy", # original order preserved
"fastmcp",
"run",
"server.py",
"--skip-env",
]
assert cmd == expected
@ -116,10 +135,11 @@ class TestRunWithUv:
"--with",
"fastmcp",
"--with-requirements",
str(req_path.expanduser().resolve()), # resolved to absolute path
str(req_path.resolve()), # auto-resolved to absolute path
"fastmcp",
"run",
"server.py",
"--skip-env",
]
assert cmd == expected
@ -150,6 +170,7 @@ class TestRunWithUv:
"fastmcp",
"run",
"server.py",
"--skip-env",
"--transport",
"http",
"--host",
@ -169,11 +190,14 @@ class TestRunWithUv:
"""Test run_with_uv with all options combined."""
mock_run.return_value = Mock(returncode=0)
# Use an absolute path that works on all platforms
project_path = Path.cwd() / "workspace"
with pytest.raises(SystemExit) as exc_info:
run_with_uv(
"server.py",
python_version="3.10",
project=Path("/workspace"),
project=project_path,
with_packages=["pandas"],
with_requirements=Path("reqs.txt"),
transport="http",
@ -191,12 +215,14 @@ class TestRunWithUv:
assert Path(cmd[5]).is_absolute()
assert cmd[6:10] == ["--with", "fastmcp", "--with", "pandas"]
assert cmd[10] == "--with-requirements"
# Check requirements path is absolute
# Check requirements path is now auto-resolved to absolute
assert Path(cmd[11]).is_absolute()
assert Path(cmd[11]).name == "reqs.txt"
assert cmd[12:] == [
"fastmcp",
"run",
"server.py",
"--skip-env",
"--transport",
"http",
"--port",

View file

@ -1,39 +1,38 @@
from pathlib import Path
from fastmcp.utilities.cli import build_uv_command
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import Environment
class TestBuildUVCommand:
"""Test the build_uv_command function."""
class TestEnvironmentBuildUVArgs:
"""Test the Environment.build_uv_args() method."""
def test_build_uv_command_basic(self):
"""Test building basic uv command."""
cmd = build_uv_command("server.py")
expected = ["uv", "run", "--with", "fastmcp", "fastmcp", "run", "server.py"]
assert cmd == expected
def test_build_uv_args_basic(self):
"""Test building basic uv args."""
env = Environment()
args = env.build_uv_args(["fastmcp", "run", "server.py"])
expected = ["run", "--with", "fastmcp", "fastmcp", "run", "server.py"]
assert args == expected
def test_build_uv_command_with_editable(self):
"""Test building uv command with editable package."""
editable_path = Path("/path/to/package")
cmd = build_uv_command("server.py", with_editable=editable_path)
def test_build_uv_args_with_editable(self):
"""Test building uv args with editable package."""
editable_path = "/path/to/package"
env = Environment(editable=editable_path)
args = env.build_uv_args(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-editable",
str(editable_path.expanduser().resolve()),
editable_path,
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
assert args == expected
def test_build_uv_command_with_packages(self):
"""Test building uv command with additional packages."""
cmd = build_uv_command("server.py", with_packages=["pkg1", "pkg2"])
def test_build_uv_args_with_packages(self):
"""Test building uv args with additional packages."""
env = Environment(dependencies=["pkg1", "pkg2"])
args = env.build_uv_args(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--with",
"fastmcp",
@ -45,13 +44,13 @@ class TestBuildUVCommand:
"run",
"server.py",
]
assert cmd == expected
assert args == expected
def test_build_uv_command_with_python_version(self):
"""Test building uv command with Python version."""
cmd = build_uv_command("server.py", python_version="3.11")
def test_build_uv_args_with_python_version(self):
"""Test building uv args with Python version."""
env = Environment(python="3.11")
args = env.build_uv_args(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--python",
"3.11",
@ -61,113 +60,109 @@ class TestBuildUVCommand:
"run",
"server.py",
]
assert cmd == expected
assert args == expected
def test_build_uv_command_with_project(self):
"""Test building uv command with project directory."""
project_path = Path("/path/to/project")
cmd = build_uv_command("server.py", project=project_path)
def test_build_uv_args_with_project(self):
"""Test building uv args with project directory."""
project_path = "/path/to/project"
env = Environment(project=project_path)
args = env.build_uv_args(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--project",
str(project_path.expanduser().resolve()),
project_path,
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
assert args == expected
def test_build_uv_command_with_requirements(self):
"""Test building uv command with requirements file."""
req_path = Path("requirements.txt")
cmd = build_uv_command("server.py", with_requirements=req_path)
def test_build_uv_args_with_requirements(self):
"""Test building uv args with requirements file."""
req_path = "requirements.txt"
env = Environment(requirements=req_path)
args = env.build_uv_args(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-requirements",
str(req_path.expanduser().resolve()),
req_path,
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
assert args == expected
def test_build_uv_command_with_all_options(self):
"""Test building uv command with all options."""
project_path = Path("/my/project")
editable_path = Path("/local/pkg")
requirements_path = Path("reqs.txt")
cmd = build_uv_command(
"server.py",
python_version="3.10",
def test_build_uv_args_with_all_options(self):
"""Test building uv args with all options."""
project_path = "/my/project"
editable_path = "/local/pkg"
requirements_path = "reqs.txt"
env = Environment(
python="3.10",
project=project_path,
with_packages=["pandas", "numpy"],
with_requirements=requirements_path,
with_editable=editable_path,
dependencies=["pandas", "numpy"],
requirements=requirements_path,
editable=editable_path,
)
args = env.build_uv_args(["fastmcp", "run", "server.py"])
expected = [
"uv",
"run",
"--python",
"3.10",
"--project",
str(project_path.expanduser().resolve()),
project_path,
"--with",
"fastmcp",
"--with",
"numpy",
"--with",
"pandas",
"--with-editable",
str(editable_path.expanduser().resolve()),
"--with",
"numpy",
"--with-requirements",
str(requirements_path.expanduser().resolve()),
requirements_path,
"--with-editable",
editable_path,
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
assert args == expected
def test_with_editable_resolves_dot(self):
"""Test that '.' in with_editable becomes absolute."""
cmd = build_uv_command("server.py", with_editable=Path("."))
idx = cmd.index("--with-editable") + 1
assert Path(cmd[idx]).is_absolute()
def test_build_uv_args_no_command(self):
"""Test building uv args with no command."""
env = Environment(python="3.11")
args = env.build_uv_args()
expected = ["run", "--python", "3.11", "--with", "fastmcp"]
assert args == expected
def test_with_editable_resolves_tilde(self):
"""Test that '~' in with_editable is expanded."""
cmd = build_uv_command("server.py", with_editable=Path("~/project"))
idx = cmd.index("--with-editable") + 1
assert Path(cmd[idx]).is_absolute()
assert "~" not in cmd[idx]
def test_build_uv_args_string_command(self):
"""Test building uv args with string command."""
env = Environment()
args = env.build_uv_args("python")
expected = ["run", "--with", "fastmcp", "python"]
assert args == expected
def test_with_requirements_resolves_dot(self):
"""Test that '.' in with_requirements becomes absolute."""
cmd = build_uv_command("server.py", with_requirements=Path("./reqs.txt"))
idx = cmd.index("--with-requirements") + 1
assert Path(cmd[idx]).is_absolute()
def test_needs_uv_true(self):
"""Test that needs_uv returns True when environment settings are present."""
env = Environment(python="3.11")
assert env.needs_uv() is True
def test_with_requirements_resolves_tilde(self):
"""Test that '~' in with_requirements is expanded."""
cmd = build_uv_command("server.py", with_requirements=Path("~/reqs.txt"))
idx = cmd.index("--with-requirements") + 1
assert Path(cmd[idx]).is_absolute()
assert "~" not in cmd[idx]
env = Environment(dependencies=["pkg"])
assert env.needs_uv() is True
def test_project_resolves_relative(self):
"""Test that relative path in project becomes absolute."""
cmd = build_uv_command("server.py", project=Path("../project"))
idx = cmd.index("--project") + 1
assert Path(cmd[idx]).is_absolute()
env = Environment(requirements="reqs.txt")
assert env.needs_uv() is True
def test_project_resolves_tilde(self):
"""Test that '~' in project is expanded."""
cmd = build_uv_command("server.py", project=Path("~/work"))
idx = cmd.index("--project") + 1
assert Path(cmd[idx]).is_absolute()
assert "~" not in cmd[idx]
env = Environment(project="/project")
assert env.needs_uv() is True
env = Environment(editable="/pkg")
assert env.needs_uv() is True
def test_needs_uv_false(self):
"""Test that needs_uv returns False when no environment settings are present."""
env = Environment()
assert env.needs_uv() is False