Skip to content

Commit 465e403

Browse files
authored
Improve fastmcp.json environment configuration and project-based deployments (#1631)
1 parent 3f24e40 commit 465e403

File tree

18 files changed

+866
-356
lines changed

18 files changed

+866
-356
lines changed

docs/deployment/server-configuration.mdx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,14 @@ These settings leverage standard `uv` arguments for environment creation. When a
165165
```
166166
</ParamField>
167167

168-
<ParamField body="editable" type="string">
169-
Path to a package to install in editable/development mode. Useful for local development when you want changes to be reflected immediately.
168+
<ParamField body="editable" type="list[string]">
169+
List of paths to packages to install in editable/development mode. Useful for local development when you want changes to be reflected immediately. Supports multiple packages for monorepo setups or shared libraries.
170170
```json
171-
"editable": "."
171+
"editable": ["."]
172+
```
173+
Or with multiple packages:
174+
```json
175+
"editable": [".", "../shared-lib", "/path/to/another-package"]
172176
```
173177
</ParamField>
174178
</Expandable>
@@ -312,6 +316,20 @@ fastmcp run fastmcp.json --skip-source
312316
fastmcp run fastmcp.json --skip-env --skip-source
313317
```
314318

319+
### Pre-building Environments
320+
321+
You can use `fastmcp project prepare` to create a persistent uv project with all dependencies pre-installed:
322+
323+
```bash
324+
# Create a persistent environment
325+
fastmcp project prepare fastmcp.json --output-dir ./env
326+
327+
# Use the pre-built environment to run the server
328+
fastmcp run fastmcp.json --project ./env
329+
```
330+
331+
This pattern separates environment setup (slow) from server execution (fast), useful for deployment scenarios.
332+
315333
### Using an Existing Environment
316334

317335
By default, FastMCP creates an isolated environment with `uv` based on your configuration. When you already have a suitable Python environment, use the `--skip-env` flag to skip environment creation:

docs/patterns/cli.mdx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ fastmcp --help
2222
| `dev` | Run a server with the MCP Inspector for testing | **Supports:** Local files and fastmcp.json configs. **Deps:** Always runs via `uv run` subprocess (never uses your local environment); dependencies must be specified or available in a uv-managed project. With fastmcp.json: Uses configured dependencies |
2323
| `install` | Install a server in MCP client applications | **Supports:** Local files and fastmcp.json configs. **Deps:** Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable`. With fastmcp.json: Uses configured dependencies |
2424
| `inspect` | Generate a JSON report about a FastMCP server | **Supports:** Local files and fastmcp.json configs. **Deps:** Uses your current environment; you are responsible for ensuring all dependencies are available |
25+
| `project prepare` | Create a persistent uv project from fastmcp.json environment config | **Supports:** fastmcp.json configs only. **Deps:** Creates a uv project directory with all dependencies pre-installed for reuse with `--project` flag |
2526
| `version` | Display version information | N/A |
2627

2728
## `fastmcp run`
@@ -473,6 +474,37 @@ fastmcp inspect server.py:my_server
473474
fastmcp inspect server.py --output analysis.json
474475
```
475476

477+
## `fastmcp project prepare`
478+
479+
Create a persistent uv project directory from a fastmcp.json file's environment configuration. This allows you to pre-install all dependencies once and reuse them with the `--project` flag.
480+
481+
```bash
482+
fastmcp project prepare fastmcp.json --output-dir ./env
483+
```
484+
485+
### Options
486+
487+
| Option | Flag | Description |
488+
| ------ | ---- | ----------- |
489+
| Output Directory | `--output-dir` | **Required.** Directory where the persistent uv project will be created |
490+
491+
### Usage Pattern
492+
493+
```bash
494+
# Step 1: Prepare the environment (installs dependencies)
495+
fastmcp project prepare fastmcp.json --output-dir ./my-env
496+
497+
# Step 2: Run using the prepared environment (fast, no dependency installation)
498+
fastmcp run fastmcp.json --project ./my-env
499+
```
500+
501+
The prepare command creates a uv project with:
502+
- A `pyproject.toml` containing all dependencies from the fastmcp.json
503+
- A `.venv` with all packages pre-installed
504+
- A `uv.lock` file for reproducible environments
505+
506+
This is useful when you want to separate environment setup from server execution, such as in deployment scenarios where dependencies are installed once and the server is run multiple times.
507+
476508
## `fastmcp version`
477509

478510
Display version information about FastMCP and related components.

src/fastmcp/cli/claude.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def update_claude_config(
101101
# Build uv run command using Environment.build_uv_args()
102102
env_config = Environment(
103103
dependencies=deduplicated_packages,
104-
editable=str(with_editable) if with_editable else None,
104+
editable=[str(with_editable)] if with_editable else None,
105105
)
106106
args = env_config.build_uv_args()
107107

src/fastmcp/cli/cli.py

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,11 @@ async def dev(
229229
if config.environment.requirements
230230
else None
231231
)
232+
# Note: config.environment.editable is a list, but CLI only supports single path
233+
# Take the first editable path if available
232234
with_editable = with_editable or (
233-
Path(config.environment.editable)
234-
if config.environment.editable
235+
Path(config.environment.editable[0])
236+
if config.environment.editable and config.environment.editable[0]
235237
else None
236238
)
237239

@@ -303,7 +305,7 @@ async def dev(
303305
dependencies=with_packages if with_packages else None,
304306
requirements=str(with_requirements) if with_requirements else None,
305307
project=str(project) if project else None,
306-
editable=str(with_editable) if with_editable else None,
308+
editable=[str(with_editable)] if with_editable else None,
307309
)
308310
uv_cmd = ["uv"] + env_config.build_uv_args(["fastmcp", "run", server_spec])
309311

@@ -415,19 +417,19 @@ async def run(
415417
help="Requirements file to install dependencies from",
416418
),
417419
] = None,
418-
skip_env: Annotated[
420+
skip_source: Annotated[
419421
bool,
420422
cyclopts.Parameter(
421-
"--skip-env",
422-
help="Skip environment setup with uv (use when already in a uv environment)",
423+
"--skip-source",
424+
help="Skip source preparation step (use when source is already prepared)",
423425
negative="",
424426
),
425427
] = False,
426-
skip_source: Annotated[
428+
skip_env: Annotated[
427429
bool,
428430
cyclopts.Parameter(
429-
"--skip-source",
430-
help="Skip source preparation step (use when source is already prepared)",
431+
"--skip-env",
432+
help="Skip environment configuration (for internal use when already in a uv environment)",
431433
negative="",
432434
),
433435
] = False,
@@ -509,7 +511,8 @@ async def run(
509511
)
510512

511513
# Merge environment config with CLI values (CLI takes precedence)
512-
if config.environment:
514+
# BUT: Skip this if --skip-env is set
515+
if config.environment and not skip_env:
513516
python = python or config.environment.python
514517
project = project or (
515518
Path(config.environment.project)
@@ -552,12 +555,10 @@ async def run(
552555
)
553556

554557
# Check if we need to use uv run (either from CLI args or config)
555-
# Skip if --skip-env flag is set (we're already in a uv environment)
556-
needs_uv = not skip_env and (
557-
python or with_packages or with_requirements or project or editable
558-
)
559-
if not skip_env and not needs_uv and config and config.environment:
560-
# Check if config's environment needs uv
558+
# When --skip-env is set, we ignore config.environment entirely
559+
needs_uv = python or with_packages or with_requirements or project or editable
560+
if not needs_uv and config and config.environment and not skip_env:
561+
# Check if config's environment needs uv (but only if not skipping env)
561562
needs_uv = config.environment.needs_uv()
562563

563564
if needs_uv:
@@ -818,6 +819,101 @@ async def inspect(
818819
sys.exit(1)
819820

820821

822+
# Create project subcommand group
823+
project_app = cyclopts.App(name="project", help="Manage FastMCP projects")
824+
825+
826+
@project_app.command
827+
async def prepare(
828+
config_path: Annotated[
829+
str | None,
830+
cyclopts.Parameter(help="Path to fastmcp.json configuration file"),
831+
] = None,
832+
output_dir: Annotated[
833+
str | None,
834+
cyclopts.Parameter(help="Directory to create the persistent environment in"),
835+
] = None,
836+
skip_source: Annotated[
837+
bool,
838+
cyclopts.Parameter(help="Skip source preparation (e.g., git clone)"),
839+
] = False,
840+
) -> None:
841+
"""Prepare a FastMCP project by creating a persistent uv environment.
842+
843+
This command creates a persistent uv project with all dependencies installed:
844+
- Creates a pyproject.toml with dependencies from the config
845+
- Installs all Python packages into a .venv
846+
- Prepares the source (git clone, download, etc.) unless --skip-source
847+
848+
After running this command, you can use:
849+
fastmcp run <config> --project <output-dir>
850+
851+
This is useful for:
852+
- CI/CD pipelines with separate build and run stages
853+
- Docker images where you prepare during build
854+
- Production deployments where you want fast startup times
855+
856+
Example:
857+
fastmcp project prepare myserver.json --output-dir ./prepared-env
858+
fastmcp run myserver.json --project ./prepared-env
859+
"""
860+
from pathlib import Path
861+
862+
from fastmcp.utilities.fastmcp_config import FastMCPConfig
863+
864+
# Require output-dir
865+
if output_dir is None:
866+
logger.error(
867+
"The --output-dir parameter is required.\n"
868+
"Please specify where to create the persistent environment."
869+
)
870+
sys.exit(1)
871+
872+
# Auto-detect fastmcp.json if not provided
873+
if config_path is None:
874+
found_config = FastMCPConfig.find_config()
875+
if found_config:
876+
config_path = str(found_config)
877+
logger.info(f"Using configuration from {config_path}")
878+
else:
879+
logger.error(
880+
"No configuration file specified and no fastmcp.json found.\n"
881+
"Please specify a configuration file or create a fastmcp.json."
882+
)
883+
sys.exit(1)
884+
885+
config_file = Path(config_path)
886+
if not config_file.exists():
887+
logger.error(f"Configuration file not found: {config_path}")
888+
sys.exit(1)
889+
890+
output_path = Path(output_dir)
891+
892+
try:
893+
# Load the configuration
894+
config = FastMCPConfig.from_file(config_file)
895+
896+
# Prepare environment and source
897+
await config.prepare(
898+
skip_source=skip_source,
899+
output_dir=output_path,
900+
)
901+
902+
console.print(
903+
f"[bold green]✓[/bold green] Project prepared successfully in {output_path}!\n"
904+
f"You can now run the server with:\n"
905+
f" [cyan]fastmcp run {config_path} --project {output_dir}[/cyan]"
906+
)
907+
908+
except Exception as e:
909+
logger.error(f"Failed to prepare project: {e}")
910+
console.print(f"[bold red]✗[/bold red] Failed to prepare project: {e}")
911+
sys.exit(1)
912+
913+
914+
# Add project subcommand group
915+
app.command(project_app)
916+
821917
# Add install subcommands using proper Cyclopts pattern
822918
app.command(install_app)
823919

src/fastmcp/cli/install/claude_code.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def install_claude_code(
121121
dependencies=deduplicated_packages,
122122
requirements=str(with_requirements) if with_requirements else None,
123123
project=str(project) if project else None,
124-
editable=str(with_editable) if with_editable else None,
124+
editable=[str(with_editable)] if with_editable else None,
125125
)
126126
args = env_config.build_uv_args()
127127

src/fastmcp/cli/install/claude_desktop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def install_claude_desktop(
8686
dependencies=deduplicated_packages,
8787
requirements=str(with_requirements) if with_requirements else None,
8888
project=str(project) if project else None,
89-
editable=str(with_editable) if with_editable else None,
89+
editable=[str(with_editable)] if with_editable else None,
9090
)
9191
args = env_config.build_uv_args()
9292

src/fastmcp/cli/install/cursor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def install_cursor_workspace(
120120
dependencies=deduplicated_packages,
121121
requirements=str(with_requirements.resolve()) if with_requirements else None,
122122
project=str(project.resolve()) if project else None,
123-
editable=str(with_editable.resolve()) if with_editable else None,
123+
editable=[str(with_editable.resolve())] if with_editable else None,
124124
)
125125
args = env_config.build_uv_args()
126126

@@ -200,7 +200,7 @@ def install_cursor(
200200
dependencies=deduplicated_packages,
201201
requirements=str(with_requirements.resolve()) if with_requirements else None,
202202
project=str(project.resolve()) if project else None,
203-
editable=str(with_editable.resolve()) if with_editable else None,
203+
editable=[str(with_editable.resolve())] if with_editable else None,
204204
)
205205
args = env_config.build_uv_args()
206206

src/fastmcp/cli/install/mcp_json.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def install_mcp_json(
6161
dependencies=deduplicated_packages,
6262
requirements=str(with_requirements) if with_requirements else None,
6363
project=str(project) if project else None,
64-
editable=str(with_editable) if with_editable else None,
64+
editable=[str(with_editable)] if with_editable else None,
6565
)
6666
args = env_config.build_uv_args()
6767

0 commit comments

Comments
 (0)