Jeremiah Lowin commited on
Commit
5f2c09d
·
unverified ·
1 Parent(s): d2b87a8

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

Browse files
Files changed (40) hide show
  1. docs/deployment/server-configuration.mdx +539 -0
  2. docs/docs.json +1 -0
  3. docs/integrations/claude-code.mdx +14 -9
  4. docs/integrations/claude-desktop.mdx +13 -8
  5. docs/integrations/cursor.mdx +13 -8
  6. docs/integrations/mcp-json-configuration.mdx +17 -7
  7. docs/patterns/cli.mdx +56 -13
  8. docs/schemas/fastmcp_config/latest.json +1 -0
  9. docs/schemas/fastmcp_config/v1.json +1 -0
  10. docs/servers/server.mdx +2 -6
  11. examples/atproto_mcp/fastmcp.json +9 -0
  12. examples/atproto_mcp/src/atproto_mcp/server.py +1 -6
  13. examples/fastmcp_config/env_interpolation_example.json +20 -0
  14. examples/fastmcp_config/fastmcp.json +11 -0
  15. examples/fastmcp_config/full_example.fastmcp.json +30 -0
  16. examples/fastmcp_config/server.py +39 -0
  17. examples/fastmcp_config/simple.fastmcp.json +7 -0
  18. examples/fastmcp_config_demo/README.md +55 -0
  19. examples/fastmcp_config_demo/fastmcp.json +15 -0
  20. examples/fastmcp_config_demo/server.py +70 -0
  21. examples/memory.fastmcp.json +12 -0
  22. examples/mount_example.fastmcp.json +4 -0
  23. examples/mount_example.py +1 -3
  24. examples/screenshot.fastmcp.json +7 -0
  25. examples/smart_home/hub.fastmcp.json +9 -0
  26. examples/smart_home/lights.fastmcp.json +9 -0
  27. src/fastmcp/cli/cli.py +298 -10
  28. src/fastmcp/cli/install/shared.py +39 -3
  29. src/fastmcp/cli/run.py +135 -6
  30. src/fastmcp/client/transports.py +36 -16
  31. src/fastmcp/mcp_config.py +15 -12
  32. src/fastmcp/server/server.py +19 -1
  33. src/fastmcp/utilities/fastmcp_config/__init__.py +21 -0
  34. src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py +678 -0
  35. src/fastmcp/utilities/fastmcp_config/v1/schema.json +361 -0
  36. tests/cli/test_config.py +513 -0
  37. tests/cli/test_fastmcp_config_integration.py +355 -0
  38. tests/cli/test_fastmcp_config_schema.py +163 -0
  39. tests/cli/test_run_config.py +298 -0
  40. tests/deprecated/test_dependencies.py +30 -0
docs/deployment/server-configuration.mdx ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Server Configuration with fastmcp.json
3
+ sidebarTitle: Server Configuration
4
+ description: Use fastmcp.json for declarative server configuration
5
+ icon: file-code
6
+ ---
7
+
8
+ import { VersionBadge } from "/snippets/version-badge.mdx"
9
+
10
+ <VersionBadge version="2.11.4" />
11
+
12
+ 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.
13
+
14
+ ## Overview
15
+
16
+ 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.
17
+
18
+ When you have a `fastmcp.json` file, running your server becomes as simple as:
19
+
20
+ ```bash
21
+ # Run the server using the configuration
22
+ fastmcp run fastmcp.json
23
+
24
+ # Or if fastmcp.json exists in the current directory
25
+ fastmcp run
26
+ ```
27
+
28
+ 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.
29
+
30
+ ## JSON Schema Support
31
+
32
+ FastMCP provides JSON schemas for IDE autocomplete and validation. Add the schema reference to your `fastmcp.json` for enhanced developer experience:
33
+
34
+ ```json
35
+ {
36
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
37
+ "entrypoint": {
38
+ "file": "server.py",
39
+ "object": "mcp"
40
+ }
41
+ }
42
+ ```
43
+
44
+ Two schema URLs are available:
45
+ - **Version-specific**: `https://gofastmcp.com/schemas/fastmcp_config/v1.json`
46
+ - **Latest version**: `https://gofastmcp.com/schemas/fastmcp_config/latest.json`
47
+
48
+ Modern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.
49
+
50
+ ## File Structure
51
+
52
+ The `fastmcp.json` file has three main sections, each controlling a different aspect of your server:
53
+
54
+ ```json
55
+ {
56
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
57
+ "entrypoint": {
58
+ "file": "server.py",
59
+ "object": "mcp"
60
+ },
61
+ "environment": {
62
+ // Python environment and dependencies
63
+ },
64
+ "deployment": {
65
+ // Runtime configuration
66
+ }
67
+ }
68
+ ```
69
+
70
+ Only the `entrypoint` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
71
+
72
+ ## Configuration Fields
73
+
74
+ ### Entrypoint
75
+
76
+ 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.
77
+
78
+ <Card icon="code" title="Entrypoint Configuration">
79
+ <ParamField body="entrypoint" type="object | string" required>
80
+ The server entry point. Can be specified in three formats:
81
+
82
+ **Object format** (recommended): Explicit file and object specification
83
+ ```json
84
+ "entrypoint": {
85
+ "file": "src/server.py",
86
+ "object": "mcp"
87
+ }
88
+ ```
89
+
90
+ **String with object**: File path with colon and object name
91
+ ```json
92
+ "entrypoint": "src/server.py:app"
93
+ ```
94
+
95
+ **String format**: Simple path to Python file (searches for common names: mcp, server, app)
96
+ ```json
97
+ "entrypoint": "server.py"
98
+ ```
99
+
100
+ <Expandable title="Path Resolution">
101
+ - File paths are resolved relative to the configuration file's location
102
+ - 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`
103
+ - When no object is specified, FastMCP automatically searches for common server names: `mcp`, `server`, or `app`
104
+ </Expandable>
105
+ </ParamField>
106
+ </Card>
107
+
108
+ ### Environment
109
+
110
+ 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.
111
+
112
+ <Card icon="code" title="Environment Configuration">
113
+ <ParamField body="environment" type="object">
114
+ Optional Python environment configuration. When any field is specified, FastMCP automatically creates an isolated environment using `uv`.
115
+
116
+ <Expandable title="Environment Fields">
117
+ <ParamField body="python" type="string">
118
+ Python version constraint. Examples:
119
+ - Exact version: `"3.12"`
120
+ - Minimum version: `">=3.10"`
121
+ - Version range: `">=3.10,<3.13"`
122
+ </ParamField>
123
+
124
+ <ParamField body="dependencies" type="list[str]">
125
+ List of pip packages with optional version specifiers (PEP 508 format).
126
+ ```json
127
+ "dependencies": ["pandas>=2.0", "requests", "httpx"]
128
+ ```
129
+ </ParamField>
130
+
131
+ <ParamField body="requirements" type="string">
132
+ Path to a requirements.txt file, resolved relative to the config file location.
133
+ ```json
134
+ "requirements": "requirements.txt"
135
+ ```
136
+ </ParamField>
137
+
138
+ <ParamField body="project" type="string">
139
+ Path to a project directory containing pyproject.toml for uv project management.
140
+ ```json
141
+ "project": "."
142
+ ```
143
+ </ParamField>
144
+
145
+ <ParamField body="editable" type="string">
146
+ Path to a package to install in editable/development mode.
147
+ ```json
148
+ "editable": "./my-package"
149
+ ```
150
+ </ParamField>
151
+ </Expandable>
152
+ </ParamField>
153
+ </Card>
154
+
155
+ When environment configuration is provided, FastMCP:
156
+ 1. Creates an isolated Python environment using `uv`
157
+ 2. Installs the specified dependencies
158
+ 3. Runs your server in this clean environment
159
+
160
+ ### Deployment
161
+
162
+ The deployment section controls runtime configuration including transport protocol, networking, logging, and environment variables.
163
+
164
+ <Card icon="code" title="Deployment Configuration">
165
+ <ParamField body="deployment" type="object">
166
+ Optional runtime configuration for the server.
167
+
168
+ <Expandable title="Deployment Fields">
169
+ <ParamField body="transport" type="string" default="stdio">
170
+ Protocol for client communication:
171
+ - `"stdio"`: Standard input/output for desktop clients
172
+ - `"http"`: Network-accessible HTTP server
173
+ - `"sse"`: Server-sent events
174
+ </ParamField>
175
+
176
+ <ParamField body="host" type="string" default="127.0.0.1">
177
+ Network interface to bind (HTTP transport only):
178
+ - `"127.0.0.1"`: Local connections only
179
+ - `"0.0.0.0"`: All network interfaces
180
+ </ParamField>
181
+
182
+ <ParamField body="port" type="integer" default="3000">
183
+ Port number for HTTP transport.
184
+ </ParamField>
185
+
186
+ <ParamField body="path" type="string" default="/mcp/">
187
+ URL path for the MCP endpoint when using HTTP transport.
188
+ </ParamField>
189
+
190
+ <ParamField body="log_level" type="string" default="INFO">
191
+ Server logging verbosity. Options:
192
+ - `"DEBUG"`: Detailed debugging information
193
+ - `"INFO"`: General informational messages
194
+ - `"WARNING"`: Warning messages
195
+ - `"ERROR"`: Error messages only
196
+ - `"CRITICAL"`: Critical errors only
197
+ </ParamField>
198
+
199
+ <ParamField body="env" type="object">
200
+ Environment variables to set when running the server. Supports `${VAR_NAME}` syntax for runtime interpolation.
201
+ ```json
202
+ "env": {
203
+ "API_KEY": "secret-key",
204
+ "DATABASE_URL": "postgres://${DB_USER}@${DB_HOST}/mydb"
205
+ }
206
+ ```
207
+ </ParamField>
208
+
209
+ <ParamField body="cwd" type="string">
210
+ Working directory for the server process. Relative paths are resolved from the config file location.
211
+ </ParamField>
212
+
213
+ <ParamField body="args" type="list[str]">
214
+ Command-line arguments to pass to the server, passed after `--` to the server's argument parser.
215
+ ```json
216
+ "args": ["--config", "server-config.json"]
217
+ ```
218
+ </ParamField>
219
+ </Expandable>
220
+ </ParamField>
221
+ </Card>
222
+
223
+ ## Usage with CLI Commands
224
+
225
+ FastMCP automatically detects and uses `fastmcp.json` files, making server execution simple and consistent:
226
+
227
+ ```bash
228
+ # Auto-detect fastmcp.json in current directory
229
+ cd my-project
230
+ fastmcp run # No arguments needed!
231
+
232
+ # Or specify a configuration file explicitly
233
+ fastmcp run prod.fastmcp.json
234
+ ```
235
+
236
+ The configuration file works with all FastMCP commands:
237
+ - **`run`** - Start the server in production mode
238
+ - **`dev`** - Launch with the Inspector UI for development
239
+ - **`inspect`** - View server capabilities and configuration
240
+ - **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
241
+
242
+ 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.
243
+
244
+ ### Custom Naming Patterns
245
+
246
+ You can use different configuration files for different environments:
247
+
248
+ - `fastmcp.json` - Default configuration
249
+ - `dev.fastmcp.json` - Development settings
250
+ - `prod.fastmcp.json` - Production settings
251
+ - `test_fastmcp.json` - Test configuration
252
+
253
+ Any file with "fastmcp.json" in the name is recognized as a configuration file.
254
+
255
+ ## Examples
256
+
257
+ <Tabs>
258
+ <Tab title="Basic Configuration">
259
+
260
+ A minimal configuration for a simple server:
261
+
262
+ ```json
263
+ {
264
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
265
+ "entrypoint": {
266
+ "file": "server.py",
267
+ "object": "mcp"
268
+ }
269
+ }
270
+ ```
271
+ 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.
272
+ </Tab>
273
+ <Tab title="Development Configuration">
274
+
275
+ A configuration optimized for local development:
276
+
277
+ ```json
278
+ {
279
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
280
+ "entrypoint": "src/server.py:app",
281
+ "environment": {
282
+ "python": "3.12",
283
+ "dependencies": ["fastmcp[dev]"],
284
+ "editable": "."
285
+ },
286
+ "deployment": {
287
+ "transport": "http",
288
+ "host": "127.0.0.1",
289
+ "port": 8000,
290
+ "log_level": "DEBUG",
291
+ "env": {
292
+ "DEBUG": "true",
293
+ "ENV": "development"
294
+ }
295
+ }
296
+ }
297
+ ```
298
+ </Tab>
299
+ <Tab title="Production Configuration">
300
+
301
+ A production-ready configuration with full dependency management:
302
+
303
+ ```json
304
+ {
305
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
306
+ "entrypoint": {
307
+ "file": "app/main.py",
308
+ "object": "mcp_server"
309
+ },
310
+ "environment": {
311
+ "python": "3.11",
312
+ "requirements": "requirements/production.txt",
313
+ "project": "."
314
+ },
315
+ "deployment": {
316
+ "transport": "http",
317
+ "host": "0.0.0.0",
318
+ "port": 3000,
319
+ "path": "/api/mcp/",
320
+ "log_level": "INFO",
321
+ "env": {
322
+ "ENV": "production",
323
+ "API_BASE_URL": "https://api.example.com",
324
+ "DATABASE_URL": "postgresql://user:pass@db.example.com/prod"
325
+ },
326
+ "cwd": "/app",
327
+ "args": ["--workers", "4"]
328
+ }
329
+ }
330
+ ```
331
+ </Tab>
332
+ <Tab title="Data Science Server">
333
+
334
+ Configuration for a data analysis server with scientific packages:
335
+
336
+ ```json
337
+ {
338
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
339
+ "entrypoint": {
340
+ "file": "analysis_server.py",
341
+ "object": "mcp"
342
+ },
343
+ "environment": {
344
+ "python": "3.11",
345
+ "dependencies": [
346
+ "pandas>=2.0",
347
+ "numpy",
348
+ "scikit-learn",
349
+ "matplotlib",
350
+ "jupyterlab"
351
+ ]
352
+ },
353
+ "deployment": {
354
+ "transport": "stdio",
355
+ "env": {
356
+ "MATPLOTLIB_BACKEND": "Agg",
357
+ "DATA_PATH": "./datasets"
358
+ }
359
+ }
360
+ }
361
+ ```
362
+ </Tab>
363
+ <Tab title="Multi-Environment Setup">
364
+
365
+ You can maintain multiple configuration files for different environments:
366
+
367
+ **dev.fastmcp.json**:
368
+ ```json
369
+ {
370
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
371
+ "entrypoint": {
372
+ "file": "server.py",
373
+ "object": "mcp"
374
+ },
375
+ "deployment": {
376
+ "transport": "http",
377
+ "log_level": "DEBUG"
378
+ }
379
+ }
380
+ ```
381
+
382
+ **prod.fastmcp.json**:
383
+ ```json
384
+ {
385
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
386
+ "entrypoint": {
387
+ "file": "server.py",
388
+ "object": "mcp"
389
+ },
390
+ "environment": {
391
+ "requirements": "requirements/production.txt"
392
+ },
393
+ "deployment": {
394
+ "transport": "http",
395
+ "host": "0.0.0.0",
396
+ "log_level": "WARNING"
397
+ }
398
+ }
399
+ ```
400
+
401
+ Run different configurations:
402
+ ```bash
403
+ fastmcp run dev.fastmcp.json # Development
404
+ fastmcp run prod.fastmcp.json # Production
405
+ ```
406
+ </Tab>
407
+ </Tabs>
408
+ ## CLI Override Behavior
409
+
410
+ Command-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:
411
+
412
+ ```bash
413
+ # Config specifies port 3000, CLI overrides to 8080
414
+ fastmcp run fastmcp.json --port 8080
415
+
416
+ # Config specifies stdio, CLI overrides to HTTP
417
+ fastmcp run fastmcp.json --transport http
418
+
419
+ # Add extra dependencies not in config
420
+ fastmcp run fastmcp.json --with requests --with httpx
421
+ ```
422
+
423
+ This precedence order enables:
424
+ - Quick testing of different settings
425
+ - Environment-specific overrides in deployment scripts
426
+ - Debugging with increased log levels
427
+ - Temporary configuration changes
428
+
429
+ ## Best Practices
430
+
431
+ When using `fastmcp.json` for your projects, consider these recommendations:
432
+
433
+ **Version Control**: Always commit your `fastmcp.json` to version control. It's essential project documentation that ensures others can run your server correctly.
434
+
435
+ **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.
436
+
437
+ **Dependency Management**: Specify exact versions for production dependencies to ensure reproducible builds:
438
+ ```json
439
+ {
440
+ "dependencies": [
441
+ "pandas==2.1.0",
442
+ "requests==2.31.0"
443
+ ]
444
+ }
445
+ ```
446
+
447
+ **Path Resolution**: Remember that paths in the configuration are relative to the config file location. Use relative paths for portability:
448
+ ```json
449
+ {
450
+ "entrypoint": "./src/server.py",
451
+ "environment": {
452
+ "requirements": "./requirements.txt"
453
+ }
454
+ }
455
+ ```
456
+
457
+ **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.
458
+
459
+ ### Environment Variable Interpolation
460
+
461
+ 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:
462
+
463
+ ```json
464
+ {
465
+ "deployment": {
466
+ "env": {
467
+ "API_URL": "https://api.${ENVIRONMENT}.example.com",
468
+ "DATABASE_URL": "postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/myapp",
469
+ "CACHE_KEY": "myapp_${ENVIRONMENT}_${VERSION}"
470
+ }
471
+ }
472
+ }
473
+ ```
474
+
475
+ When the server starts, FastMCP replaces `${ENVIRONMENT}`, `${DB_USER}`, etc. with values from your system's environment variables. If a variable doesn't exist, the placeholder is preserved as-is.
476
+
477
+ **Example**: If your system has `ENVIRONMENT=production` and `DB_HOST=db.example.com`:
478
+ ```json
479
+ // Configuration
480
+ {
481
+ "deployment": {
482
+ "env": {
483
+ "API_URL": "https://api.${ENVIRONMENT}.example.com",
484
+ "DB_HOST": "${DB_HOST}"
485
+ }
486
+ }
487
+ }
488
+
489
+ // Result at runtime
490
+ {
491
+ "API_URL": "https://api.production.example.com",
492
+ "DB_HOST": "db.example.com"
493
+ }
494
+ ```
495
+
496
+ This feature is particularly useful for:
497
+ - Deploying the same configuration across development, staging, and production
498
+ - Keeping sensitive values out of configuration files
499
+ - Building dynamic URLs and connection strings
500
+ - Creating environment-specific prefixes or suffixes
501
+
502
+ ## Migrating from CLI Arguments
503
+
504
+ 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:
505
+
506
+ **CLI Command**:
507
+ ```bash
508
+ uv run --with pandas --with requests \
509
+ fastmcp run server.py \
510
+ --transport http \
511
+ --port 8000 \
512
+ --log-level INFO
513
+ ```
514
+
515
+ **Equivalent fastmcp.json**:
516
+ ```json
517
+ {
518
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
519
+ "entrypoint": {
520
+ "file": "server.py",
521
+ "object": "mcp"
522
+ },
523
+ "environment": {
524
+ "dependencies": ["pandas", "requests"]
525
+ },
526
+ "deployment": {
527
+ "transport": "http",
528
+ "port": 8000,
529
+ "log_level": "INFO"
530
+ }
531
+ }
532
+ ```
533
+
534
+ Now simply run:
535
+ ```bash
536
+ fastmcp run # Automatically finds and uses fastmcp.json
537
+ ```
538
+
539
+ The configuration file approach provides better documentation, easier sharing, and consistent execution across different environments while maintaining the flexibility to override settings when needed.
docs/docs.json CHANGED
@@ -110,6 +110,7 @@
110
  "icon": "rocket",
111
  "pages": [
112
  "deployment/running-server",
 
113
  "deployment/testing",
114
  "deployment/self-hosted",
115
  "deployment/fastmcp-cloud"
 
110
  "icon": "rocket",
111
  "pages": [
112
  "deployment/running-server",
113
+ "deployment/server-configuration",
114
  "deployment/testing",
115
  "deployment/self-hosted",
116
  "deployment/fastmcp-cloud"
docs/integrations/claude-code.mdx CHANGED
@@ -81,17 +81,22 @@ fastmcp install claude-code server.py --with-requirements requirements.txt
81
  fastmcp install claude-code server.py --with-editable ./my-local-package
82
  ```
83
 
84
- Alternatively, you can specify dependencies directly in your server code:
85
-
86
- ```python server.py
87
- from fastmcp import FastMCP
88
-
89
- mcp = FastMCP(
90
- name="Dice Roller",
91
- dependencies=["pandas", "requests"]
92
- )
 
 
 
 
93
  ```
94
 
 
95
  #### Python Version and Project Configuration
96
 
97
  Control the Python environment for your server with these options:
 
81
  fastmcp install claude-code server.py --with-editable ./my-local-package
82
  ```
83
 
84
+ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
85
+
86
+ ```json fastmcp.json
87
+ {
88
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
89
+ "entrypoint": {
90
+ "file": "server.py",
91
+ "object": "mcp"
92
+ },
93
+ "environment": {
94
+ "dependencies": ["pandas", "requests"]
95
+ }
96
+ }
97
  ```
98
 
99
+
100
  #### Python Version and Project Configuration
101
 
102
  Control the Python environment for your server with these options:
docs/integrations/claude-desktop.mdx CHANGED
@@ -98,17 +98,22 @@ fastmcp install claude-desktop server.py --with-requirements requirements.txt
98
  fastmcp install claude-desktop server.py --with-editable ./my-local-package
99
  ```
100
 
101
- Alternatively, you can specify dependencies directly in your server code:
102
 
103
- ```python server.py
104
- from fastmcp import FastMCP
105
-
106
- mcp = FastMCP(
107
- name="Dice Roller",
108
- dependencies=["pandas", "requests"]
109
- )
 
 
 
 
110
  ```
111
 
 
112
  #### Python Version and Project Directory
113
 
114
  FastMCP allows you to control the Python environment for your server:
 
98
  fastmcp install claude-desktop server.py --with-editable ./my-local-package
99
  ```
100
 
101
+ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
102
 
103
+ ```json fastmcp.json
104
+ {
105
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
106
+ "entrypoint": {
107
+ "file": "server.py",
108
+ "object": "mcp"
109
+ },
110
+ "environment": {
111
+ "dependencies": ["pandas", "requests"]
112
+ }
113
+ }
114
  ```
115
 
116
+
117
  #### Python Version and Project Directory
118
 
119
  FastMCP allows you to control the Python environment for your server:
docs/integrations/cursor.mdx CHANGED
@@ -99,17 +99,22 @@ fastmcp install cursor server.py --with-requirements requirements.txt
99
  fastmcp install cursor server.py --with-editable ./my-local-package
100
  ```
101
 
102
- Alternatively, you can specify dependencies directly in your server code:
103
 
104
- ```python server.py
105
- from fastmcp import FastMCP
106
-
107
- mcp = FastMCP(
108
- name="Dice Roller",
109
- dependencies=["pandas", "requests"]
110
- )
 
 
 
 
111
  ```
112
 
 
113
  #### Python Version and Project Configuration
114
 
115
  Control your server's Python environment with these options:
 
99
  fastmcp install cursor server.py --with-editable ./my-local-package
100
  ```
101
 
102
+ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
103
 
104
+ ```json fastmcp.json
105
+ {
106
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
107
+ "entrypoint": {
108
+ "file": "server.py",
109
+ "object": "mcp"
110
+ },
111
+ "environment": {
112
+ "dependencies": ["pandas", "requests"]
113
+ }
114
+ }
115
  ```
116
 
117
+
118
  #### Python Version and Project Configuration
119
 
120
  Control your server's Python environment with these options:
docs/integrations/mcp-json-configuration.mdx CHANGED
@@ -174,17 +174,27 @@ fastmcp install mcp-json server.py --with-editable ./my-package
174
  fastmcp install mcp-json server.py --with-requirements requirements.txt
175
  ```
176
 
177
- You can also specify dependencies directly in your server code:
178
 
179
- ```python server.py
180
- from fastmcp import FastMCP
 
 
 
 
 
 
 
 
 
 
181
 
182
- mcp = FastMCP(
183
- name="Data Analysis Server",
184
- dependencies=["pandas", "matplotlib", "seaborn"]
185
- )
186
  ```
187
 
 
188
  ### Environment Variables
189
 
190
  ```bash
 
174
  fastmcp install mcp-json server.py --with-requirements requirements.txt
175
  ```
176
 
177
+ You can also use a `fastmcp.json` configuration file (recommended):
178
 
179
+ ```json fastmcp.json
180
+ {
181
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
182
+ "entrypoint": {
183
+ "file": "server.py",
184
+ "object": "mcp"
185
+ },
186
+ "environment": {
187
+ "dependencies": ["pandas", "matplotlib", "seaborn"]
188
+ }
189
+ }
190
+ ```
191
 
192
+ Then simply install with:
193
+ ```bash
194
+ fastmcp install mcp-json fastmcp.json
 
195
  ```
196
 
197
+
198
  ### Environment Variables
199
 
200
  ```bash
docs/patterns/cli.mdx CHANGED
@@ -18,10 +18,10 @@ fastmcp --help
18
 
19
  | Command | Purpose | Dependency Management |
20
  | ------- | ------- | --------------------- |
21
- | `run` | Run a FastMCP server directly | **Supports:** Local files, factory functions, URLs, MCP configs. **Deps:** Uses your local environment directly. With `--python`, `--with`, `--project`, or `--with-requirements`: Runs via `uv run` subprocess |
22
- | `dev` | Run a server with the MCP Inspector for testing | **Supports:** Local files only. **Deps:** Always runs via `uv run` subprocess (never uses your local environment); dependencies must be specified or available in a uv-managed project |
23
- | `install` | Install a server in MCP client applications | **Supports:** Local files only. **Deps:** Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` |
24
- | `inspect` | Generate a JSON report about a FastMCP server | **Supports:** Local files only. **Deps:** Uses your current environment; you are responsible for ensuring all dependencies are available |
25
  | `version` | Display version information | N/A |
26
 
27
  ## `fastmcp run`
@@ -61,7 +61,8 @@ The `fastmcp run` command supports the following entrypoints:
61
  2. **[Explicit server object](#explicit-server-object)**: `server.py:custom_name` - imports and uses the specified server object
62
  3. **[Factory function](#factory-function)**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
63
  4. **[Remote server proxy](#remote-server-proxy)**: `https://example.com/mcp-server` - connects to a remote server and creates a **local proxy server**
64
- 5. **MCP configuration file**: `mcp.json` - runs servers defined in a standard MCP configuration file
 
65
 
66
  <Warning>
67
  Note: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == "__main__"` block. This means:
@@ -158,6 +159,28 @@ To start a local proxy, you can use the following syntax:
158
  fastmcp run https://example.com/mcp
159
  ```
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  #### MCP Configuration
162
 
163
  FastMCP can also run servers defined in a standard MCP configuration file. This is useful when you want to run multiple servers from a single file, or when you want to use a client that doesn't support direct connections to remote servers.
@@ -179,7 +202,12 @@ fastmcp dev server.py
179
  ```
180
 
181
  <Tip>
182
- This command always runs your server via `uv run` subprocess (never your local environment) to work with the MCP Inspector. All dependencies must be explicitly specified using the `--with` and/or `--with-editable` options, or be available in a uv-managed project.
 
 
 
 
 
183
  </Tip>
184
 
185
  <Warning>
@@ -214,14 +242,15 @@ This command does not support HTTP testing. To test a server over Streamable HTT
214
 
215
  ### Entrypoints
216
 
217
- The `dev` command supports local FastMCP server files only:
218
 
219
  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.
220
  2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
221
  3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
 
222
 
223
  <Warning>
224
- The `dev` command **only supports local files** - no URLs, remote servers, or MCP configuration files.
225
  </Warning>
226
 
227
  **Examples**
@@ -230,6 +259,12 @@ The `dev` command **only supports local files** - no URLs, remote servers, or MC
230
  # Run dev server with editable mode and additional packages
231
  fastmcp dev server.py -e . --with pandas --with matplotlib
232
 
 
 
 
 
 
 
233
  # Run dev server with specific Python version
234
  fastmcp dev server.py --python 3.11
235
 
@@ -286,18 +321,19 @@ Note that for security reasons, MCP clients usually run every server in a comple
286
 
287
  ### Entrypoints
288
 
289
- The `install` command supports local FastMCP server files only:
290
 
291
  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.
292
  2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
293
  3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
 
294
 
295
  <Note>
296
- Factory functions are particularly useful for install commands since they allow setup code to run that would otherwise be ignored when the MCP client runs your server.
297
  </Note>
298
 
299
  <Warning>
300
- The `install` command **only supports local files** - no URLs, remote servers, or MCP configuration files. For remote servers, use your MCP client's native configuration.
301
  </Warning>
302
 
303
  **Examples**
@@ -306,6 +342,12 @@ The `install` command **only supports local files** - no URLs, remote servers, o
306
  # Auto-detects server object (looks for 'mcp', 'server', or 'app')
307
  fastmcp install claude-desktop server.py
308
 
 
 
 
 
 
 
309
  # Uses specific server object
310
  fastmcp install claude-desktop server.py:my_server
311
 
@@ -395,14 +437,15 @@ fastmcp inspect server.py
395
 
396
  ### Entrypoints
397
 
398
- The `inspect` command supports local FastMCP server files only:
399
 
400
  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.
401
  2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
402
  3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
 
403
 
404
  <Warning>
405
- The `inspect` command **only supports local files** - no URLs, remote servers, or MCP configuration files.
406
  </Warning>
407
 
408
  **Examples**
 
18
 
19
  | Command | Purpose | Dependency Management |
20
  | ------- | ------- | --------------------- |
21
+ | `run` | Run a FastMCP server directly | **Supports:** Local files, factory functions, URLs, fastmcp.json configs, MCP configs. **Deps:** Uses your local environment directly. With `--python`, `--with`, `--project`, or `--with-requirements`: Runs via `uv run` subprocess. With fastmcp.json: Automatically manages dependencies based on configuration |
22
+ | `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 |
23
+ | `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 |
24
+ | `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
  | `version` | Display version information | N/A |
26
 
27
  ## `fastmcp run`
 
61
  2. **[Explicit server object](#explicit-server-object)**: `server.py:custom_name` - imports and uses the specified server object
62
  3. **[Factory function](#factory-function)**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
63
  4. **[Remote server proxy](#remote-server-proxy)**: `https://example.com/mcp-server` - connects to a remote server and creates a **local proxy server**
64
+ 5. **[FastMCP configuration file](#fastmcp-configuration)**: `fastmcp.json` - runs servers using FastMCP's declarative configuration format (auto-detects files in current directory)
65
+ 6. **MCP configuration file**: `mcp.json` - runs servers defined in a standard MCP configuration file
66
 
67
  <Warning>
68
  Note: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == "__main__"` block. This means:
 
159
  fastmcp run https://example.com/mcp
160
  ```
161
 
162
+ #### FastMCP Configuration
163
+ <VersionBadge version="2.11.4" />
164
+
165
+ FastMCP supports declarative configuration through `fastmcp.json` files. When you run `fastmcp run` without arguments, it automatically looks for a `fastmcp.json` file in the current directory:
166
+
167
+ ```bash
168
+ # Auto-detect fastmcp.json in current directory
169
+ fastmcp run
170
+
171
+ # Or explicitly specify a configuration file
172
+ fastmcp run my-config.fastmcp.json
173
+ ```
174
+
175
+ The configuration file handles dependencies, environment variables, and transport settings. Command-line arguments override configuration file values:
176
+
177
+ ```bash
178
+ # Override port from config file
179
+ fastmcp run fastmcp.json --port 8080
180
+ ```
181
+
182
+ See [Server Configuration](/deployment/server-configuration) for detailed documentation on fastmcp.json.
183
+
184
  #### MCP Configuration
185
 
186
  FastMCP can also run servers defined in a standard MCP configuration file. This is useful when you want to run multiple servers from a single file, or when you want to use a client that doesn't support direct connections to remote servers.
 
202
  ```
203
 
204
  <Tip>
205
+ This command always runs your server via `uv run` subprocess (never your local environment) to work with the MCP Inspector. Dependencies can be:
206
+ - Specified using `--with` and/or `--with-editable` options
207
+ - Defined in a `fastmcp.json` configuration file
208
+ - Available in a uv-managed project
209
+
210
+ When using `fastmcp.json`, the dev command automatically uses the configured dependencies.
211
  </Tip>
212
 
213
  <Warning>
 
242
 
243
  ### Entrypoints
244
 
245
+ The `dev` command supports local FastMCP server files and configuration:
246
 
247
  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.
248
  2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
249
  3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
250
+ 4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration (auto-detects in current directory)
251
 
252
  <Warning>
253
+ The `dev` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.
254
  </Warning>
255
 
256
  **Examples**
 
259
  # Run dev server with editable mode and additional packages
260
  fastmcp dev server.py -e . --with pandas --with matplotlib
261
 
262
+ # Run dev server with fastmcp.json configuration (auto-detects)
263
+ fastmcp dev
264
+
265
+ # Run dev server with explicit fastmcp.json file
266
+ fastmcp dev dev.fastmcp.json
267
+
268
  # Run dev server with specific Python version
269
  fastmcp dev server.py --python 3.11
270
 
 
321
 
322
  ### Entrypoints
323
 
324
+ The `install` command supports local FastMCP server files and configuration:
325
 
326
  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.
327
  2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
328
  3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
329
+ 4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration with dependencies and settings
330
 
331
  <Note>
332
+ Factory functions are particularly useful for install commands since they allow setup code to run that would otherwise be ignored when the MCP client runs your server. When using fastmcp.json, dependencies are automatically handled.
333
  </Note>
334
 
335
  <Warning>
336
+ The `install` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files. For remote servers, use your MCP client's native configuration.
337
  </Warning>
338
 
339
  **Examples**
 
342
  # Auto-detects server object (looks for 'mcp', 'server', or 'app')
343
  fastmcp install claude-desktop server.py
344
 
345
+ # Install with fastmcp.json configuration (auto-detects)
346
+ fastmcp install claude-desktop
347
+
348
+ # Install with explicit fastmcp.json file
349
+ fastmcp install claude-desktop my-config.fastmcp.json
350
+
351
  # Uses specific server object
352
  fastmcp install claude-desktop server.py:my_server
353
 
 
437
 
438
  ### Entrypoints
439
 
440
+ The `inspect` command supports local FastMCP server files and configuration:
441
 
442
  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.
443
  2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
444
  3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
445
+ 4. **FastMCP configuration**: `fastmcp.json` - inspects servers defined with FastMCP's declarative configuration
446
 
447
  <Warning>
448
+ The `inspect` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.
449
  </Warning>
450
 
451
  **Examples**
docs/schemas/fastmcp_config/latest.json ADDED
@@ -0,0 +1 @@
 
 
1
+ v1.json
docs/schemas/fastmcp_config/v1.json ADDED
@@ -0,0 +1 @@
 
 
1
+ ../../../src/fastmcp/utilities/fastmcp_config/v1/schema.json
docs/servers/server.mdx CHANGED
@@ -52,9 +52,6 @@ The `FastMCP` constructor accepts several arguments:
52
  A list of tools (or functions to convert to tools) to add to the server. In some cases, providing tools programmatically may be more convenient than using the `@mcp.tool` decorator
53
  </ParamField>
54
 
55
- <ParamField body="dependencies" type="list[str] | None">
56
- Optional server dependencies list with package specifications
57
- </ParamField>
58
 
59
  <ParamField body="include_tags" type="set[str] | None">
60
  Only expose components with at least one matching tag
@@ -202,13 +199,13 @@ if __name__ == "__main__":
202
  # This runs the server, defaulting to STDIO transport
203
  mcp.run()
204
 
205
- # To use a different transport, e.g., Streamable HTTP:
206
  # mcp.run(transport="http", host="127.0.0.1", port=9000)
207
  ```
208
 
209
  FastMCP supports several transport options:
210
  - STDIO (default, for local tools)
211
- - Streamable HTTP (recommended for web services)
212
  - SSE (legacy web transport, deprecated)
213
 
214
  The server can also be run using the FastMCP CLI.
@@ -319,7 +316,6 @@ from fastmcp import FastMCP
319
  # Configure server-specific settings
320
  mcp = FastMCP(
321
  name="ConfiguredServer",
322
- dependencies=["requests", "pandas>=2.0.0"], # Optional server dependencies
323
  include_tags={"public", "api"}, # Only expose these tagged components
324
  exclude_tags={"internal", "deprecated"}, # Hide these tagged components
325
  on_duplicate_tools="error", # Handle duplicate registrations
 
52
  A list of tools (or functions to convert to tools) to add to the server. In some cases, providing tools programmatically may be more convenient than using the `@mcp.tool` decorator
53
  </ParamField>
54
 
 
 
 
55
 
56
  <ParamField body="include_tags" type="set[str] | None">
57
  Only expose components with at least one matching tag
 
199
  # This runs the server, defaulting to STDIO transport
200
  mcp.run()
201
 
202
+ # To use a different transport, e.g., HTTP:
203
  # mcp.run(transport="http", host="127.0.0.1", port=9000)
204
  ```
205
 
206
  FastMCP supports several transport options:
207
  - STDIO (default, for local tools)
208
+ - HTTP (recommended for web services, uses Streamable HTTP protocol)
209
  - SSE (legacy web transport, deprecated)
210
 
211
  The server can also be run using the FastMCP CLI.
 
316
  # Configure server-specific settings
317
  mcp = FastMCP(
318
  name="ConfiguredServer",
 
319
  include_tags={"public", "api"}, # Only expose these tagged components
320
  exclude_tags={"internal", "deprecated"}, # Hide these tagged components
321
  on_duplicate_tools="error", # Handle duplicate registrations
examples/atproto_mcp/fastmcp.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "src/atproto_mcp/server.py",
4
+ "environment": {
5
+ "dependencies": [
6
+ "atproto_mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp"
7
+ ]
8
+ }
9
+ }
examples/atproto_mcp/src/atproto_mcp/server.py CHANGED
@@ -22,12 +22,7 @@ from atproto_mcp.types import (
22
  )
23
  from fastmcp import FastMCP
24
 
25
- atproto_mcp = FastMCP(
26
- "ATProto MCP Server",
27
- dependencies=[
28
- "atproto_mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp",
29
- ],
30
- )
31
 
32
 
33
  # Resources - read-only operations
 
22
  )
23
  from fastmcp import FastMCP
24
 
25
+ atproto_mcp = FastMCP("ATProto MCP Server")
 
 
 
 
 
26
 
27
 
28
  # Resources - read-only operations
examples/fastmcp_config/env_interpolation_example.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "src/server.py:app",
4
+ "environment": {
5
+ "python": "3.12",
6
+ "dependencies": ["fastmcp", "httpx", "pandas"]
7
+ },
8
+ "deployment": {
9
+ "transport": "http",
10
+ "host": "0.0.0.0",
11
+ "port": 8000,
12
+ "env": {
13
+ "API_BASE_URL": "https://api.${ENVIRONMENT}.example.com",
14
+ "DATABASE_URL": "postgres://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}",
15
+ "CACHE_PREFIX": "myapp_${ENVIRONMENT}_v1",
16
+ "LOG_LEVEL": "${LOG_LEVEL}",
17
+ "FEATURE_FLAGS": "${FEATURE_FLAGS}"
18
+ }
19
+ }
20
+ }
examples/fastmcp_config/fastmcp.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp/v1.json",
3
+ "entrypoint": "server.py",
4
+ "environment": {
5
+ "python": "3.12",
6
+ "dependencies": ["requests"]
7
+ },
8
+ "deployment": {
9
+ "transport": "stdio"
10
+ }
11
+ }
examples/fastmcp_config/full_example.fastmcp.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": {
4
+ "file": "server.py",
5
+ "object": "mcp"
6
+ },
7
+ "environment": {
8
+ "python": "3.12",
9
+ "dependencies": [
10
+ "requests>=2.31.0",
11
+ "httpx"
12
+ ],
13
+ "requirements": null,
14
+ "project": null,
15
+ "editable": null
16
+ },
17
+ "deployment": {
18
+ "transport": "http",
19
+ "host": "127.0.0.1",
20
+ "port": 8000,
21
+ "path": "/mcp/",
22
+ "log_level": "INFO",
23
+ "env": {
24
+ "DEBUG": "false",
25
+ "API_TIMEOUT": "30"
26
+ },
27
+ "cwd": null,
28
+ "args": null
29
+ }
30
+ }
examples/fastmcp_config/server.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example FastMCP server for demonstrating fastmcp.json configuration."""
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ # Create the FastMCP server instance
6
+ mcp = FastMCP("Config Example Server")
7
+
8
+
9
+ @mcp.tool
10
+ def echo(text: str) -> str:
11
+ """Echo the provided text back to the user."""
12
+ return f"You said: {text}"
13
+
14
+
15
+ @mcp.tool
16
+ def add(a: int, b: int) -> int:
17
+ """Add two numbers together."""
18
+ return a + b
19
+
20
+
21
+ @mcp.resource("config://example")
22
+ def get_example_config() -> str:
23
+ """Return an example configuration."""
24
+ return """
25
+ This server is configured using fastmcp.json.
26
+
27
+ The configuration file specifies:
28
+ - Python version
29
+ - Dependencies
30
+ - Transport settings
31
+ - Other runtime options
32
+ """
33
+
34
+
35
+ # This allows the server to run with: fastmcp run server.py
36
+ if __name__ == "__main__":
37
+ import asyncio
38
+
39
+ asyncio.run(mcp.run_async())
examples/fastmcp_config/simple.fastmcp.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp/v1.json",
3
+ "entrypoint": "server.py",
4
+ "deployment": {
5
+ "transport": "stdio"
6
+ }
7
+ }
examples/fastmcp_config_demo/README.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FastMCP Configuration Demo
2
+
3
+ This example demonstrates the recommended way to configure FastMCP servers using `fastmcp.json`.
4
+
5
+ ## Migration from Dependencies Parameter
6
+
7
+ Previously (deprecated as of FastMCP 2.11.4), you would specify dependencies in the Python code:
8
+
9
+ ```python
10
+ mcp = FastMCP("Demo Server", dependencies=["pyautogui", "Pillow"])
11
+ ```
12
+
13
+ Now, dependencies are declared in `fastmcp.json`:
14
+
15
+ ```json
16
+ {
17
+ "environment": {
18
+ "dependencies": ["pyautogui", "Pillow"]
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Running the Server
24
+
25
+ With the configuration file in place, you can run the server in several ways:
26
+
27
+ ```bash
28
+ # Auto-detect fastmcp.json in current directory
29
+ cd examples/fastmcp_config_demo
30
+ fastmcp run
31
+
32
+ # Or specify the config file explicitly
33
+ fastmcp run examples/fastmcp_config_demo/fastmcp.json
34
+
35
+ # Or use development mode with the Inspector UI
36
+ fastmcp dev examples/fastmcp_config_demo/fastmcp.json
37
+ ```
38
+
39
+ ## Benefits
40
+
41
+ - **Single source of truth**: All configuration in one place
42
+ - **Environment isolation**: Dependencies are installed in an isolated UV environment
43
+ - **No import-time issues**: Dependencies are installed before the server is imported
44
+ - **IDE support**: JSON schema provides autocomplete and validation
45
+ - **Shareable**: Easy to share complete server configuration with others
46
+
47
+ ## Configuration Structure
48
+
49
+ The `fastmcp.json` file supports three main sections:
50
+
51
+ 1. **entrypoint** (required): The Python file containing your server
52
+ 2. **environment** (optional): Python version and dependencies
53
+ 3. **deployment** (optional): Runtime settings like transport and logging
54
+
55
+ See the [full documentation](https://gofastmcp.com/docs/deployment/server-configuration) for more details.
examples/fastmcp_config_demo/fastmcp.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "server.py",
4
+ "environment": {
5
+ "python": "3.11",
6
+ "dependencies": [
7
+ "pyautogui",
8
+ "Pillow"
9
+ ]
10
+ },
11
+ "deployment": {
12
+ "transport": "stdio",
13
+ "log_level": "INFO"
14
+ }
15
+ }
examples/fastmcp_config_demo/server.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Example server demonstrating fastmcp.json configuration.
3
+
4
+ This server previously would have used the deprecated dependencies parameter:
5
+ mcp = FastMCP("Demo Server", dependencies=["pyautogui", "Pillow"])
6
+
7
+ Now dependencies are declared in fastmcp.json alongside this file.
8
+ """
9
+
10
+ import io
11
+
12
+ from fastmcp import FastMCP
13
+ from fastmcp.utilities.types import Image
14
+
15
+ # Create server - dependencies are now in fastmcp.json
16
+ mcp = FastMCP("Screenshot Demo")
17
+
18
+
19
+ @mcp.tool
20
+ def take_screenshot() -> Image:
21
+ """
22
+ Take a screenshot of the user's screen and return it as an image.
23
+
24
+ Use this tool anytime the user wants you to look at something on their screen.
25
+ """
26
+ import pyautogui
27
+
28
+ buffer = io.BytesIO()
29
+
30
+ # Capture and compress the screenshot to stay under size limits
31
+ screenshot = pyautogui.screenshot()
32
+ screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True)
33
+
34
+ return Image(data=buffer.getvalue(), format="jpeg")
35
+
36
+
37
+ @mcp.tool
38
+ def analyze_colors() -> dict:
39
+ """
40
+ Analyze the dominant colors in the current screen.
41
+
42
+ Returns a dictionary with color statistics from the screen.
43
+ """
44
+ import pyautogui
45
+ from PIL import Image as PILImage
46
+
47
+ screenshot = pyautogui.screenshot()
48
+ # Convert to smaller size for faster analysis
49
+ small = screenshot.resize((100, 100), PILImage.Resampling.LANCZOS)
50
+
51
+ # Get colors
52
+ colors = small.getcolors(maxcolors=10000)
53
+ if not colors:
54
+ return {"error": "Too many colors to analyze"}
55
+
56
+ # Sort by frequency
57
+ sorted_colors = sorted(colors, key=lambda x: x[0], reverse=True)[:10]
58
+
59
+ return {
60
+ "top_colors": [
61
+ {"count": count, "rgb": color} for count, color in sorted_colors
62
+ ],
63
+ "total_pixels": sum(c[0] for c in colors),
64
+ }
65
+
66
+
67
+ if __name__ == "__main__":
68
+ import asyncio
69
+
70
+ asyncio.run(mcp.run_async())
examples/memory.fastmcp.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "memory.py",
4
+ "environment": {
5
+ "dependencies": [
6
+ "pydantic-ai-slim[openai]",
7
+ "asyncpg",
8
+ "numpy",
9
+ "pgvector"
10
+ ]
11
+ }
12
+ }
examples/mount_example.fastmcp.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "mount_example.py"
4
+ }
examples/mount_example.py CHANGED
@@ -54,9 +54,7 @@ async def news_data():
54
 
55
 
56
  # Main application
57
- app = FastMCP(
58
- "Main App", dependencies=["fastmcp@git+https://github.com/jlowin/fastmcp.git"]
59
- )
60
 
61
 
62
  @app.tool
 
54
 
55
 
56
  # Main application
57
+ app = FastMCP("Main App")
 
 
58
 
59
 
60
  @app.tool
examples/screenshot.fastmcp.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "screenshot.py",
4
+ "environment": {
5
+ "dependencies": ["pyautogui", "Pillow"]
6
+ }
7
+ }
examples/smart_home/hub.fastmcp.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "src/smart_home/hub.py",
4
+ "environment": {
5
+ "dependencies": [
6
+ "smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
7
+ ]
8
+ }
9
+ }
examples/smart_home/lights.fastmcp.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
3
+ "entrypoint": "src/smart_home/lights/server.py",
4
+ "environment": {
5
+ "dependencies": [
6
+ "smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
7
+ ]
8
+ }
9
+ }
src/fastmcp/cli/cli.py CHANGED
@@ -139,7 +139,7 @@ def version(
139
 
140
  @app.command
141
  async def dev(
142
- server_spec: str,
143
  *,
144
  with_editable: Annotated[
145
  Path | None,
@@ -202,8 +202,57 @@ async def dev(
202
  """Run an MCP server with the MCP Inspector for development.
203
 
204
  Args:
205
- server_spec: Python file to run, optionally with :object suffix
206
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  file, server_object = run_module.parse_file_path(server_spec)
208
 
209
  logger.debug(
@@ -220,8 +269,18 @@ async def dev(
220
 
221
  try:
222
  # Import server to get dependencies
 
223
  server: FastMCP = await run_module.import_server(file, server_object)
224
- if server.dependencies is not None:
 
 
 
 
 
 
 
 
 
225
  with_packages = list(set(with_packages + server.dependencies))
226
 
227
  env_vars = {}
@@ -284,7 +343,7 @@ async def dev(
284
 
285
  @app.command
286
  async def run(
287
- server_spec: str,
288
  *server_args: str,
289
  transport: Annotated[
290
  run_module.TransportType | None,
@@ -361,18 +420,81 @@ async def run(
361
  ) -> None:
362
  """Run an MCP server or connect to a remote one.
363
 
364
- The server can be specified in four ways:
365
  1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
366
  2. Import approach: "server.py:app" - imports and runs the specified server object
367
  3. URL approach: "http://server-url" - connects to a remote server and creates a proxy
368
  4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
 
 
369
 
370
  Server arguments can be passed after -- :
371
  fastmcp run server.py -- --config config.json --debug
372
 
373
  Args:
374
- server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
375
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  logger.debug(
377
  "Running server or client",
378
  extra={
@@ -386,8 +508,14 @@ async def run(
386
  },
387
  )
388
 
389
- # If any uv-specific options are provided, use uv run
390
- if python or with_packages or with_requirements or project:
 
 
 
 
 
 
391
  try:
392
  run_module.run_with_uv(
393
  server_spec=server_spec,
@@ -437,7 +565,7 @@ async def run(
437
 
438
  @app.command
439
  async def inspect(
440
- server_spec: str,
441
  *,
442
  output: Annotated[
443
  Path,
@@ -446,6 +574,35 @@ async def inspect(
446
  help="Output file path for the JSON report (default: server-info.json)",
447
  ),
448
  ] = Path("server-info.json"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  ) -> None:
450
  """Inspect an MCP server and generate a JSON report.
451
 
@@ -458,10 +615,104 @@ async def inspect(
458
  fastmcp inspect server.py -o report.json
459
  fastmcp inspect server.py:mcp -o analysis.json
460
  fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
 
 
461
 
462
  Args:
463
- server_spec: Python file to inspect, optionally with :object suffix
464
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  # Parse the server specification
466
  file, server_object = run_module.parse_file_path(server_spec)
467
 
@@ -514,6 +765,43 @@ async def inspect(
514
  sys.exit(1)
515
 
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  # Add install subcommands using proper Cyclopts pattern
518
  app.command(install_app)
519
 
 
139
 
140
  @app.command
141
  async def dev(
142
+ server_spec: str | None = None,
143
  *,
144
  with_editable: Annotated[
145
  Path | None,
 
202
  """Run an MCP server with the MCP Inspector for development.
203
 
204
  Args:
205
+ server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
206
  """
207
+ # Auto-detect fastmcp.json if no server_spec provided
208
+ if server_spec is None:
209
+ from pathlib import Path
210
+
211
+ from fastmcp.utilities.fastmcp_config import FastMCPConfig
212
+
213
+ config_path = Path("fastmcp.json")
214
+ if not config_path.exists():
215
+ # Check if fastmcp.json exists in current directory
216
+ found_config = FastMCPConfig.find_config()
217
+ if found_config:
218
+ config_path = found_config
219
+ else:
220
+ logger.error(
221
+ "No server specification provided and no fastmcp.json found in current directory.\n"
222
+ "Please specify a server file or create a fastmcp.json configuration."
223
+ )
224
+ sys.exit(1)
225
+
226
+ # Load the config to get settings
227
+ config = FastMCPConfig.from_file(config_path)
228
+ entrypoint = config.get_entrypoint(config_path)
229
+
230
+ # Convert entrypoint to string format for dev command
231
+ if entrypoint.object:
232
+ server_spec = f"{entrypoint.file}:{entrypoint.object}"
233
+ else:
234
+ server_spec = entrypoint.file
235
+
236
+ # Merge environment settings with CLI args (CLI takes precedence)
237
+ if config.environment:
238
+ merged_env = config.environment.merge_with_cli_args(
239
+ python=python,
240
+ with_packages=with_packages,
241
+ with_requirements=with_requirements,
242
+ project=project,
243
+ with_editable=with_editable,
244
+ )
245
+ python = merged_env["python"]
246
+ with_packages = merged_env["with_packages"]
247
+ with_requirements = merged_env["with_requirements"]
248
+ project = merged_env["project"]
249
+ with_editable = merged_env["with_editable"]
250
+
251
+ # Get server port from deployment config if not specified
252
+ if config.deployment and config.deployment.port:
253
+ server_port = server_port or config.deployment.port
254
+
255
+ logger.info(f"Using configuration from {config_path}")
256
  file, server_object = run_module.parse_file_path(server_spec)
257
 
258
  logger.debug(
 
269
 
270
  try:
271
  # Import server to get dependencies
272
+ # TODO: Remove dependencies handling (deprecated in v2.11.4)
273
  server: FastMCP = await run_module.import_server(file, server_object)
274
+ if server.dependencies:
275
+ import warnings
276
+
277
+ warnings.warn(
278
+ f"Server '{server.name}' uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). "
279
+ "Please migrate to fastmcp.json configuration file. "
280
+ "See https://gofastmcp.com/docs/deployment/server-configuration for details.",
281
+ DeprecationWarning,
282
+ stacklevel=2,
283
+ )
284
  with_packages = list(set(with_packages + server.dependencies))
285
 
286
  env_vars = {}
 
343
 
344
  @app.command
345
  async def run(
346
+ server_spec: str | None = None,
347
  *server_args: str,
348
  transport: Annotated[
349
  run_module.TransportType | None,
 
420
  ) -> None:
421
  """Run an MCP server or connect to a remote one.
422
 
423
+ The server can be specified in several ways:
424
  1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
425
  2. Import approach: "server.py:app" - imports and runs the specified server object
426
  3. URL approach: "http://server-url" - connects to a remote server and creates a proxy
427
  4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
428
+ 5. FastMCP config: "fastmcp.json" - runs server using FastMCP configuration
429
+ 6. No argument: looks for fastmcp.json in current directory
430
 
431
  Server arguments can be passed after -- :
432
  fastmcp run server.py -- --config config.json --debug
433
 
434
  Args:
435
+ server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
436
  """
437
+ # Load configuration if needed
438
+ from pathlib import Path
439
+
440
+ from fastmcp.utilities.fastmcp_config import FastMCPConfig
441
+
442
+ config = None
443
+ config_path = None
444
+
445
+ # Auto-detect fastmcp.json if no server_spec provided
446
+ if server_spec is None:
447
+ config_path = Path("fastmcp.json")
448
+ if not config_path.exists():
449
+ # Check if fastmcp.json exists in current directory
450
+ found_config = FastMCPConfig.find_config()
451
+ if found_config:
452
+ config_path = found_config
453
+ else:
454
+ logger.error(
455
+ "No server specification provided and no fastmcp.json found in current directory.\n"
456
+ "Please specify a server file or create a fastmcp.json configuration."
457
+ )
458
+ sys.exit(1)
459
+
460
+ server_spec = str(config_path)
461
+ logger.info(f"Using configuration from {config_path}")
462
+
463
+ # Load config if server_spec is a fastmcp.json file
464
+ if server_spec.endswith("fastmcp.json"):
465
+ config_path = Path(server_spec)
466
+ if config_path.exists():
467
+ config = FastMCPConfig.from_file(config_path)
468
+
469
+ # Merge deployment config with CLI values (CLI takes precedence)
470
+ if config.deployment:
471
+ merged_deploy = config.deployment.merge_with_cli_args(
472
+ transport=transport,
473
+ host=host,
474
+ port=port,
475
+ path=path,
476
+ log_level=log_level,
477
+ server_args=list(server_args) if server_args else None,
478
+ )
479
+ transport = merged_deploy["transport"]
480
+ host = merged_deploy["host"]
481
+ port = merged_deploy["port"]
482
+ path = merged_deploy["path"]
483
+ log_level = merged_deploy["log_level"]
484
+ server_args = merged_deploy["server_args"] or ()
485
+
486
+ # Merge environment config with CLI values (CLI takes precedence)
487
+ if config.environment:
488
+ merged_env = config.environment.merge_with_cli_args(
489
+ python=python,
490
+ with_packages=with_packages,
491
+ with_requirements=with_requirements,
492
+ project=project,
493
+ )
494
+ python = merged_env["python"]
495
+ with_packages = merged_env["with_packages"]
496
+ with_requirements = merged_env["with_requirements"]
497
+ project = merged_env["project"]
498
  logger.debug(
499
  "Running server or client",
500
  extra={
 
508
  },
509
  )
510
 
511
+ # Check if we need to use uv run (either from CLI args or config)
512
+ needs_uv = python or with_packages or with_requirements or project
513
+ if not needs_uv and config and config.environment:
514
+ # Check if config's environment needs uv
515
+ needs_uv = config.environment.needs_uv()
516
+
517
+ if needs_uv:
518
+ # Use uv run subprocess - always use run_with_uv which handles output correctly
519
  try:
520
  run_module.run_with_uv(
521
  server_spec=server_spec,
 
565
 
566
  @app.command
567
  async def inspect(
568
+ server_spec: str | None = None,
569
  *,
570
  output: Annotated[
571
  Path,
 
574
  help="Output file path for the JSON report (default: server-info.json)",
575
  ),
576
  ] = Path("server-info.json"),
577
+ python: Annotated[
578
+ str | None,
579
+ cyclopts.Parameter(
580
+ "--python",
581
+ help="Python version to use (e.g., 3.10, 3.11)",
582
+ ),
583
+ ] = None,
584
+ with_packages: Annotated[
585
+ list[str],
586
+ cyclopts.Parameter(
587
+ "--with",
588
+ help="Additional packages to install (can be used multiple times)",
589
+ negative=False,
590
+ ),
591
+ ] = [],
592
+ project: Annotated[
593
+ Path | None,
594
+ cyclopts.Parameter(
595
+ "--project",
596
+ help="Run the command within the given project directory",
597
+ ),
598
+ ] = None,
599
+ with_requirements: Annotated[
600
+ Path | None,
601
+ cyclopts.Parameter(
602
+ "--with-requirements",
603
+ help="Requirements file to install dependencies from",
604
+ ),
605
+ ] = None,
606
  ) -> None:
607
  """Inspect an MCP server and generate a JSON report.
608
 
 
615
  fastmcp inspect server.py -o report.json
616
  fastmcp inspect server.py:mcp -o analysis.json
617
  fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
618
+ fastmcp inspect fastmcp.json
619
+ fastmcp inspect # auto-detect fastmcp.json
620
 
621
  Args:
622
+ server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
623
  """
624
+ # Load configuration if needed
625
+ from pathlib import Path
626
+
627
+ from fastmcp.utilities.fastmcp_config import FastMCPConfig
628
+
629
+ config = None
630
+ config_path = None
631
+
632
+ # Auto-detect fastmcp.json if no server_spec provided
633
+ if server_spec is None:
634
+ config_path = Path("fastmcp.json")
635
+ if not config_path.exists():
636
+ # Check if fastmcp.json exists in current directory
637
+ found_config = FastMCPConfig.find_config()
638
+ if found_config:
639
+ config_path = found_config
640
+ else:
641
+ logger.error(
642
+ "No server specification provided and no fastmcp.json found in current directory.\n"
643
+ "Please specify a server file or create a fastmcp.json configuration."
644
+ )
645
+ sys.exit(1)
646
+
647
+ server_spec = str(config_path)
648
+ logger.info(f"Using configuration from {config_path}")
649
+
650
+ # Load config if server_spec is a fastmcp.json file
651
+ if server_spec.endswith("fastmcp.json"):
652
+ config_path = Path(server_spec)
653
+ if config_path.exists():
654
+ config = FastMCPConfig.from_file(config_path)
655
+ # Get the actual entrypoint with resolved paths
656
+ entrypoint = config.get_entrypoint(config_path)
657
+
658
+ if entrypoint.object:
659
+ server_spec = f"{entrypoint.file}:{entrypoint.object}"
660
+ else:
661
+ server_spec = entrypoint.file
662
+
663
+ # Merge environment settings from config with CLI (CLI takes precedence)
664
+ if config.environment:
665
+ merged_env = config.environment.merge_with_cli_args(
666
+ python=python,
667
+ with_packages=with_packages,
668
+ with_requirements=with_requirements,
669
+ project=project,
670
+ )
671
+ python = merged_env["python"]
672
+ with_packages = merged_env["with_packages"]
673
+ with_requirements = merged_env["with_requirements"]
674
+ project = merged_env["project"]
675
+
676
+ # Check if we need to use uv run
677
+ needs_uv = python or with_packages or with_requirements or project
678
+ if not needs_uv and config and config.environment:
679
+ needs_uv = config.environment.needs_uv()
680
+
681
+ if needs_uv:
682
+ # Build and run uv command
683
+ if config and config.environment:
684
+ # Use environment config's run_with_uv method
685
+ inspect_command = [
686
+ "fastmcp",
687
+ "inspect",
688
+ server_spec,
689
+ "--output",
690
+ str(output),
691
+ ]
692
+ config.environment.run_with_uv(inspect_command)
693
+ else:
694
+ # Build an EnvironmentConfig from CLI args for consistency
695
+ from fastmcp.utilities.fastmcp_config import (
696
+ EnvironmentConfig,
697
+ )
698
+
699
+ env_config = EnvironmentConfig(
700
+ python=python,
701
+ dependencies=with_packages,
702
+ requirements=str(with_requirements) if with_requirements else None,
703
+ project=str(project) if project else None,
704
+ )
705
+
706
+ inspect_command = [
707
+ "fastmcp",
708
+ "inspect",
709
+ server_spec,
710
+ "--output",
711
+ str(output),
712
+ ]
713
+ env_config.run_with_uv(inspect_command)
714
+
715
+ # Direct import path (no uv needed)
716
  # Parse the server specification
717
  file, server_object = run_module.parse_file_path(server_spec)
718
 
 
765
  sys.exit(1)
766
 
767
 
768
+ @app.command
769
+ def generate_schema(
770
+ *,
771
+ output: Annotated[
772
+ Path | None,
773
+ cyclopts.Parameter(
774
+ name=["--output", "-o"],
775
+ help="Output file path for the JSON schema",
776
+ ),
777
+ ] = None,
778
+ ) -> None:
779
+ """Generate JSON schema for fastmcp.json configuration files.
780
+
781
+ This generates a JSON schema that can be used by IDEs and validators
782
+ to provide auto-completion and validation for fastmcp.json files.
783
+
784
+ Examples:
785
+ fastmcp generate-schema
786
+ fastmcp generate-schema -o schema.json
787
+ """
788
+ import json
789
+
790
+ from fastmcp.utilities.fastmcp_config import (
791
+ generate_schema as gen_schema,
792
+ )
793
+
794
+ schema = gen_schema()
795
+ schema_json = json.dumps(schema, indent=2)
796
+
797
+ if output:
798
+ output.parent.mkdir(parents=True, exist_ok=True)
799
+ output.write_text(schema_json)
800
+ logger.info(f"Schema written to {output}")
801
+ else:
802
+ console.print(schema_json)
803
+
804
+
805
  # Add install subcommands using proper Cyclopts pattern
806
  app.command(install_app)
807
 
src/fastmcp/cli/install/shared.py CHANGED
@@ -30,9 +30,35 @@ async def process_common_args(
30
  env_vars: list[str],
31
  env_file: Path | None,
32
  ) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
33
- """Process common arguments shared by all install commands."""
34
- # Parse server spec
35
- file, server_object = parse_file_path(server_spec)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  logger.debug(
38
  "Installing server",
@@ -59,8 +85,18 @@ async def process_common_args(
59
  name = file.stem
60
 
61
  # Get server dependencies if available
 
62
  server_dependencies = getattr(server, "dependencies", []) if server else []
63
  if server_dependencies:
 
 
 
 
 
 
 
 
 
64
  with_packages = list(set(with_packages + server_dependencies))
65
 
66
  # Process environment variables if provided
 
30
  env_vars: list[str],
31
  env_file: Path | None,
32
  ) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
33
+ """Process common arguments shared by all install commands.
34
+
35
+ Handles both fastmcp.json config files and traditional file.py:object syntax.
36
+ """
37
+ # Check if server_spec is a fastmcp.json file
38
+ if server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name:
39
+ from fastmcp.utilities.fastmcp_config import FastMCPConfig
40
+
41
+ config_path = Path(server_spec).resolve()
42
+ if not config_path.exists():
43
+ print(f"[red]Configuration file not found: {config_path}[/red]")
44
+ sys.exit(1)
45
+
46
+ # Load config and get entrypoint
47
+ config = FastMCPConfig.from_file(config_path)
48
+ entrypoint = config.get_entrypoint(config_path)
49
+
50
+ # Convert to file and server_object
51
+ file = Path(entrypoint.file)
52
+ server_object = entrypoint.object
53
+
54
+ # Merge packages from config if not overridden
55
+ if config.environment and config.environment.dependencies:
56
+ # Merge with CLI packages (CLI takes precedence)
57
+ config_packages = config.environment.dependencies or []
58
+ with_packages = list(set(with_packages + config_packages))
59
+ else:
60
+ # Parse traditional server spec
61
+ file, server_object = parse_file_path(server_spec)
62
 
63
  logger.debug(
64
  "Installing server",
 
85
  name = file.stem
86
 
87
  # Get server dependencies if available
88
+ # TODO: Remove dependencies handling (deprecated in v2.11.4)
89
  server_dependencies = getattr(server, "dependencies", []) if server else []
90
  if server_dependencies:
91
+ import warnings
92
+
93
+ warnings.warn(
94
+ "Server uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). "
95
+ "Please migrate to fastmcp.json configuration file. "
96
+ "See https://gofastmcp.com/docs/deployment/server-configuration for details.",
97
+ DeprecationWarning,
98
+ stacklevel=2,
99
+ )
100
  with_packages = list(set(with_packages + server_dependencies))
101
 
102
  # Process environment variables if provided
src/fastmcp/cli/run.py CHANGED
@@ -13,6 +13,12 @@ from typing import Any, Literal
13
  from mcp.server.fastmcp import FastMCP as FastMCP1x
14
 
15
  from fastmcp.server.server import FastMCP
 
 
 
 
 
 
16
  from fastmcp.utilities.logging import get_logger
17
 
18
  logger = get_logger("cli.run")
@@ -89,12 +95,13 @@ async def import_server(file: Path, server_or_factory: str | None = None) -> Any
89
  for name in ["mcp", "server", "app"]:
90
  if hasattr(module, name):
91
  obj = getattr(module, name)
92
- return await _resolve_server_or_factory(obj, file, name)
 
93
 
94
  logger.error(
95
  f"No server object found in {file}. Please either:\n"
96
  "1. Use a standard variable name (mcp, server, or app)\n"
97
- "2. Specify the object name with file:object syntax",
98
  extra={"file": str(file)},
99
  )
100
  sys.exit(1)
@@ -187,7 +194,7 @@ def run_with_uv(
187
  """Run a MCP server using uv run subprocess.
188
 
189
  Args:
190
- server_spec: Python file, object specification (file:obj), or URL
191
  python_version: Python version to use (e.g. "3.10")
192
  with_packages: Additional packages to install
193
  with_requirements: Requirements file to use
@@ -199,6 +206,55 @@ def run_with_uv(
199
  log_level: Log level
200
  show_banner: Whether to show the server banner
201
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  cmd = ["uv", "run"]
203
 
204
  # Add Python version if specified
@@ -280,6 +336,48 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
280
  return server
281
 
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  async def import_server_with_args(
284
  file: Path,
285
  server_or_factory: str | None = None,
@@ -320,7 +418,7 @@ async def run_command(
320
  """Run a MCP server or connect to a remote one.
321
 
322
  Args:
323
- server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
324
  transport: Transport protocol to use
325
  host: Host to bind to when using http transport
326
  port: Port to bind to when using http transport
@@ -334,7 +432,38 @@ async def run_command(
334
  # Handle URL case
335
  server = create_client_server(server_spec)
336
  logger.debug(f"Created client proxy server for {server_spec}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  elif server_spec.endswith(".json"):
 
338
  server = create_mcp_config_server(Path(server_spec))
339
  else:
340
  # Handle file case
@@ -358,8 +487,8 @@ async def run_command(
358
  kwargs["port"] = port
359
  if path:
360
  kwargs["path"] = path
361
- if log_level:
362
- kwargs["log_level"] = log_level
363
 
364
  if not show_banner:
365
  kwargs["show_banner"] = False
 
13
  from mcp.server.fastmcp import FastMCP as FastMCP1x
14
 
15
  from fastmcp.server.server import FastMCP
16
+ from fastmcp.utilities.fastmcp_config import (
17
+ DeploymentConfig,
18
+ EntrypointConfig,
19
+ EnvironmentConfig,
20
+ FastMCPConfig,
21
+ )
22
  from fastmcp.utilities.logging import get_logger
23
 
24
  logger = get_logger("cli.run")
 
95
  for name in ["mcp", "server", "app"]:
96
  if hasattr(module, name):
97
  obj = getattr(module, name)
98
+ if isinstance(obj, FastMCP | FastMCP1x):
99
+ return await _resolve_server_or_factory(obj, file, name)
100
 
101
  logger.error(
102
  f"No server object found in {file}. Please either:\n"
103
  "1. Use a standard variable name (mcp, server, or app)\n"
104
+ "2. Specify the object name in fastmcp.json or use `file.py:object` syntax as your path.",
105
  extra={"file": str(file)},
106
  )
107
  sys.exit(1)
 
194
  """Run a MCP server using uv run subprocess.
195
 
196
  Args:
197
+ server_spec: Python file, object specification (file:obj), config file, or URL
198
  python_version: Python version to use (e.g. "3.10")
199
  with_packages: Additional packages to install
200
  with_requirements: Requirements file to use
 
206
  log_level: Log level
207
  show_banner: Whether to show the server banner
208
  """
209
+ # Check if server_spec is a fastmcp.json file
210
+ if server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name:
211
+ config_path = Path(server_spec).resolve() # Get absolute path
212
+ if config_path.exists():
213
+ # Load config
214
+ config = FastMCPConfig.from_file(config_path)
215
+
216
+ # Get entrypoint with resolved paths
217
+ entrypoint = config.get_entrypoint(config_path)
218
+ if entrypoint.object:
219
+ server_spec = f"{entrypoint.file}:{entrypoint.object}"
220
+ else:
221
+ server_spec = entrypoint.file
222
+
223
+ # Merge environment config with CLI args
224
+ # Check if environment has any non-None values
225
+ if config.environment and any(
226
+ getattr(config.environment, field, None) is not None
227
+ for field in EnvironmentConfig.model_fields
228
+ ):
229
+ merged_env = config.environment.merge_with_cli_args(
230
+ python=python_version,
231
+ with_packages=with_packages,
232
+ with_requirements=with_requirements,
233
+ project=project,
234
+ )
235
+ python_version = merged_env["python"]
236
+ with_packages = merged_env["with_packages"]
237
+ with_requirements = merged_env["with_requirements"]
238
+ project = merged_env["project"]
239
+
240
+ # Merge deployment config with CLI args
241
+ # Check if deployment has any non-None values
242
+ if config.deployment and any(
243
+ getattr(config.deployment, field, None) is not None
244
+ for field in DeploymentConfig.model_fields
245
+ ):
246
+ merged_deploy = config.deployment.merge_with_cli_args(
247
+ transport=transport,
248
+ host=host,
249
+ port=port,
250
+ path=path,
251
+ log_level=log_level,
252
+ )
253
+ transport = merged_deploy["transport"]
254
+ host = merged_deploy["host"]
255
+ port = merged_deploy["port"]
256
+ path = merged_deploy["path"]
257
+ log_level = merged_deploy["log_level"]
258
  cmd = ["uv", "run"]
259
 
260
  # Add Python version if specified
 
336
  return server
337
 
338
 
339
+ def load_fastmcp_config(
340
+ config_path: Path,
341
+ ) -> tuple[EntrypointConfig, DeploymentConfig | None, EnvironmentConfig | None]:
342
+ """Load a FastMCP configuration from a fastmcp.json file.
343
+
344
+ Args:
345
+ config_path: Path to fastmcp.json file
346
+
347
+ Returns:
348
+ Tuple of (entrypoint, deployment config, environment config)
349
+ """
350
+ config = FastMCPConfig.from_file(config_path)
351
+
352
+ # Apply runtime settings from deployment config
353
+ if config.deployment:
354
+ config.deployment.apply_runtime_settings(config_path)
355
+
356
+ # Get entrypoint as structured object with resolved paths
357
+ entrypoint = config.get_entrypoint(config_path)
358
+
359
+ # Return None for empty configs (backward compatibility)
360
+ deployment = (
361
+ config.deployment
362
+ if any(
363
+ getattr(config.deployment, field, None) is not None
364
+ for field in DeploymentConfig.model_fields
365
+ )
366
+ else None
367
+ )
368
+
369
+ environment = (
370
+ config.environment
371
+ if any(
372
+ getattr(config.environment, field, None) is not None
373
+ for field in EnvironmentConfig.model_fields
374
+ )
375
+ else None
376
+ )
377
+
378
+ return entrypoint, deployment, environment
379
+
380
+
381
  async def import_server_with_args(
382
  file: Path,
383
  server_or_factory: str | None = None,
 
418
  """Run a MCP server or connect to a remote one.
419
 
420
  Args:
421
+ server_spec: Python file, object specification (file:obj), config file, or URL
422
  transport: Transport protocol to use
423
  host: Host to bind to when using http transport
424
  port: Port to bind to when using http transport
 
432
  # Handle URL case
433
  server = create_client_server(server_spec)
434
  logger.debug(f"Created client proxy server for {server_spec}")
435
+ elif (
436
+ server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name
437
+ ):
438
+ # Handle fastmcp.json configuration file (matches test_fastmcp.json, my.fastmcp.json, etc)
439
+ config_path = Path(server_spec)
440
+ entrypoint, deployment, environment = load_fastmcp_config(config_path)
441
+
442
+ # Merge deployment config with CLI arguments (CLI takes precedence)
443
+ if deployment:
444
+ merged = deployment.merge_with_cli_args(
445
+ transport=transport,
446
+ host=host,
447
+ port=port,
448
+ path=path,
449
+ log_level=log_level,
450
+ server_args=server_args,
451
+ )
452
+ transport = merged["transport"]
453
+ host = merged["host"]
454
+ port = merged["port"]
455
+ path = merged["path"]
456
+ log_level = merged["log_level"]
457
+ server_args = merged["server_args"]
458
+
459
+ # Import the server from the structured entrypoint
460
+ file_path = Path(entrypoint.file)
461
+ server = await import_server_with_args(
462
+ file_path, entrypoint.object, server_args
463
+ )
464
+ logger.debug(f'Found server "{server.name}" from config {config_path}')
465
  elif server_spec.endswith(".json"):
466
+ # Handle other JSON files as MCPConfig
467
  server = create_mcp_config_server(Path(server_spec))
468
  else:
469
  # Handle file case
 
487
  kwargs["port"] = port
488
  if path:
489
  kwargs["path"] = path
490
+ # Note: log_level is not currently supported by run_async
491
+ # TODO: Add log_level support to server.run_async
492
 
493
  if not show_banner:
494
  kwargs["show_banner"] = False
src/fastmcp/client/transports.py CHANGED
@@ -21,6 +21,9 @@ from mcp.client.session import (
21
  MessageHandlerFnT,
22
  SamplingFnT,
23
  )
 
 
 
24
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
25
  from mcp.shared._httpx_utils import McpHttpClientFactory
26
  from mcp.shared.memory import create_client_server_memory_streams
@@ -33,6 +36,7 @@ from fastmcp.client.auth.oauth import OAuth
33
  from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
34
  from fastmcp.server.dependencies import get_http_headers
35
  from fastmcp.server.server import FastMCP
 
36
  from fastmcp.utilities.logging import get_logger
37
 
38
  logger = get_logger(__name__)
@@ -192,8 +196,6 @@ class SSETransport(ClientTransport):
192
  async def connect_session(
193
  self, **session_kwargs: Unpack[SessionKwargs]
194
  ) -> AsyncIterator[ClientSession]:
195
- from mcp.client.sse import sse_client
196
-
197
  client_kwargs: dict[str, Any] = {}
198
 
199
  # load headers from an active HTTP request, if available. This will only be true
@@ -264,8 +266,6 @@ class StreamableHttpTransport(ClientTransport):
264
  async def connect_session(
265
  self, **session_kwargs: Unpack[SessionKwargs]
266
  ) -> AsyncIterator[ClientSession]:
267
- from mcp.client.streamable_http import streamablehttp_client
268
-
269
  client_kwargs: dict[str, Any] = {}
270
 
271
  # load headers from an active HTTP request, if available. This will only be true
@@ -429,8 +429,6 @@ async def _stdio_transport_connect_task(
429
  """A standalone connection task for a stdio transport. It is not a part of the StdioTransport class
430
  to ensure that the connection task does not hold a reference to the Transport object."""
431
 
432
- from mcp.client.stdio import stdio_client
433
-
434
  try:
435
  async with contextlib.AsyncExitStack() as stack:
436
  try:
@@ -598,16 +596,38 @@ class UvStdioTransport(StdioTransport):
598
  f"Project directory not found: {project_directory}"
599
  )
600
 
601
- # Build uv arguments
602
- uv_args: list[str] = ["run"]
603
- if project_directory:
604
- uv_args.extend(["--directory", str(project_directory)])
605
- if python_version:
606
- uv_args.extend(["--python", python_version])
607
- for pkg in with_packages or []:
608
- uv_args.extend(["--with", pkg])
609
- if with_requirements:
610
- uv_args.extend(["--with-requirements", str(with_requirements)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
  if module:
612
  uv_args.append("--module")
613
 
 
21
  MessageHandlerFnT,
22
  SamplingFnT,
23
  )
24
+ from mcp.client.sse import sse_client
25
+ from mcp.client.stdio import stdio_client
26
+ from mcp.client.streamable_http import streamablehttp_client
27
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
28
  from mcp.shared._httpx_utils import McpHttpClientFactory
29
  from mcp.shared.memory import create_client_server_memory_streams
 
36
  from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
37
  from fastmcp.server.dependencies import get_http_headers
38
  from fastmcp.server.server import FastMCP
39
+ from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import EnvironmentConfig
40
  from fastmcp.utilities.logging import get_logger
41
 
42
  logger = get_logger(__name__)
 
196
  async def connect_session(
197
  self, **session_kwargs: Unpack[SessionKwargs]
198
  ) -> AsyncIterator[ClientSession]:
 
 
199
  client_kwargs: dict[str, Any] = {}
200
 
201
  # load headers from an active HTTP request, if available. This will only be true
 
266
  async def connect_session(
267
  self, **session_kwargs: Unpack[SessionKwargs]
268
  ) -> AsyncIterator[ClientSession]:
 
 
269
  client_kwargs: dict[str, Any] = {}
270
 
271
  # load headers from an active HTTP request, if available. This will only be true
 
429
  """A standalone connection task for a stdio transport. It is not a part of the StdioTransport class
430
  to ensure that the connection task does not hold a reference to the Transport object."""
431
 
 
 
432
  try:
433
  async with contextlib.AsyncExitStack() as stack:
434
  try:
 
596
  f"Project directory not found: {project_directory}"
597
  )
598
 
599
+ # Create EnvironmentConfig from provided parameters (internal use)
600
+ env_config = EnvironmentConfig(
601
+ python=python_version,
602
+ dependencies=with_packages,
603
+ requirements=with_requirements,
604
+ project=project_directory,
605
+ editable=None, # Not exposed in this transport
606
+ )
607
+
608
+ # Build uv arguments using the config
609
+ uv_args: list[str] = []
610
+
611
+ # Check if we need any environment setup
612
+ if env_config.needs_uv():
613
+ # Use the config to build args, but we need to handle the command differently
614
+ # since transport has specific needs
615
+ uv_args = ["run"]
616
+
617
+ if python_version:
618
+ uv_args.extend(["--python", python_version])
619
+ if project_directory:
620
+ uv_args.extend(["--directory", str(project_directory)])
621
+
622
+ # Note: Don't add fastmcp as dependency here, transport is for general use
623
+ for pkg in with_packages or []:
624
+ uv_args.extend(["--with", pkg])
625
+ if with_requirements:
626
+ uv_args.extend(["--with-requirements", str(with_requirements)])
627
+ else:
628
+ # No environment setup needed
629
+ uv_args = ["run"]
630
+
631
  if module:
632
  uv_args.append("--module")
633
 
src/fastmcp/mcp_config.py CHANGED
@@ -36,7 +36,6 @@ from pydantic import (
36
  BaseModel,
37
  ConfigDict,
38
  Field,
39
- ValidationInfo,
40
  model_validator,
41
  )
42
  from typing_extensions import Self, override
@@ -239,20 +238,24 @@ class MCPConfig(BaseModel):
239
  For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
240
  """
241
 
242
- mcpServers: dict[str, MCPServerTypes]
243
 
244
  model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields
245
 
246
  @model_validator(mode="before")
247
- def validate_mcp_servers(self, info: ValidationInfo) -> dict[str, Any]:
248
- """Validate the MCP servers."""
249
- if not isinstance(self, dict):
250
- raise ValueError("MCPConfig format requires a dictionary of servers.")
251
-
252
- if "mcpServers" not in self:
253
- self = {"mcpServers": self}
254
-
255
- return self
 
 
 
 
256
 
257
  def add_server(self, name: str, server: MCPServerTypes) -> None:
258
  """Add or update a server in the configuration."""
@@ -289,7 +292,7 @@ class CanonicalMCPConfig(MCPConfig):
289
  The format is designed to be client-agnostic and extensible for future use cases.
290
  """
291
 
292
- mcpServers: dict[str, CanonicalMCPServerTypes]
293
 
294
  @override
295
  def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None:
 
36
  BaseModel,
37
  ConfigDict,
38
  Field,
 
39
  model_validator,
40
  )
41
  from typing_extensions import Self, override
 
238
  For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
239
  """
240
 
241
+ mcpServers: dict[str, MCPServerTypes] = Field(default_factory=dict)
242
 
243
  model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields
244
 
245
  @model_validator(mode="before")
246
+ @classmethod
247
+ def wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]:
248
+ """If there's no mcpServers key but there are server configs at root, wrap them."""
249
+ if "mcpServers" not in values:
250
+ # Check if any values look like server configs
251
+ has_servers = any(
252
+ isinstance(v, dict) and ("command" in v or "url" in v)
253
+ for v in values.values()
254
+ )
255
+ if has_servers:
256
+ # Move all server-like configs under mcpServers
257
+ return {"mcpServers": values}
258
+ return values
259
 
260
  def add_server(self, name: str, server: MCPServerTypes) -> None:
261
  """Add or update a server in the configuration."""
 
292
  The format is designed to be client-agnostic and extensible for future use cases.
293
  """
294
 
295
+ mcpServers: dict[str, CanonicalMCPServerTypes] = Field(default_factory=dict)
296
 
297
  @override
298
  def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None:
src/fastmcp/server/server.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations
4
 
5
  import inspect
 
6
  import re
7
  import warnings
8
  from collections.abc import AsyncIterator, Awaitable, Callable
@@ -222,7 +223,24 @@ class FastMCP(Generic[LifespanResultT]):
222
 
223
  # Set up MCP protocol handlers
224
  self._setup_handlers()
225
- self.dependencies = dependencies or fastmcp.settings.server_dependencies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
  self.include_fastmcp_meta = (
228
  include_fastmcp_meta
 
3
  from __future__ import annotations
4
 
5
  import inspect
6
+ import json
7
  import re
8
  import warnings
9
  from collections.abc import AsyncIterator, Awaitable, Callable
 
223
 
224
  # Set up MCP protocol handlers
225
  self._setup_handlers()
226
+
227
+ # Handle dependencies with deprecation warning
228
+ # TODO: Remove dependencies parameter (deprecated in v2.11.4)
229
+ if dependencies is not None:
230
+ import warnings
231
+
232
+ warnings.warn(
233
+ "The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
234
+ "Please specify dependencies in a fastmcp.json configuration file instead:\n"
235
+ '{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
236
+ f"{json.dumps(dependencies)}\n }}\n}}\n"
237
+ "See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
238
+ DeprecationWarning,
239
+ stacklevel=2,
240
+ )
241
+ self.dependencies = (
242
+ dependencies or fastmcp.settings.server_dependencies
243
+ ) # TODO: Remove (deprecated in v2.11.4)
244
 
245
  self.include_fastmcp_meta = (
246
  include_fastmcp_meta
src/fastmcp/utilities/fastmcp_config/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastMCP Configuration module.
2
+
3
+ This module provides versioned configuration support for FastMCP servers.
4
+ The current version is v1, which is re-exported here for convenience.
5
+ """
6
+
7
+ from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import (
8
+ DeploymentConfig,
9
+ EntrypointConfig,
10
+ EnvironmentConfig,
11
+ FastMCPConfig,
12
+ generate_schema,
13
+ )
14
+
15
+ __all__ = [
16
+ "FastMCPConfig",
17
+ "EntrypointConfig",
18
+ "EnvironmentConfig",
19
+ "DeploymentConfig",
20
+ "generate_schema",
21
+ ]
src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py ADDED
@@ -0,0 +1,678 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastMCP Configuration File Support.
2
+
3
+ This module provides support for fastmcp.json configuration files that allow
4
+ users to specify server settings in a declarative format instead of using
5
+ command-line arguments.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any, Literal, overload
15
+
16
+ from pydantic import BaseModel, Field, field_validator
17
+
18
+ from fastmcp.utilities.logging import get_logger
19
+
20
+ logger = get_logger("cli.config")
21
+
22
+ # JSON Schema for IDE support
23
+ FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
24
+
25
+
26
+ class EntrypointConfig(BaseModel):
27
+ """Configuration for server entrypoint when using object format."""
28
+
29
+ file: str = Field(
30
+ description="Path to Python file containing the server",
31
+ examples=["server.py", "src/server.py", "app/main.py"],
32
+ )
33
+
34
+ object: str | None = Field(
35
+ default=None,
36
+ description="Name of the server object in the file (defaults to searching for mcp/server/app)",
37
+ examples=["app", "mcp", "server"],
38
+ )
39
+
40
+ repo: str | None = Field(
41
+ default=None,
42
+ description="Git repository URL",
43
+ examples=["https://github.com/user/repo"],
44
+ )
45
+
46
+
47
+ class EnvironmentConfig(BaseModel):
48
+ """Configuration for Python environment setup."""
49
+
50
+ python: str | None = Field(
51
+ default=None,
52
+ description="Python version constraint",
53
+ examples=["3.10", "3.11", "3.12"],
54
+ )
55
+
56
+ dependencies: list[str] | None = Field(
57
+ default=None,
58
+ description="Python packages to install with PEP 508 specifiers",
59
+ examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
60
+ )
61
+
62
+ requirements: str | None = Field(
63
+ default=None,
64
+ description="Path to requirements.txt file",
65
+ examples=["requirements.txt", "../requirements/prod.txt"],
66
+ )
67
+
68
+ project: str | None = Field(
69
+ default=None,
70
+ description="Path to project directory containing pyproject.toml",
71
+ examples=[".", "../my-project"],
72
+ )
73
+
74
+ editable: str | None = Field(
75
+ default=None,
76
+ description="Directory to install in editable mode",
77
+ examples=[".", "../my-package"],
78
+ )
79
+
80
+ def build_uv_args(self, command: str | list[str] | None = None) -> list[str]:
81
+ """Build uv run arguments from this environment configuration.
82
+
83
+ Args:
84
+ command: Optional command to append (string or list of args)
85
+
86
+ Returns:
87
+ List of arguments for uv run command
88
+ """
89
+ args = ["run"]
90
+
91
+ # Add Python version if specified
92
+ if self.python:
93
+ args.extend(["--python", self.python])
94
+
95
+ # Add project directory if specified
96
+ if self.project:
97
+ args.extend(["--project", str(self.project)])
98
+
99
+ # Add fastmcp as a base dependency
100
+ args.extend(["--with", "fastmcp"])
101
+
102
+ # Add additional dependencies
103
+ if self.dependencies:
104
+ for dep in self.dependencies:
105
+ args.extend(["--with", dep])
106
+
107
+ # Add requirements file
108
+ if self.requirements:
109
+ args.extend(["--with-requirements", str(self.requirements)])
110
+
111
+ # Add editable package
112
+ if self.editable:
113
+ args.extend(["--with-editable", str(self.editable)])
114
+
115
+ # Add the command if provided
116
+ if command:
117
+ if isinstance(command, str):
118
+ args.append(command)
119
+ else:
120
+ args.extend(command)
121
+
122
+ return args
123
+
124
+ def run_with_uv(self, command: list[str]) -> None:
125
+ """Execute a command using uv run with this environment configuration.
126
+
127
+ Args:
128
+ command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
129
+ """
130
+ import subprocess
131
+ import sys
132
+
133
+ # Build the full uv command
134
+ uv_args = self.build_uv_args(command)
135
+ cmd = ["uv"] + uv_args
136
+
137
+ logger.debug(f"Running command: {' '.join(cmd)}")
138
+
139
+ try:
140
+ # Run without capturing output so it flows through naturally
141
+ process = subprocess.run(cmd, check=True)
142
+ sys.exit(process.returncode)
143
+ except subprocess.CalledProcessError as e:
144
+ logger.error(f"Command failed: {e}")
145
+ sys.exit(e.returncode)
146
+
147
+ def needs_uv(self) -> bool:
148
+ """Check if this environment config requires uv to set up.
149
+
150
+ Returns:
151
+ True if any environment settings require uv run
152
+ """
153
+ return bool(
154
+ self.python
155
+ or self.dependencies
156
+ or self.requirements
157
+ or self.project
158
+ or self.editable
159
+ )
160
+
161
+ def merge_with_cli_args(
162
+ self,
163
+ python: str | None = None,
164
+ with_packages: list[str] | None = None,
165
+ with_requirements: Path | None = None,
166
+ project: Path | None = None,
167
+ with_editable: Path | None = None,
168
+ ) -> dict[str, Any]:
169
+ """Merge environment config with CLI arguments, with CLI taking precedence.
170
+
171
+ For packages, combines both config and CLI packages.
172
+ For other fields, CLI takes precedence if provided.
173
+
174
+ Returns:
175
+ Dictionary with merged arguments suitable for CLI commands
176
+ """
177
+ from pathlib import Path
178
+
179
+ # Merge packages from both sources
180
+ packages = []
181
+ if self.dependencies:
182
+ packages.extend(self.dependencies)
183
+ if with_packages:
184
+ packages.extend(with_packages)
185
+
186
+ return {
187
+ "python": python or self.python,
188
+ "with_packages": packages,
189
+ "with_requirements": with_requirements
190
+ or (Path(self.requirements) if self.requirements else None),
191
+ "project": project or (Path(self.project) if self.project else None),
192
+ "with_editable": with_editable
193
+ or (Path(self.editable) if self.editable else None),
194
+ }
195
+
196
+
197
+ class DeploymentConfig(BaseModel):
198
+ """Configuration for server deployment and runtime settings."""
199
+
200
+ transport: Literal["stdio", "http", "sse"] | None = Field(
201
+ default=None,
202
+ description="Transport protocol to use",
203
+ )
204
+
205
+ host: str | None = Field(
206
+ default=None,
207
+ description="Host to bind to when using HTTP transport",
208
+ examples=["127.0.0.1", "0.0.0.0", "localhost"],
209
+ )
210
+
211
+ port: int | None = Field(
212
+ default=None,
213
+ description="Port to bind to when using HTTP transport",
214
+ examples=[8000, 3000, 5000],
215
+ )
216
+
217
+ path: str | None = Field(
218
+ default=None,
219
+ description="URL path for the server endpoint",
220
+ examples=["/mcp/", "/api/mcp/", "/sse/"],
221
+ )
222
+
223
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = Field(
224
+ default=None,
225
+ description="Log level for the server",
226
+ )
227
+
228
+ cwd: str | None = Field(
229
+ default=None,
230
+ description="Working directory for the server process",
231
+ examples=[".", "./src", "/app"],
232
+ )
233
+
234
+ env: dict[str, str] | None = Field(
235
+ default=None,
236
+ description="Environment variables to set when running the server",
237
+ examples=[{"API_KEY": "secret", "DEBUG": "true"}],
238
+ )
239
+
240
+ args: list[str] | None = Field(
241
+ default=None,
242
+ description="Arguments to pass to the server (after --)",
243
+ examples=[["--config", "config.json", "--debug"]],
244
+ )
245
+
246
+ def merge_with_cli_args(
247
+ self,
248
+ transport: str | None = None,
249
+ host: str | None = None,
250
+ port: int | None = None,
251
+ path: str | None = None,
252
+ log_level: str | None = None,
253
+ server_args: list[str] | None = None,
254
+ ) -> dict[str, Any]:
255
+ """Merge deployment config with CLI arguments, with CLI taking precedence.
256
+
257
+ Returns:
258
+ Dictionary with merged arguments suitable for CLI commands
259
+ """
260
+ return {
261
+ "transport": transport or self.transport,
262
+ "host": host or self.host,
263
+ "port": port or self.port,
264
+ "path": path or self.path,
265
+ "log_level": log_level or self.log_level,
266
+ "server_args": server_args if server_args is not None else self.args,
267
+ }
268
+
269
+ def apply_runtime_settings(self, config_path: Path | None = None) -> None:
270
+ """Apply runtime settings like environment variables and working directory.
271
+
272
+ Args:
273
+ config_path: Path to config file for resolving relative paths
274
+
275
+ Environment variables support interpolation with ${VAR_NAME} syntax.
276
+ For example: "API_URL": "https://api.${ENVIRONMENT}.example.com"
277
+ will substitute the value of the ENVIRONMENT variable at runtime.
278
+ """
279
+ import os
280
+ from pathlib import Path
281
+
282
+ # Set environment variables with interpolation support
283
+ if self.env:
284
+ for key, value in self.env.items():
285
+ # Interpolate environment variables in the value
286
+ interpolated_value = self._interpolate_env_vars(value)
287
+ os.environ[key] = interpolated_value
288
+
289
+ # Change working directory
290
+ if self.cwd:
291
+ cwd_path = Path(self.cwd)
292
+ if not cwd_path.is_absolute() and config_path:
293
+ cwd_path = (config_path.parent / cwd_path).resolve()
294
+ os.chdir(cwd_path)
295
+
296
+ def _interpolate_env_vars(self, value: str) -> str:
297
+ """Interpolate environment variables in a string.
298
+
299
+ Replaces ${VAR_NAME} with the value of VAR_NAME from the environment.
300
+ If the variable is not set, the placeholder is left unchanged.
301
+
302
+ Args:
303
+ value: String potentially containing ${VAR_NAME} placeholders
304
+
305
+ Returns:
306
+ String with environment variables interpolated
307
+ """
308
+
309
+ def replace_var(match: re.Match) -> str:
310
+ var_name = match.group(1)
311
+ # Return the environment variable value if it exists, otherwise keep the placeholder
312
+ return os.environ.get(var_name, match.group(0))
313
+
314
+ # Match ${VAR_NAME} pattern and replace with environment variable values
315
+ return re.sub(r"\$\{([^}]+)\}", replace_var, value)
316
+
317
+
318
+ class FastMCPConfig(BaseModel):
319
+ """Configuration for a FastMCP server.
320
+
321
+ This configuration file allows you to specify all settings needed to run
322
+ a FastMCP server in a declarative format.
323
+ """
324
+
325
+ # Schema field for IDE support
326
+ schema_: str | None = Field(
327
+ default="https://gofastmcp.com/schemas/fastmcp_config/v1.json",
328
+ alias="$schema",
329
+ description="JSON schema for IDE support and validation",
330
+ )
331
+
332
+ # Server entrypoint - supports both string and object format
333
+ entrypoint: EntrypointConfig = Field(
334
+ description="Server entrypoint as a string (file or file:object) or object with file/object/repo",
335
+ examples=[
336
+ "server.py",
337
+ "server.py:app",
338
+ {"file": "src/server.py", "object": "app"},
339
+ ],
340
+ )
341
+
342
+ # Environment configuration
343
+ environment: EnvironmentConfig = Field(
344
+ default_factory=lambda: EnvironmentConfig(),
345
+ description="Python environment setup configuration",
346
+ )
347
+
348
+ # Deployment configuration
349
+ deployment: DeploymentConfig = Field(
350
+ default_factory=lambda: DeploymentConfig(),
351
+ description="Server deployment and runtime settings",
352
+ )
353
+
354
+ # purely for static type checkers to avoid issues with providng str entrypoint
355
+ if TYPE_CHECKING:
356
+
357
+ @overload
358
+ def __init__(
359
+ self, *, entrypoint: str | dict | EntrypointConfig, **data
360
+ ) -> None: ...
361
+ @overload
362
+ def __init__(
363
+ self, *, environment: dict | EnvironmentConfig, **data
364
+ ) -> None: ...
365
+ @overload
366
+ def __init__(self, *, deployment: dict | DeploymentConfig, **data) -> None: ...
367
+ def __init__(self, **data) -> None: ...
368
+
369
+ @field_validator("entrypoint", mode="before")
370
+ @classmethod
371
+ def validate_entrypoint(cls, v: str | EntrypointConfig) -> EntrypointConfig:
372
+ """Validate and convert entrypoint to proper format.
373
+
374
+ Supports:
375
+ - String format: "server.py" or "server.py:object"
376
+ - Object format: {"file": "server.py", "object": "app"}
377
+ - EntrypointConfig instance (passed through)
378
+
379
+ The string format with :object syntax is automatically parsed into
380
+ the object format for consistency.
381
+ """
382
+ if isinstance(v, EntrypointConfig):
383
+ # Already an EntrypointConfig instance, return as-is
384
+ return v
385
+ elif isinstance(v, dict):
386
+ return EntrypointConfig(**v)
387
+ elif isinstance(v, str):
388
+ # Parse file.py:object syntax into object format if present
389
+ if ":" in v:
390
+ # Check if it's a Windows path (e.g., C:\...)
391
+ has_windows_drive = len(v) > 1 and v[1] == ":"
392
+
393
+ # Only split if colon is not part of Windows drive
394
+ if ":" in (v[2:] if has_windows_drive else v):
395
+ file, obj = v.rsplit(":", 1)
396
+ return EntrypointConfig(file=file, object=obj)
397
+ else:
398
+ return EntrypointConfig(file=v)
399
+
400
+ raise ValueError("entrypoint must be a string, EntrypointConfig instance")
401
+
402
+ @field_validator("environment", mode="before")
403
+ @classmethod
404
+ def validate_environment(cls, v: dict | EnvironmentConfig) -> EnvironmentConfig:
405
+ """Validate and convert environment to EnvironmentConfig.
406
+
407
+ Accepts:
408
+ - EnvironmentConfig instance
409
+ - dict that can be converted to EnvironmentConfig
410
+ """
411
+ if isinstance(v, EnvironmentConfig):
412
+ return v
413
+ elif isinstance(v, dict):
414
+ return EnvironmentConfig(**v) # type: ignore[arg-type]
415
+ else:
416
+ raise ValueError("environment must be a dict, EnvironmentConfig instance")
417
+
418
+ @field_validator("deployment", mode="before")
419
+ @classmethod
420
+ def validate_deployment(cls, v: dict | DeploymentConfig) -> DeploymentConfig:
421
+ """Validate and convert deployment to DeploymentConfig.
422
+
423
+ Accepts:
424
+ - DeploymentConfig instance
425
+ - dict that can be converted to DeploymentConfig
426
+
427
+ """
428
+ if isinstance(v, DeploymentConfig):
429
+ return v
430
+ elif isinstance(v, dict):
431
+ return DeploymentConfig(**v) # type: ignore[arg-type]
432
+ else:
433
+ raise ValueError("deployment must be a dict, DeploymentConfig instance")
434
+
435
+ def get_entrypoint(self, config_path: Path | None = None) -> EntrypointConfig:
436
+ """Get the entrypoint as a structured object with resolved paths.
437
+
438
+ Args:
439
+ config_path: Path to config file for resolving relative paths
440
+
441
+ Returns:
442
+ EntrypointConfig object with file, object, and repo fields.
443
+ If config_path is provided, relative file paths are resolved
444
+ relative to the config file location.
445
+ """
446
+ if isinstance(self.entrypoint, str):
447
+ # Parse string format into structured object
448
+ if ":" in self.entrypoint:
449
+ file, obj = self.entrypoint.rsplit(":", 1)
450
+ entrypoint = EntrypointConfig(file=file, object=obj)
451
+ else:
452
+ entrypoint = EntrypointConfig(file=self.entrypoint)
453
+ else:
454
+ # Already an EntrypointConfig
455
+ entrypoint = self.entrypoint
456
+
457
+ # Resolve relative paths if config_path provided
458
+ if config_path:
459
+ file_path = Path(entrypoint.file)
460
+ if not file_path.is_absolute():
461
+ resolved_path = (config_path.parent / file_path).resolve()
462
+ # Create new EntrypointConfig with resolved path
463
+ entrypoint = EntrypointConfig(
464
+ file=str(resolved_path),
465
+ object=entrypoint.object,
466
+ repo=entrypoint.repo,
467
+ )
468
+
469
+ return entrypoint
470
+
471
+ @classmethod
472
+ def from_file(cls, file_path: Path) -> FastMCPConfig:
473
+ """Load configuration from a JSON file.
474
+
475
+ Args:
476
+ file_path: Path to the configuration file
477
+
478
+ Returns:
479
+ FastMCPConfig instance
480
+
481
+ Raises:
482
+ FileNotFoundError: If the file doesn't exist
483
+ json.JSONDecodeError: If the file is not valid JSON
484
+ pydantic.ValidationError: If the configuration is invalid
485
+ """
486
+ if not file_path.exists():
487
+ raise FileNotFoundError(f"Configuration file not found: {file_path}")
488
+
489
+ with file_path.open("r", encoding="utf-8") as f:
490
+ data = json.load(f)
491
+
492
+ return cls.model_validate(data)
493
+
494
+ @classmethod
495
+ def from_cli_args(
496
+ cls,
497
+ entrypoint: str,
498
+ transport: Literal["stdio", "http", "sse", "streamable-http"] | None = None,
499
+ host: str | None = None,
500
+ port: int | None = None,
501
+ path: str | None = None,
502
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
503
+ | None = None,
504
+ python: str | None = None,
505
+ dependencies: list[str] | None = None,
506
+ requirements: str | None = None,
507
+ project: str | None = None,
508
+ editable: str | None = None,
509
+ env: dict[str, str] | None = None,
510
+ cwd: str | None = None,
511
+ args: list[str] | None = None,
512
+ ) -> FastMCPConfig:
513
+ """Create a config from CLI arguments.
514
+
515
+ This allows us to have a single code path where everything
516
+ goes through a config object.
517
+
518
+ Args:
519
+ entrypoint: Server entrypoint (file or file:object)
520
+ transport: Transport protocol
521
+ host: Host for HTTP transport
522
+ port: Port for HTTP transport
523
+ path: URL path for server
524
+ log_level: Logging level
525
+ python: Python version
526
+ dependencies: Python packages to install
527
+ requirements: Path to requirements file
528
+ project: Path to project directory
529
+ editable: Path to install in editable mode
530
+ env: Environment variables
531
+ cwd: Working directory
532
+ args: Server arguments
533
+
534
+ Returns:
535
+ FastMCPConfig instance
536
+ """
537
+ # Build environment config if any env args provided
538
+ environment = None
539
+ if any([python, dependencies, requirements, project, editable]):
540
+ environment = EnvironmentConfig(
541
+ python=python,
542
+ dependencies=dependencies,
543
+ requirements=requirements,
544
+ project=project,
545
+ editable=editable,
546
+ )
547
+
548
+ # Build deployment config if any deployment args provided
549
+ deployment = None
550
+ if any([transport, host, port, path, log_level, env, cwd, args]):
551
+ # Convert streamable-http to http for backward compatibility
552
+ if transport == "streamable-http":
553
+ transport = "http" # type: ignore[assignment]
554
+ deployment = DeploymentConfig(
555
+ transport=transport, # type: ignore[arg-type]
556
+ host=host,
557
+ port=port,
558
+ path=path,
559
+ log_level=log_level,
560
+ env=env,
561
+ cwd=cwd,
562
+ args=args,
563
+ )
564
+
565
+ return cls(
566
+ entrypoint=entrypoint,
567
+ environment=environment,
568
+ deployment=deployment,
569
+ )
570
+
571
+ @classmethod
572
+ def find_config(cls, start_path: Path | None = None) -> Path | None:
573
+ """Find a fastmcp.json file in the specified directory.
574
+
575
+ Args:
576
+ start_path: Directory to look in (defaults to current directory)
577
+
578
+ Returns:
579
+ Path to the configuration file, or None if not found
580
+ """
581
+ if start_path is None:
582
+ start_path = Path.cwd()
583
+
584
+ config_path = start_path / "fastmcp.json"
585
+ if config_path.exists():
586
+ logger.debug(f"Found configuration file: {config_path}")
587
+ return config_path
588
+
589
+ return None
590
+
591
+ async def load_server(self, config_path: Path | None = None) -> Any:
592
+ """Load the server from the configuration.
593
+
594
+ This handles environment setup, working directory changes,
595
+ and imports the server module.
596
+
597
+ Args:
598
+ config_path: Path to the config file (for resolving relative paths)
599
+
600
+ Returns:
601
+ The imported server object
602
+ """
603
+ import os
604
+ from pathlib import Path
605
+
606
+ # Set environment variables if specified
607
+ if self.deployment and self.deployment.env:
608
+ for key, value in self.deployment.env.items():
609
+ os.environ[key] = value
610
+
611
+ # Change working directory if specified
612
+ if self.deployment and self.deployment.cwd:
613
+ cwd_path = Path(self.deployment.cwd)
614
+ if not cwd_path.is_absolute():
615
+ # If config_path provided, resolve relative to it
616
+ if config_path:
617
+ cwd_path = (config_path.parent / cwd_path).resolve()
618
+ else:
619
+ cwd_path = cwd_path.resolve()
620
+ os.chdir(cwd_path)
621
+
622
+ # Get structured entrypoint with resolved paths
623
+ entrypoint = self.get_entrypoint(config_path)
624
+
625
+ # Import the server
626
+ from fastmcp.cli.run import import_server_with_args
627
+
628
+ file_path = Path(entrypoint.file)
629
+ server_args = self.deployment.args if self.deployment else None
630
+
631
+ return await import_server_with_args(file_path, entrypoint.object, server_args)
632
+
633
+ async def run_server(self, **kwargs: Any) -> None:
634
+ """Load and run the server with this configuration.
635
+
636
+ Args:
637
+ **kwargs: Additional arguments to pass to server.run_async()
638
+ These override config settings
639
+ """
640
+ server = await self.load_server()
641
+
642
+ # Build run arguments from config
643
+ run_args = {}
644
+ if self.deployment:
645
+ if self.deployment.transport:
646
+ run_args["transport"] = self.deployment.transport
647
+ if self.deployment.host:
648
+ run_args["host"] = self.deployment.host
649
+ if self.deployment.port:
650
+ run_args["port"] = self.deployment.port
651
+ if self.deployment.path:
652
+ run_args["path"] = self.deployment.path
653
+ # Note: log_level not currently supported by run_async
654
+
655
+ # Override with any provided kwargs
656
+ run_args.update(kwargs)
657
+
658
+ # Run the server
659
+ await server.run_async(**run_args)
660
+
661
+
662
+ def generate_schema() -> dict[str, Any]:
663
+ """Generate JSON schema for fastmcp.json files.
664
+
665
+ This is used to create the schema file that IDEs can use for
666
+ validation and auto-completion.
667
+
668
+ Returns:
669
+ JSON schema as a dictionary
670
+ """
671
+ schema = FastMCPConfig.model_json_schema()
672
+
673
+ # Add some metadata
674
+ schema["$id"] = FASTMCP_JSON_SCHEMA
675
+ schema["title"] = "FastMCP Configuration"
676
+ schema["description"] = "Configuration file for FastMCP servers"
677
+
678
+ return schema
src/fastmcp/utilities/fastmcp_config/v1/schema.json ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$defs": {
3
+ "DeploymentConfig": {
4
+ "description": "Configuration for server deployment and runtime settings.",
5
+ "properties": {
6
+ "transport": {
7
+ "anyOf": [
8
+ {
9
+ "enum": [
10
+ "stdio",
11
+ "http",
12
+ "sse"
13
+ ],
14
+ "type": "string"
15
+ },
16
+ {
17
+ "type": "null"
18
+ }
19
+ ],
20
+ "default": null,
21
+ "description": "Transport protocol to use",
22
+ "title": "Transport"
23
+ },
24
+ "host": {
25
+ "anyOf": [
26
+ {
27
+ "type": "string"
28
+ },
29
+ {
30
+ "type": "null"
31
+ }
32
+ ],
33
+ "default": null,
34
+ "description": "Host to bind to when using HTTP transport",
35
+ "examples": [
36
+ "127.0.0.1",
37
+ "0.0.0.0",
38
+ "localhost"
39
+ ],
40
+ "title": "Host"
41
+ },
42
+ "port": {
43
+ "anyOf": [
44
+ {
45
+ "type": "integer"
46
+ },
47
+ {
48
+ "type": "null"
49
+ }
50
+ ],
51
+ "default": null,
52
+ "description": "Port to bind to when using HTTP transport",
53
+ "examples": [
54
+ 8000,
55
+ 3000,
56
+ 5000
57
+ ],
58
+ "title": "Port"
59
+ },
60
+ "path": {
61
+ "anyOf": [
62
+ {
63
+ "type": "string"
64
+ },
65
+ {
66
+ "type": "null"
67
+ }
68
+ ],
69
+ "default": null,
70
+ "description": "URL path for the server endpoint",
71
+ "examples": [
72
+ "/mcp/",
73
+ "/api/mcp/",
74
+ "/sse/"
75
+ ],
76
+ "title": "Path"
77
+ },
78
+ "log_level": {
79
+ "anyOf": [
80
+ {
81
+ "enum": [
82
+ "DEBUG",
83
+ "INFO",
84
+ "WARNING",
85
+ "ERROR",
86
+ "CRITICAL"
87
+ ],
88
+ "type": "string"
89
+ },
90
+ {
91
+ "type": "null"
92
+ }
93
+ ],
94
+ "default": null,
95
+ "description": "Log level for the server",
96
+ "title": "Log Level"
97
+ },
98
+ "cwd": {
99
+ "anyOf": [
100
+ {
101
+ "type": "string"
102
+ },
103
+ {
104
+ "type": "null"
105
+ }
106
+ ],
107
+ "default": null,
108
+ "description": "Working directory for the server process",
109
+ "examples": [
110
+ ".",
111
+ "./src",
112
+ "/app"
113
+ ],
114
+ "title": "Cwd"
115
+ },
116
+ "env": {
117
+ "anyOf": [
118
+ {
119
+ "additionalProperties": {
120
+ "type": "string"
121
+ },
122
+ "type": "object"
123
+ },
124
+ {
125
+ "type": "null"
126
+ }
127
+ ],
128
+ "default": null,
129
+ "description": "Environment variables to set when running the server",
130
+ "examples": [
131
+ {
132
+ "API_KEY": "secret",
133
+ "DEBUG": "true"
134
+ }
135
+ ],
136
+ "title": "Env"
137
+ },
138
+ "args": {
139
+ "anyOf": [
140
+ {
141
+ "items": {
142
+ "type": "string"
143
+ },
144
+ "type": "array"
145
+ },
146
+ {
147
+ "type": "null"
148
+ }
149
+ ],
150
+ "default": null,
151
+ "description": "Arguments to pass to the server (after --)",
152
+ "examples": [
153
+ [
154
+ "--config",
155
+ "config.json",
156
+ "--debug"
157
+ ]
158
+ ],
159
+ "title": "Args"
160
+ }
161
+ },
162
+ "title": "DeploymentConfig",
163
+ "type": "object"
164
+ },
165
+ "EntrypointConfig": {
166
+ "description": "Configuration for server entrypoint when using object format.",
167
+ "properties": {
168
+ "file": {
169
+ "description": "Path to Python file containing the server",
170
+ "examples": [
171
+ "server.py",
172
+ "src/server.py",
173
+ "app/main.py"
174
+ ],
175
+ "title": "File",
176
+ "type": "string"
177
+ },
178
+ "object": {
179
+ "anyOf": [
180
+ {
181
+ "type": "string"
182
+ },
183
+ {
184
+ "type": "null"
185
+ }
186
+ ],
187
+ "default": null,
188
+ "description": "Name of the server object in the file (defaults to searching for mcp/server/app)",
189
+ "examples": [
190
+ "app",
191
+ "mcp",
192
+ "server"
193
+ ],
194
+ "title": "Object"
195
+ },
196
+ "repo": {
197
+ "anyOf": [
198
+ {
199
+ "type": "string"
200
+ },
201
+ {
202
+ "type": "null"
203
+ }
204
+ ],
205
+ "default": null,
206
+ "description": "Git repository URL",
207
+ "examples": [
208
+ "https://github.com/user/repo"
209
+ ],
210
+ "title": "Repo"
211
+ }
212
+ },
213
+ "required": [
214
+ "file"
215
+ ],
216
+ "title": "EntrypointConfig",
217
+ "type": "object"
218
+ },
219
+ "EnvironmentConfig": {
220
+ "description": "Configuration for Python environment setup.",
221
+ "properties": {
222
+ "python": {
223
+ "anyOf": [
224
+ {
225
+ "type": "string"
226
+ },
227
+ {
228
+ "type": "null"
229
+ }
230
+ ],
231
+ "default": null,
232
+ "description": "Python version constraint",
233
+ "examples": [
234
+ "3.10",
235
+ "3.11",
236
+ "3.12"
237
+ ],
238
+ "title": "Python"
239
+ },
240
+ "dependencies": {
241
+ "anyOf": [
242
+ {
243
+ "items": {
244
+ "type": "string"
245
+ },
246
+ "type": "array"
247
+ },
248
+ {
249
+ "type": "null"
250
+ }
251
+ ],
252
+ "default": null,
253
+ "description": "Python packages to install with PEP 508 specifiers",
254
+ "examples": [
255
+ [
256
+ "fastmcp>=2.0,<3",
257
+ "httpx",
258
+ "pandas>=2.0"
259
+ ]
260
+ ],
261
+ "title": "Dependencies"
262
+ },
263
+ "requirements": {
264
+ "anyOf": [
265
+ {
266
+ "type": "string"
267
+ },
268
+ {
269
+ "type": "null"
270
+ }
271
+ ],
272
+ "default": null,
273
+ "description": "Path to requirements.txt file",
274
+ "examples": [
275
+ "requirements.txt",
276
+ "../requirements/prod.txt"
277
+ ],
278
+ "title": "Requirements"
279
+ },
280
+ "project": {
281
+ "anyOf": [
282
+ {
283
+ "type": "string"
284
+ },
285
+ {
286
+ "type": "null"
287
+ }
288
+ ],
289
+ "default": null,
290
+ "description": "Path to project directory containing pyproject.toml",
291
+ "examples": [
292
+ ".",
293
+ "../my-project"
294
+ ],
295
+ "title": "Project"
296
+ },
297
+ "editable": {
298
+ "anyOf": [
299
+ {
300
+ "type": "string"
301
+ },
302
+ {
303
+ "type": "null"
304
+ }
305
+ ],
306
+ "default": null,
307
+ "description": "Directory to install in editable mode",
308
+ "examples": [
309
+ ".",
310
+ "../my-package"
311
+ ],
312
+ "title": "Editable"
313
+ }
314
+ },
315
+ "title": "EnvironmentConfig",
316
+ "type": "object"
317
+ }
318
+ },
319
+ "description": "Configuration file for FastMCP servers",
320
+ "properties": {
321
+ "$schema": {
322
+ "anyOf": [
323
+ {
324
+ "type": "string"
325
+ },
326
+ {
327
+ "type": "null"
328
+ }
329
+ ],
330
+ "default": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
331
+ "description": "JSON schema for IDE support and validation",
332
+ "title": "$Schema"
333
+ },
334
+ "entrypoint": {
335
+ "$ref": "#/$defs/EntrypointConfig",
336
+ "description": "Server entrypoint as a string (file or file:object) or object with file/object/repo",
337
+ "examples": [
338
+ "server.py",
339
+ "server.py:app",
340
+ {
341
+ "file": "src/server.py",
342
+ "object": "app"
343
+ }
344
+ ]
345
+ },
346
+ "environment": {
347
+ "$ref": "#/$defs/EnvironmentConfig",
348
+ "description": "Python environment setup configuration"
349
+ },
350
+ "deployment": {
351
+ "$ref": "#/$defs/DeploymentConfig",
352
+ "description": "Server deployment and runtime settings"
353
+ }
354
+ },
355
+ "required": [
356
+ "entrypoint"
357
+ ],
358
+ "title": "FastMCP Configuration",
359
+ "type": "object",
360
+ "$id": "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
361
+ }
tests/cli/test_config.py ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for FastMCP configuration file support with nested structure."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from pydantic import ValidationError
9
+
10
+ from fastmcp.utilities.fastmcp_config import (
11
+ DeploymentConfig,
12
+ EntrypointConfig,
13
+ EnvironmentConfig,
14
+ FastMCPConfig,
15
+ )
16
+
17
+
18
+ class TestEntrypointConfig:
19
+ """Test EntrypointConfig class."""
20
+
21
+ def test_string_entrypoint(self):
22
+ """Test that string entrypoint is converted to EntrypointConfig."""
23
+ config = FastMCPConfig(entrypoint="server.py")
24
+ # With the new validator, this should be converted to EntrypointConfig
25
+ assert isinstance(config.entrypoint, EntrypointConfig)
26
+ assert config.entrypoint.file == "server.py"
27
+ assert config.entrypoint.object is None
28
+
29
+ # get_entrypoint should return the same object
30
+ entrypoint = config.get_entrypoint()
31
+ assert isinstance(entrypoint, EntrypointConfig)
32
+ assert entrypoint.file == "server.py"
33
+ assert entrypoint.object is None
34
+
35
+ def test_string_entrypoint_with_object(self):
36
+ """Test string entrypoint with :object syntax."""
37
+ config = FastMCPConfig(entrypoint="server.py:app")
38
+ # With the new validator, this should be converted to EntrypointConfig
39
+ assert isinstance(config.entrypoint, EntrypointConfig)
40
+ assert config.entrypoint.file == "server.py"
41
+ assert config.entrypoint.object == "app"
42
+
43
+ # get_entrypoint should return the same object
44
+ entrypoint = config.get_entrypoint()
45
+ assert isinstance(entrypoint, EntrypointConfig)
46
+ assert entrypoint.file == "server.py"
47
+ assert entrypoint.object == "app"
48
+
49
+ def test_object_entrypoint(self):
50
+ """Test EntrypointConfig object format."""
51
+ config = FastMCPConfig(
52
+ entrypoint=EntrypointConfig(file="src/server.py", object="mcp")
53
+ )
54
+ assert isinstance(config.entrypoint, EntrypointConfig)
55
+ assert config.entrypoint.file == "src/server.py"
56
+ assert config.entrypoint.object == "mcp"
57
+
58
+ def test_get_entrypoint_path_resolution(self, tmp_path):
59
+ """Test that get_entrypoint resolves paths relative to config file."""
60
+ config_dir = tmp_path / "config"
61
+ config_dir.mkdir()
62
+ server_dir = tmp_path / "src"
63
+ server_dir.mkdir()
64
+ server_file = server_dir / "server.py"
65
+ server_file.write_text("# server")
66
+
67
+ config = FastMCPConfig(entrypoint="../src/server.py")
68
+ entrypoint = config.get_entrypoint(config_dir / "fastmcp.json")
69
+
70
+ # Should resolve to absolute path
71
+ assert Path(entrypoint.file).is_absolute()
72
+ assert Path(entrypoint.file) == server_file.resolve()
73
+
74
+
75
+ class TestEnvironmentConfig:
76
+ """Test EnvironmentConfig class."""
77
+
78
+ def test_environment_config_fields(self):
79
+ """Test all EnvironmentConfig fields."""
80
+ config = FastMCPConfig(
81
+ entrypoint="server.py",
82
+ environment={
83
+ "python": "3.12",
84
+ "dependencies": ["requests", "numpy>=2.0"],
85
+ "requirements": "requirements.txt",
86
+ "project": ".",
87
+ "editable": "../my-package",
88
+ },
89
+ )
90
+
91
+ env = config.environment
92
+ assert env.python == "3.12"
93
+ assert env.dependencies == ["requests", "numpy>=2.0"]
94
+ assert env.requirements == "requirements.txt"
95
+ assert env.project == "."
96
+ assert env.editable == "../my-package"
97
+
98
+ def test_needs_uv(self):
99
+ """Test needs_uv() method."""
100
+ # No environment config - doesn't need UV
101
+ config = FastMCPConfig(entrypoint="server.py")
102
+ assert not config.environment.needs_uv()
103
+
104
+ # Empty environment - doesn't need UV
105
+ config = FastMCPConfig(entrypoint="server.py", environment={})
106
+ assert not config.environment.needs_uv()
107
+
108
+ # With dependencies - needs UV
109
+ config = FastMCPConfig(
110
+ entrypoint="server.py", environment={"dependencies": ["requests"]}
111
+ )
112
+ assert config.environment.needs_uv()
113
+
114
+ # With Python version - needs UV
115
+ config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
116
+ assert config.environment.needs_uv()
117
+
118
+ def test_build_uv_args(self):
119
+ """Test build_uv_args() method."""
120
+ config = FastMCPConfig(
121
+ entrypoint="server.py",
122
+ environment={
123
+ "python": "3.12",
124
+ "dependencies": ["requests", "numpy"],
125
+ "requirements": "requirements.txt",
126
+ "project": ".",
127
+ },
128
+ )
129
+
130
+ args = config.environment.build_uv_args(["fastmcp", "run", "server.py"])
131
+
132
+ assert args[0] == "run"
133
+ assert "--python" in args
134
+ assert "3.12" in args
135
+ assert "--project" in args
136
+ assert "--with" in args
137
+ assert "fastmcp" in args
138
+ assert "requests" in args
139
+ assert "numpy" in args
140
+ assert "--with-requirements" in args
141
+ assert "requirements.txt" in args
142
+ assert "fastmcp" in args[-3:]
143
+ assert "run" in args[-2:]
144
+ assert "server.py" in args[-1:]
145
+
146
+ def test_merge_with_cli_args(self):
147
+ """Test merge_with_cli_args() method."""
148
+ config = FastMCPConfig(
149
+ entrypoint="server.py",
150
+ environment={
151
+ "python": "3.11",
152
+ "dependencies": ["requests"],
153
+ },
154
+ )
155
+
156
+ # CLI args should take precedence
157
+ merged = config.environment.merge_with_cli_args(
158
+ python="3.12", # Override
159
+ with_packages=["numpy"], # Add to dependencies
160
+ with_requirements=None,
161
+ project=None,
162
+ )
163
+
164
+ assert merged["python"] == "3.12" # CLI override
165
+ assert set(merged["with_packages"]) == {"requests", "numpy"} # Merged
166
+ assert merged["with_requirements"] is None
167
+ assert merged["project"] is None
168
+
169
+ def test_run_with_uv(self):
170
+ """Test run_with_uv() subprocess execution."""
171
+ config = FastMCPConfig(
172
+ entrypoint="server.py", environment={"dependencies": ["requests"]}
173
+ )
174
+
175
+ # run_with_uv calls sys.exit, so we expect SystemExit
176
+ with pytest.raises(SystemExit) as exc_info:
177
+ # This will fail because we're running exit(1)
178
+ # but it tests that the subprocess is called correctly
179
+ config.environment.run_with_uv(["python", "-c", "exit(1)"])
180
+
181
+ # Check that it exited with code 1
182
+ assert exc_info.value.code == 1
183
+
184
+
185
+ class TestDeploymentConfig:
186
+ """Test DeploymentConfig class."""
187
+
188
+ def test_deployment_config_fields(self):
189
+ """Test all DeploymentConfig fields."""
190
+ config = FastMCPConfig(
191
+ entrypoint="server.py",
192
+ deployment={
193
+ "transport": "http",
194
+ "host": "0.0.0.0",
195
+ "port": 8000,
196
+ "path": "/api/",
197
+ "log_level": "DEBUG",
198
+ "env": {"API_KEY": "secret"},
199
+ "cwd": "./work",
200
+ "args": ["--debug"],
201
+ },
202
+ )
203
+
204
+ deploy = config.deployment
205
+ assert deploy.transport == "http"
206
+ assert deploy.host == "0.0.0.0"
207
+ assert deploy.port == 8000
208
+ assert deploy.path == "/api/"
209
+ assert deploy.log_level == "DEBUG"
210
+ assert deploy.env == {"API_KEY": "secret"}
211
+ assert deploy.cwd == "./work"
212
+ assert deploy.args == ["--debug"]
213
+
214
+ def test_merge_with_cli_args(self):
215
+ """Test DeploymentConfig merge_with_cli_args() method."""
216
+ config = FastMCPConfig(
217
+ entrypoint="server.py",
218
+ deployment={
219
+ "transport": "stdio",
220
+ "port": 3000,
221
+ "log_level": "INFO",
222
+ },
223
+ )
224
+
225
+ # CLI args should take precedence
226
+ merged = config.deployment.merge_with_cli_args(
227
+ transport="http", # Override
228
+ host="localhost", # New value
229
+ port=None, # Keep config value
230
+ path=None,
231
+ log_level="DEBUG", # Override
232
+ server_args=["--test"],
233
+ )
234
+
235
+ assert merged["transport"] == "http" # CLI override
236
+ assert merged["host"] == "localhost" # CLI value
237
+ assert merged["port"] == 3000 # Config value (CLI was None)
238
+ assert merged["log_level"] == "DEBUG" # CLI override
239
+ assert merged["server_args"] == ["--test"] # CLI value
240
+
241
+ def test_apply_runtime_settings(self, tmp_path):
242
+ """Test apply_runtime_settings() method."""
243
+ import os
244
+
245
+ # Create config with env vars and cwd
246
+ work_dir = tmp_path / "work"
247
+ work_dir.mkdir()
248
+
249
+ config = FastMCPConfig(
250
+ entrypoint="server.py",
251
+ deployment={
252
+ "env": {"TEST_VAR": "test_value"},
253
+ "cwd": "work",
254
+ },
255
+ )
256
+
257
+ original_cwd = os.getcwd()
258
+ original_env = os.environ.get("TEST_VAR")
259
+
260
+ try:
261
+ config.deployment.apply_runtime_settings(tmp_path / "fastmcp.json")
262
+
263
+ # Check environment variable was set
264
+ assert os.environ["TEST_VAR"] == "test_value"
265
+
266
+ # Check working directory was changed
267
+ assert Path.cwd() == work_dir.resolve()
268
+
269
+ finally:
270
+ # Restore original state
271
+ os.chdir(original_cwd)
272
+ if original_env is None:
273
+ os.environ.pop("TEST_VAR", None)
274
+ else:
275
+ os.environ["TEST_VAR"] = original_env
276
+
277
+ def test_env_var_interpolation(self, tmp_path):
278
+ """Test environment variable interpolation in deployment env."""
279
+ import os
280
+
281
+ # Set up test environment variables
282
+ os.environ["BASE_URL"] = "example.com"
283
+ os.environ["ENV_NAME"] = "production"
284
+
285
+ config = FastMCPConfig(
286
+ entrypoint="server.py",
287
+ deployment={
288
+ "env": {
289
+ "API_URL": "https://api.${BASE_URL}/v1",
290
+ "DATABASE": "postgres://${ENV_NAME}.db",
291
+ "PREFIXED": "MY_${ENV_NAME}_SERVER",
292
+ "MISSING": "value_${NONEXISTENT}_here",
293
+ "STATIC": "no_interpolation",
294
+ }
295
+ },
296
+ )
297
+
298
+ original_values = {
299
+ key: os.environ.get(key)
300
+ for key in ["API_URL", "DATABASE", "PREFIXED", "MISSING", "STATIC"]
301
+ }
302
+
303
+ try:
304
+ config.deployment.apply_runtime_settings()
305
+
306
+ # Check interpolated values
307
+ assert os.environ["API_URL"] == "https://api.example.com/v1"
308
+ assert os.environ["DATABASE"] == "postgres://production.db"
309
+ assert os.environ["PREFIXED"] == "MY_production_SERVER"
310
+ # Missing variables should keep the placeholder
311
+ assert os.environ["MISSING"] == "value_${NONEXISTENT}_here"
312
+ # Static values should remain unchanged
313
+ assert os.environ["STATIC"] == "no_interpolation"
314
+
315
+ finally:
316
+ # Clean up
317
+ os.environ.pop("BASE_URL", None)
318
+ os.environ.pop("ENV_NAME", None)
319
+ for key, value in original_values.items():
320
+ if value is None:
321
+ os.environ.pop(key, None)
322
+ else:
323
+ os.environ[key] = value
324
+
325
+
326
+ class TestFastMCPConfig:
327
+ """Test FastMCPConfig root configuration."""
328
+
329
+ def test_minimal_config(self):
330
+ """Test creating a config with only required fields."""
331
+ config = FastMCPConfig(entrypoint="server.py")
332
+ assert isinstance(config.entrypoint, EntrypointConfig)
333
+ assert config.entrypoint.file == "server.py"
334
+ assert config.entrypoint.object is None
335
+ # Environment and deployment are now always present but empty
336
+ assert isinstance(config.environment, EnvironmentConfig)
337
+ assert isinstance(config.deployment, DeploymentConfig)
338
+ # Check they have no values set
339
+ assert not config.environment.needs_uv()
340
+ assert all(
341
+ getattr(config.deployment, field, None) is None
342
+ for field in DeploymentConfig.model_fields
343
+ )
344
+
345
+ def test_nested_structure(self):
346
+ """Test the nested configuration structure."""
347
+ config = FastMCPConfig(
348
+ entrypoint="server.py",
349
+ environment={
350
+ "python": "3.12",
351
+ "dependencies": ["fastmcp"],
352
+ },
353
+ deployment={
354
+ "transport": "stdio",
355
+ "log_level": "INFO",
356
+ },
357
+ )
358
+
359
+ assert isinstance(config.entrypoint, EntrypointConfig)
360
+ assert config.entrypoint.file == "server.py"
361
+ assert config.entrypoint.object is None
362
+ assert isinstance(config.environment, EnvironmentConfig)
363
+ assert isinstance(config.deployment, DeploymentConfig)
364
+
365
+ def test_from_file(self, tmp_path):
366
+ """Test loading config from JSON file with nested structure."""
367
+ config_data = {
368
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
369
+ "entrypoint": {"file": "src/server.py", "object": "app"},
370
+ "environment": {"python": "3.12", "dependencies": ["requests"]},
371
+ "deployment": {"transport": "http", "port": 8000},
372
+ }
373
+
374
+ config_file = tmp_path / "fastmcp.json"
375
+ config_file.write_text(json.dumps(config_data))
376
+
377
+ config = FastMCPConfig.from_file(config_file)
378
+
379
+ # When loaded from JSON with object format, it becomes EntrypointConfig
380
+ assert isinstance(config.entrypoint, EntrypointConfig)
381
+ assert config.entrypoint.file == "src/server.py"
382
+ assert config.entrypoint.object == "app"
383
+ assert config.environment.python == "3.12"
384
+ assert config.environment.dependencies == ["requests"]
385
+ assert config.deployment.transport == "http"
386
+ assert config.deployment.port == 8000
387
+
388
+ def test_from_file_with_string_entrypoint(self, tmp_path):
389
+ """Test loading config with string entrypoint."""
390
+ config_data = {
391
+ "entrypoint": "server.py:mcp",
392
+ "environment": {"dependencies": ["fastmcp"]},
393
+ }
394
+
395
+ config_file = tmp_path / "fastmcp.json"
396
+ config_file.write_text(json.dumps(config_data))
397
+
398
+ config = FastMCPConfig.from_file(config_file)
399
+ # String entrypoint with : should be converted to EntrypointConfig
400
+ assert isinstance(config.entrypoint, EntrypointConfig)
401
+ assert config.entrypoint.file == "server.py"
402
+ assert config.entrypoint.object == "mcp"
403
+
404
+ # get_entrypoint should return the same
405
+ entrypoint = config.get_entrypoint()
406
+ assert entrypoint.file == "server.py"
407
+ assert entrypoint.object == "mcp"
408
+
409
+ def test_string_entrypoint_with_object_and_environment(self, tmp_path):
410
+ """Test that file.py:object syntax works with environment config."""
411
+ config_data = {
412
+ "entrypoint": "src/server.py:app",
413
+ "environment": {"python": "3.12", "dependencies": ["fastmcp", "requests"]},
414
+ "deployment": {"transport": "http", "port": 8000},
415
+ }
416
+
417
+ config_file = tmp_path / "fastmcp.json"
418
+ config_file.write_text(json.dumps(config_data))
419
+
420
+ config = FastMCPConfig.from_file(config_file)
421
+
422
+ # Should be parsed into EntrypointConfig
423
+ assert isinstance(config.entrypoint, EntrypointConfig)
424
+ assert config.entrypoint.file == "src/server.py"
425
+ assert config.entrypoint.object == "app"
426
+
427
+ # Environment config should still work
428
+ assert config.environment.python == "3.12"
429
+ assert config.environment.dependencies == ["fastmcp", "requests"]
430
+
431
+ # Deployment config should still work
432
+ assert config.deployment.transport == "http"
433
+ assert config.deployment.port == 8000
434
+
435
+ def test_find_config_in_current_dir(self, tmp_path):
436
+ """Test finding config in current directory."""
437
+ config_file = tmp_path / "fastmcp.json"
438
+ config_file.write_text(json.dumps({"entrypoint": "server.py"}))
439
+
440
+ original_cwd = os.getcwd()
441
+ try:
442
+ os.chdir(tmp_path)
443
+ found = FastMCPConfig.find_config()
444
+ assert found == config_file
445
+ finally:
446
+ os.chdir(original_cwd)
447
+
448
+ def test_find_config_not_in_parent_dir(self, tmp_path):
449
+ """Test that config is NOT found in parent directory."""
450
+ config_file = tmp_path / "fastmcp.json"
451
+ config_file.write_text(json.dumps({"entrypoint": "server.py"}))
452
+
453
+ subdir = tmp_path / "subdir"
454
+ subdir.mkdir()
455
+
456
+ # Should NOT find config in parent directory
457
+ found = FastMCPConfig.find_config(subdir)
458
+ assert found is None
459
+
460
+ def test_find_config_in_specified_dir(self, tmp_path):
461
+ """Test finding config in the specified directory."""
462
+ config_file = tmp_path / "fastmcp.json"
463
+ config_file.write_text(json.dumps({"entrypoint": "server.py"}))
464
+
465
+ # Should find config when looking in the directory that contains it
466
+ found = FastMCPConfig.find_config(tmp_path)
467
+ assert found == config_file
468
+
469
+ def test_find_config_not_found(self, tmp_path):
470
+ """Test when config is not found."""
471
+ found = FastMCPConfig.find_config(tmp_path)
472
+ assert found is None
473
+
474
+ def test_invalid_transport(self, tmp_path):
475
+ """Test loading config with invalid transport value."""
476
+ config_data = {
477
+ "entrypoint": "server.py",
478
+ "deployment": {"transport": "invalid_transport"},
479
+ }
480
+
481
+ config_file = tmp_path / "fastmcp.json"
482
+ config_file.write_text(json.dumps(config_data))
483
+
484
+ with pytest.raises(ValidationError):
485
+ FastMCPConfig.from_file(config_file)
486
+
487
+ def test_optional_sections(self):
488
+ """Test that all config sections are optional except entrypoint."""
489
+ # Only entrypoint is required
490
+ config = FastMCPConfig(entrypoint="server.py")
491
+ assert isinstance(config.entrypoint, EntrypointConfig)
492
+ assert config.entrypoint.file == "server.py"
493
+ # Environment and deployment are now always present but may be empty
494
+ assert isinstance(config.environment, EnvironmentConfig)
495
+ assert isinstance(config.deployment, DeploymentConfig)
496
+
497
+ # Only environment with values
498
+ config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
499
+ assert config.environment.python == "3.12"
500
+ assert isinstance(config.deployment, DeploymentConfig)
501
+ assert all(
502
+ getattr(config.deployment, field, None) is None
503
+ for field in DeploymentConfig.model_fields
504
+ )
505
+
506
+ # Only deployment with values
507
+ config = FastMCPConfig(entrypoint="server.py", deployment={"transport": "http"})
508
+ assert isinstance(config.environment, EnvironmentConfig)
509
+ assert all(
510
+ getattr(config.environment, field, None) is None
511
+ for field in EnvironmentConfig.model_fields
512
+ )
513
+ assert config.deployment.transport == "http"
tests/cli/test_fastmcp_config_integration.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Integration tests for fastmcp.json configuration system."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from fastmcp.client import Client
10
+ from fastmcp.utilities.fastmcp_config import FastMCPConfig
11
+
12
+
13
+ @pytest.fixture
14
+ def server_with_config(tmp_path):
15
+ """Create a complete server setup with fastmcp.json config."""
16
+ # Create server file
17
+ server_file = tmp_path / "server.py"
18
+ server_file.write_text("""
19
+ from fastmcp import FastMCP
20
+
21
+ mcp = FastMCP("Config Test Server")
22
+
23
+ @mcp.tool
24
+ def hello(name: str = "World") -> str:
25
+ '''Say hello to someone'''
26
+ return f"Hello, {name}!"
27
+
28
+ @mcp.resource("resource://greeting")
29
+ def get_greeting() -> str:
30
+ '''Get a greeting message'''
31
+ return "Welcome to FastMCP!"
32
+
33
+ if __name__ == "__main__":
34
+ import asyncio
35
+ asyncio.run(mcp.run_async())
36
+ """)
37
+
38
+ # Create config file
39
+ config_data = {
40
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
41
+ "entrypoint": "server.py",
42
+ "environment": {
43
+ "python": sys.version.split()[0], # Use current Python version
44
+ "dependencies": ["fastmcp"],
45
+ },
46
+ "deployment": {"transport": "stdio", "log_level": "INFO"},
47
+ }
48
+
49
+ config_file = tmp_path / "fastmcp.json"
50
+ config_file.write_text(json.dumps(config_data, indent=2))
51
+
52
+ return tmp_path
53
+
54
+
55
+ class TestConfigFileDetection:
56
+ """Test configuration file detection patterns."""
57
+
58
+ def test_detect_standard_fastmcp_json(self, tmp_path):
59
+ """Test detection of standard fastmcp.json file."""
60
+ config_file = tmp_path / "fastmcp.json"
61
+ config_file.write_text(json.dumps({"entrypoint": "server.py"}))
62
+
63
+ # Should be detected as fastmcp config
64
+ assert "fastmcp.json" in config_file.name
65
+ assert config_file.name.endswith("fastmcp.json")
66
+
67
+ def test_detect_prefixed_fastmcp_json(self, tmp_path):
68
+ """Test detection of prefixed fastmcp.json files."""
69
+ config_file = tmp_path / "my.fastmcp.json"
70
+ config_file.write_text(json.dumps({"entrypoint": "server.py"}))
71
+
72
+ # Should be detected as fastmcp config
73
+ assert "fastmcp.json" in config_file.name
74
+
75
+ def test_detect_test_fastmcp_json(self, tmp_path):
76
+ """Test detection of test_fastmcp.json file."""
77
+ config_file = tmp_path / "test_fastmcp.json"
78
+ config_file.write_text(json.dumps({"entrypoint": "server.py"}))
79
+
80
+ # Should be detected as fastmcp config
81
+ assert "fastmcp.json" in config_file.name
82
+
83
+
84
+ class TestConfigWithClient:
85
+ """Test fastmcp.json configuration with client connections."""
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_config_server_with_client(self, server_with_config):
89
+ """Test that a server loaded from config works with a client."""
90
+ # Load the config
91
+ config_file = server_with_config / "fastmcp.json"
92
+ config = FastMCPConfig.from_file(config_file)
93
+
94
+ # Import the server using the entrypoint
95
+ import importlib.util
96
+ import sys
97
+
98
+ entrypoint = config.get_entrypoint(config_file)
99
+ spec = importlib.util.spec_from_file_location("test_server", entrypoint.file)
100
+ if spec is None or spec.loader is None:
101
+ raise RuntimeError(f"Could not load module from {entrypoint.file}")
102
+ module = importlib.util.module_from_spec(spec)
103
+ sys.modules["test_server"] = module
104
+ spec.loader.exec_module(module)
105
+
106
+ server = module.mcp
107
+
108
+ # Connect client to server
109
+ async with Client(server) as client:
110
+ # Test tool
111
+ result = await client.call_tool("hello", {"name": "FastMCP"})
112
+ assert result.data == "Hello, FastMCP!" # Use .data for string result
113
+
114
+ # Test resource
115
+ results = await client.read_resource("resource://greeting")
116
+ assert len(results) == 1
117
+ # Resource results should have text content
118
+ assert hasattr(results[0], "text") or hasattr(results[0], "contents")
119
+ # Get the text content from the resource
120
+ text = getattr(results[0], "text", None) or getattr(
121
+ results[0], "contents", ""
122
+ )
123
+ assert "Welcome to FastMCP!" in str(text)
124
+
125
+
126
+ class TestEnvironmentExecution:
127
+ """Test environment configuration execution paths."""
128
+
129
+ def test_needs_uv_with_dependencies(self):
130
+ """Test that environment with dependencies needs UV."""
131
+ config = FastMCPConfig(
132
+ entrypoint="server.py",
133
+ environment={"dependencies": ["requests", "numpy"]}, # type: ignore[arg-type]
134
+ )
135
+
136
+ assert config.environment is not None
137
+ assert config.environment.needs_uv()
138
+
139
+ def test_needs_uv_with_python_version(self):
140
+ """Test that environment with Python version needs UV."""
141
+ config = FastMCPConfig(
142
+ entrypoint="server.py",
143
+ environment={"python": "3.12"}, # type: ignore[arg-type]
144
+ )
145
+
146
+ assert config.environment is not None
147
+ assert config.environment.needs_uv()
148
+
149
+ def test_no_uv_needed_without_environment(self):
150
+ """Test that no UV is needed without environment config."""
151
+ config = FastMCPConfig(entrypoint="server.py")
152
+
153
+ # Environment is now always present but may be empty
154
+ assert config.environment is not None
155
+ assert not config.environment.needs_uv()
156
+
157
+ def test_no_uv_needed_with_empty_environment(self):
158
+ """Test that no UV is needed with empty environment config."""
159
+ config = FastMCPConfig(
160
+ entrypoint="server.py",
161
+ environment={}, # type: ignore[arg-type]
162
+ )
163
+
164
+ assert config.environment is not None
165
+ assert not config.environment.needs_uv()
166
+
167
+
168
+ class TestCLIArgumentMerging:
169
+ """Test CLI argument merging with config values."""
170
+
171
+ def test_cli_overrides_environment(self):
172
+ """Test that CLI args override environment config."""
173
+ config = FastMCPConfig(
174
+ entrypoint="server.py",
175
+ environment={"python": "3.11", "dependencies": ["requests"]}, # type: ignore[arg-type]
176
+ )
177
+
178
+ assert config.environment is not None
179
+ merged = config.environment.merge_with_cli_args(
180
+ python="3.12", # Override Python version
181
+ with_packages=["numpy"], # Add package
182
+ with_requirements=None,
183
+ project=None,
184
+ )
185
+
186
+ assert merged["python"] == "3.12" # CLI wins
187
+ assert "requests" in merged["with_packages"] # From config
188
+ assert "numpy" in merged["with_packages"] # From CLI
189
+
190
+ def test_cli_overrides_deployment(self):
191
+ """Test that CLI args override deployment config."""
192
+ config = FastMCPConfig(
193
+ entrypoint="server.py",
194
+ deployment={"transport": "stdio", "port": 3000, "log_level": "INFO"}, # type: ignore[arg-type]
195
+ )
196
+
197
+ assert config.deployment is not None
198
+ merged = config.deployment.merge_with_cli_args(
199
+ transport="http", # Override transport
200
+ host="localhost", # New value
201
+ port=8080, # Override port
202
+ path=None,
203
+ log_level="DEBUG", # Override log level
204
+ server_args=None,
205
+ )
206
+
207
+ assert merged["transport"] == "http" # CLI wins
208
+ assert merged["host"] == "localhost" # CLI value
209
+ assert merged["port"] == 8080 # CLI wins
210
+ assert merged["log_level"] == "DEBUG" # CLI wins
211
+
212
+ def test_config_values_when_cli_is_none(self):
213
+ """Test that config values are used when CLI args are None."""
214
+ config = FastMCPConfig(
215
+ entrypoint="server.py",
216
+ deployment={"transport": "http", "port": 3000}, # type: ignore[arg-type]
217
+ )
218
+
219
+ assert config.deployment is not None
220
+ merged = config.deployment.merge_with_cli_args(
221
+ transport=None, # Use config
222
+ host=None, # No value
223
+ port=None, # Use config
224
+ path=None,
225
+ log_level=None,
226
+ server_args=None,
227
+ )
228
+
229
+ assert merged["transport"] == "http" # From config
230
+ assert merged["port"] == 3000 # From config
231
+ assert merged["host"] is None # No value provided
232
+
233
+
234
+ class TestPathResolution:
235
+ """Test path resolution in configurations."""
236
+
237
+ def test_entrypoint_path_resolution(self, tmp_path):
238
+ """Test that entrypoint paths are resolved relative to config."""
239
+ # Create nested directory structure
240
+ config_dir = tmp_path / "config"
241
+ config_dir.mkdir()
242
+ src_dir = tmp_path / "src"
243
+ src_dir.mkdir()
244
+
245
+ # Server is in src, config is in config
246
+ server_file = src_dir / "server.py"
247
+ server_file.write_text("# Server")
248
+
249
+ config = FastMCPConfig(entrypoint="../src/server.py")
250
+
251
+ # Get entrypoint resolved relative to config location
252
+ config_file = config_dir / "fastmcp.json"
253
+ entrypoint = config.get_entrypoint(config_file)
254
+
255
+ # Should resolve to absolute path of server file
256
+ assert Path(entrypoint.file) == server_file.resolve()
257
+
258
+ def test_cwd_path_resolution(self, tmp_path):
259
+ """Test that working directory is resolved relative to config."""
260
+ import os
261
+
262
+ # Create directory structure
263
+ work_dir = tmp_path / "work"
264
+ work_dir.mkdir()
265
+
266
+ config = FastMCPConfig(
267
+ entrypoint="server.py",
268
+ deployment={"cwd": "work"}, # type: ignore[arg-type]
269
+ )
270
+
271
+ original_cwd = os.getcwd()
272
+
273
+ try:
274
+ # Apply runtime settings relative to config location
275
+ assert config.deployment is not None
276
+ config.deployment.apply_runtime_settings(tmp_path / "fastmcp.json")
277
+
278
+ # Should change to work directory
279
+ assert Path.cwd() == work_dir.resolve()
280
+
281
+ finally:
282
+ os.chdir(original_cwd)
283
+
284
+ def test_requirements_path_resolution(self, tmp_path):
285
+ """Test that requirements path is resolved correctly."""
286
+ # Create requirements file
287
+ reqs_file = tmp_path / "requirements.txt"
288
+ reqs_file.write_text("fastmcp>=2.0")
289
+
290
+ config = FastMCPConfig(
291
+ entrypoint="server.py",
292
+ environment={"requirements": "requirements.txt"}, # type: ignore[arg-type]
293
+ )
294
+
295
+ # Build UV args
296
+ assert config.environment is not None
297
+ uv_args = config.environment.build_uv_args(["fastmcp", "run"])
298
+
299
+ # Should include requirements file
300
+ assert "--with-requirements" in uv_args
301
+ req_idx = uv_args.index("--with-requirements") + 1
302
+ assert uv_args[req_idx] == "requirements.txt"
303
+
304
+
305
+ class TestConfigValidation:
306
+ """Test configuration validation."""
307
+
308
+ def test_invalid_transport_rejected(self):
309
+ """Test that invalid transport values are rejected."""
310
+ with pytest.raises(ValueError):
311
+ FastMCPConfig(
312
+ entrypoint="server.py",
313
+ deployment={"transport": "invalid_transport"}, # type: ignore[arg-type]
314
+ )
315
+
316
+ def test_streamable_http_transport_rejected(self):
317
+ """Test that streamable-http transport is rejected in fastmcp.json config."""
318
+ with pytest.raises(ValueError):
319
+ FastMCPConfig(
320
+ entrypoint="server.py",
321
+ deployment={"transport": "streamable-http"}, # type: ignore[arg-type]
322
+ )
323
+
324
+ def test_invalid_log_level_rejected(self):
325
+ """Test that invalid log level values are rejected."""
326
+ with pytest.raises(ValueError):
327
+ FastMCPConfig(
328
+ entrypoint="server.py",
329
+ deployment={"log_level": "INVALID"}, # type: ignore[arg-type]
330
+ )
331
+
332
+ def test_missing_entrypoint_rejected(self):
333
+ """Test that config without entrypoint is rejected."""
334
+ with pytest.raises(ValueError):
335
+ FastMCPConfig() # type: ignore[call-arg]
336
+
337
+ def test_valid_transport_values(self):
338
+ """Test that all valid transport values are accepted."""
339
+ for transport in ["stdio", "http", "sse"]:
340
+ config = FastMCPConfig(
341
+ entrypoint="server.py",
342
+ deployment={"transport": transport}, # type: ignore[arg-type]
343
+ )
344
+ assert config.deployment is not None
345
+ assert config.deployment.transport == transport
346
+
347
+ def test_valid_log_levels(self):
348
+ """Test that all valid log levels are accepted."""
349
+ for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
350
+ config = FastMCPConfig(
351
+ entrypoint="server.py",
352
+ deployment={"log_level": level}, # type: ignore[arg-type]
353
+ )
354
+ assert config.deployment is not None
355
+ assert config.deployment.log_level == level
tests/cli/test_fastmcp_config_schema.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test that the JSON schema file matches the Pydantic model."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import generate_schema
7
+
8
+
9
+ def test_schema_file_matches_pydantic_model():
10
+ """Test that the schema.json file matches what the Pydantic model generates."""
11
+ # Path to the schema file
12
+ schema_file = (
13
+ Path(__file__).parent.parent.parent
14
+ / "src"
15
+ / "fastmcp"
16
+ / "utilities"
17
+ / "fastmcp_config"
18
+ / "v1"
19
+ / "schema.json"
20
+ )
21
+
22
+ # Load the schema file
23
+ with open(schema_file) as f:
24
+ file_schema = json.load(f)
25
+
26
+ # Generate schema from Pydantic model
27
+ generated_schema = generate_schema()
28
+
29
+ # They should be identical
30
+ assert file_schema == generated_schema, (
31
+ "The schema.json file does not match the Pydantic model schema. "
32
+ "Please regenerate the schema file by running:\n"
33
+ 'uv run python -c "from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import generate_schema; '
34
+ 'import json; print(json.dumps(generate_schema(), indent=2))" > '
35
+ f"{schema_file}"
36
+ )
37
+
38
+
39
+ def test_schema_has_correct_id():
40
+ """Test that the schema has the correct $id field."""
41
+ generated_schema = generate_schema()
42
+
43
+ assert "$id" in generated_schema
44
+ assert (
45
+ generated_schema["$id"]
46
+ == "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
47
+ )
48
+
49
+
50
+ def test_schema_has_required_fields():
51
+ """Test that the schema specifies the required fields correctly."""
52
+ generated_schema = generate_schema()
53
+
54
+ # Check that entrypoint is required
55
+ assert "required" in generated_schema
56
+ assert "entrypoint" in generated_schema["required"]
57
+
58
+ # Check that entrypoint is in properties
59
+ assert "properties" in generated_schema
60
+ assert "entrypoint" in generated_schema["properties"]
61
+
62
+
63
+ def test_schema_nested_structure():
64
+ """Test that the schema has the correct nested structure."""
65
+ generated_schema = generate_schema()
66
+
67
+ properties = generated_schema["properties"]
68
+
69
+ # Check environment section
70
+ assert "environment" in properties
71
+ env_schema = properties["environment"]
72
+ if "properties" in env_schema:
73
+ env_props = env_schema["properties"]
74
+ assert "python" in env_props
75
+ assert "dependencies" in env_props
76
+ assert "requirements" in env_props
77
+ assert "project" in env_props
78
+ assert "editable" in env_props
79
+
80
+ # Check deployment section
81
+ assert "deployment" in properties
82
+ deploy_schema = properties["deployment"]
83
+ if "properties" in deploy_schema:
84
+ deploy_props = deploy_schema["properties"]
85
+ assert "transport" in deploy_props
86
+ assert "host" in deploy_props
87
+ assert "port" in deploy_props
88
+ assert "log_level" in deploy_props
89
+ assert "env" in deploy_props
90
+ assert "cwd" in deploy_props
91
+ assert "args" in deploy_props
92
+
93
+
94
+ def test_schema_transport_enum():
95
+ """Test that transport field has correct enum values."""
96
+ generated_schema = generate_schema()
97
+
98
+ # Navigate to transport field
99
+ deploy_schema = generated_schema["properties"]["deployment"]
100
+
101
+ # Handle both direct properties and anyOf cases
102
+ if "anyOf" in deploy_schema:
103
+ # Find the object type in anyOf
104
+ for option in deploy_schema["anyOf"]:
105
+ if option.get("type") == "object" and "properties" in option:
106
+ transport_schema = option["properties"].get("transport", {})
107
+ if "anyOf" in transport_schema:
108
+ # Look for enum in anyOf options
109
+ for trans_option in transport_schema["anyOf"]:
110
+ if "enum" in trans_option:
111
+ valid_transports = trans_option["enum"]
112
+ assert "stdio" in valid_transports
113
+ assert "http" in valid_transports
114
+ assert "sse" in valid_transports
115
+ break
116
+ elif "properties" in deploy_schema:
117
+ transport_schema = deploy_schema["properties"].get("transport", {})
118
+ if "anyOf" in transport_schema:
119
+ for option in transport_schema["anyOf"]:
120
+ if "enum" in option:
121
+ valid_transports = option["enum"]
122
+ assert "stdio" in valid_transports
123
+ assert "http" in valid_transports
124
+ assert "sse" in valid_transports
125
+ break
126
+
127
+
128
+ def test_schema_log_level_enum():
129
+ """Test that log_level field has correct enum values."""
130
+ generated_schema = generate_schema()
131
+
132
+ # Navigate to log_level field
133
+ deploy_schema = generated_schema["properties"]["deployment"]
134
+
135
+ # Handle both direct properties and anyOf cases
136
+ if "anyOf" in deploy_schema:
137
+ # Find the object type in anyOf
138
+ for option in deploy_schema["anyOf"]:
139
+ if option.get("type") == "object" and "properties" in option:
140
+ log_level_schema = option["properties"].get("log_level", {})
141
+ if "anyOf" in log_level_schema:
142
+ # Look for enum in anyOf options
143
+ for level_option in log_level_schema["anyOf"]:
144
+ if "enum" in level_option:
145
+ valid_levels = level_option["enum"]
146
+ assert "DEBUG" in valid_levels
147
+ assert "INFO" in valid_levels
148
+ assert "WARNING" in valid_levels
149
+ assert "ERROR" in valid_levels
150
+ assert "CRITICAL" in valid_levels
151
+ break
152
+ elif "properties" in deploy_schema:
153
+ log_level_schema = deploy_schema["properties"].get("log_level", {})
154
+ if "anyOf" in log_level_schema:
155
+ for option in log_level_schema["anyOf"]:
156
+ if "enum" in option:
157
+ valid_levels = option["enum"]
158
+ assert "DEBUG" in valid_levels
159
+ assert "INFO" in valid_levels
160
+ assert "WARNING" in valid_levels
161
+ assert "ERROR" in valid_levels
162
+ assert "CRITICAL" in valid_levels
163
+ break
tests/cli/test_run_config.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Integration tests for FastMCP configuration with run command."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from fastmcp.cli.run import load_fastmcp_config
10
+ from fastmcp.utilities.fastmcp_config import (
11
+ DeploymentConfig,
12
+ EntrypointConfig,
13
+ EnvironmentConfig,
14
+ )
15
+
16
+
17
+ @pytest.fixture
18
+ def sample_config(tmp_path):
19
+ """Create a sample fastmcp.json configuration file with nested structure."""
20
+ config_data = {
21
+ "$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
22
+ "entrypoint": "server.py",
23
+ "environment": {"python": "3.11", "dependencies": ["requests"]},
24
+ "deployment": {"transport": "stdio", "env": {"TEST_VAR": "test_value"}},
25
+ }
26
+
27
+ config_file = tmp_path / "fastmcp.json"
28
+ config_file.write_text(json.dumps(config_data, indent=2))
29
+
30
+ # Create a simple server file
31
+ server_file = tmp_path / "server.py"
32
+ server_file.write_text("""
33
+ from fastmcp import FastMCP
34
+
35
+ mcp = FastMCP("Test Server")
36
+
37
+ @mcp.tool
38
+ def test_tool(message: str) -> str:
39
+ return f"Echo: {message}"
40
+ """)
41
+
42
+ return config_file
43
+
44
+
45
+ def test_load_fastmcp_config(sample_config, monkeypatch):
46
+ """Test loading configuration and returning config subsets."""
47
+
48
+ # Capture environment changes
49
+ original_env = dict(os.environ)
50
+
51
+ try:
52
+ entrypoint, deployment, environment = load_fastmcp_config(sample_config)
53
+
54
+ # Check that we got the right types
55
+ assert isinstance(entrypoint, EntrypointConfig)
56
+ assert isinstance(deployment, DeploymentConfig)
57
+ assert isinstance(environment, EnvironmentConfig)
58
+
59
+ # Check entrypoint
60
+ assert entrypoint.file.endswith("server.py")
61
+ assert Path(entrypoint.file).is_absolute()
62
+ assert entrypoint.object is None
63
+
64
+ # Check environment config
65
+ assert environment.python == "3.11"
66
+ assert environment.dependencies == ["requests"]
67
+
68
+ # Check deployment config
69
+ assert deployment.transport == "stdio"
70
+ assert deployment.env == {"TEST_VAR": "test_value"}
71
+
72
+ # Check that environment variables were applied
73
+ assert os.environ.get("TEST_VAR") == "test_value"
74
+
75
+ finally:
76
+ # Restore original environment
77
+ os.environ.clear()
78
+ os.environ.update(original_env)
79
+
80
+
81
+ def test_load_config_with_object_entrypoint(tmp_path):
82
+ """Test loading config with object-format entrypoint."""
83
+ config_data = {
84
+ "entrypoint": {"file": "src/server.py", "object": "app"},
85
+ "deployment": {"transport": "http", "port": 8000},
86
+ }
87
+
88
+ config_file = tmp_path / "fastmcp.json"
89
+ config_file.write_text(json.dumps(config_data))
90
+
91
+ # Create the server file in subdirectory
92
+ src_dir = tmp_path / "src"
93
+ src_dir.mkdir()
94
+ server_file = src_dir / "server.py"
95
+ server_file.write_text("# Server")
96
+
97
+ entrypoint, deployment, environment = load_fastmcp_config(config_file)
98
+
99
+ # Check entrypoint resolution
100
+ assert entrypoint.file == str(server_file.resolve())
101
+ assert entrypoint.object == "app"
102
+
103
+ # Check deployment
104
+ assert deployment is not None
105
+ assert deployment.transport == "http"
106
+ assert deployment.port == 8000
107
+
108
+ # No environment config
109
+ assert environment is None
110
+
111
+
112
+ def test_load_config_with_cwd(tmp_path):
113
+ """Test that DeploymentConfig applies working directory change."""
114
+
115
+ # Create a subdirectory
116
+ subdir = tmp_path / "subdir"
117
+ subdir.mkdir()
118
+
119
+ config_data = {"entrypoint": "server.py", "deployment": {"cwd": "subdir"}}
120
+
121
+ config_file = tmp_path / "fastmcp.json"
122
+ config_file.write_text(json.dumps(config_data))
123
+
124
+ # Create server file in subdirectory
125
+ server_file = subdir / "server.py"
126
+ server_file.write_text("# Test server")
127
+
128
+ original_cwd = os.getcwd()
129
+
130
+ try:
131
+ entrypoint, deployment, environment = load_fastmcp_config(config_file)
132
+
133
+ # Check that working directory was changed
134
+ assert Path.cwd() == subdir.resolve()
135
+
136
+ finally:
137
+ # Restore original working directory
138
+ os.chdir(original_cwd)
139
+
140
+
141
+ def test_load_config_with_relative_cwd(tmp_path):
142
+ """Test configuration with relative working directory."""
143
+
144
+ # Create nested subdirectories
145
+ subdir1 = tmp_path / "dir1"
146
+ subdir2 = subdir1 / "dir2"
147
+ subdir2.mkdir(parents=True)
148
+
149
+ config_data = {
150
+ "entrypoint": "server.py",
151
+ "deployment": {
152
+ "cwd": "../" # Relative to config file location
153
+ },
154
+ }
155
+
156
+ config_file = subdir2 / "fastmcp.json"
157
+ config_file.write_text(json.dumps(config_data))
158
+
159
+ # Create server file in parent directory
160
+ server_file = subdir1 / "server.py"
161
+ server_file.write_text("# Server")
162
+
163
+ original_cwd = os.getcwd()
164
+
165
+ try:
166
+ entrypoint, deployment, environment = load_fastmcp_config(config_file)
167
+
168
+ # Should change to parent directory of config file
169
+ assert Path.cwd() == subdir1.resolve()
170
+
171
+ finally:
172
+ os.chdir(original_cwd)
173
+
174
+
175
+ def test_load_minimal_config(tmp_path):
176
+ """Test loading minimal configuration with only entrypoint."""
177
+ config_data = {"entrypoint": "server.py"}
178
+
179
+ config_file = tmp_path / "fastmcp.json"
180
+ config_file.write_text(json.dumps(config_data))
181
+
182
+ # Create server file
183
+ server_file = tmp_path / "server.py"
184
+ server_file.write_text("# Server")
185
+
186
+ entrypoint, deployment, environment = load_fastmcp_config(config_file)
187
+
188
+ # Check we got entrypoint
189
+ assert isinstance(entrypoint, EntrypointConfig)
190
+ assert entrypoint.file == str(server_file.resolve())
191
+
192
+ # No deployment or environment
193
+ assert deployment is None
194
+ assert environment is None
195
+
196
+
197
+ def test_load_config_with_server_args(tmp_path):
198
+ """Test configuration with server arguments."""
199
+ config_data = {
200
+ "entrypoint": "server.py",
201
+ "deployment": {"args": ["--debug", "--config", "custom.json"]},
202
+ }
203
+
204
+ config_file = tmp_path / "fastmcp.json"
205
+ config_file.write_text(json.dumps(config_data))
206
+
207
+ # Create server file
208
+ server_file = tmp_path / "server.py"
209
+ server_file.write_text("# Server")
210
+
211
+ entrypoint, deployment, environment = load_fastmcp_config(config_file)
212
+
213
+ assert deployment is not None
214
+ assert deployment.args == ["--debug", "--config", "custom.json"]
215
+
216
+
217
+ def test_config_subset_independence(tmp_path):
218
+ """Test that config subsets can be used independently."""
219
+ config_data = {
220
+ "entrypoint": "server.py",
221
+ "environment": {"python": "3.12", "dependencies": ["pandas"]},
222
+ "deployment": {"transport": "http", "host": "0.0.0.0", "port": 3000},
223
+ }
224
+
225
+ config_file = tmp_path / "fastmcp.json"
226
+ config_file.write_text(json.dumps(config_data))
227
+
228
+ # Create server file
229
+ server_file = tmp_path / "server.py"
230
+ server_file.write_text("# Server")
231
+
232
+ entrypoint, deployment, environment = load_fastmcp_config(config_file)
233
+
234
+ # Each subset should be independently usable
235
+ assert entrypoint.file == str(server_file.resolve())
236
+ assert entrypoint.object is None
237
+
238
+ assert environment is not None
239
+ assert environment.python == "3.12"
240
+ assert environment.dependencies == ["pandas"]
241
+ assert environment.needs_uv() # Has dependencies
242
+
243
+ assert deployment is not None
244
+ assert deployment.transport == "http"
245
+ assert deployment.host == "0.0.0.0"
246
+ assert deployment.port == 3000
247
+
248
+ # Can merge deployment config with CLI args
249
+ merged = deployment.merge_with_cli_args(
250
+ transport=None, # Keep config value
251
+ host="localhost", # Override
252
+ port=8080, # Override
253
+ path="/api", # New value
254
+ log_level=None,
255
+ server_args=None,
256
+ )
257
+
258
+ assert merged["transport"] == "http" # Kept from config
259
+ assert merged["host"] == "localhost" # CLI override
260
+ assert merged["port"] == 8080 # CLI override
261
+ assert merged["path"] == "/api" # CLI value
262
+
263
+
264
+ def test_environment_config_path_resolution(tmp_path):
265
+ """Test that paths in environment config are resolved correctly."""
266
+ # Create requirements file
267
+ reqs_file = tmp_path / "requirements.txt"
268
+ reqs_file.write_text("fastmcp>=2.0")
269
+
270
+ config_data = {
271
+ "entrypoint": "server.py",
272
+ "environment": {
273
+ "requirements": "requirements.txt",
274
+ "project": ".",
275
+ "editable": "../other-project",
276
+ },
277
+ }
278
+
279
+ config_file = tmp_path / "fastmcp.json"
280
+ config_file.write_text(json.dumps(config_data))
281
+
282
+ # Create server file
283
+ server_file = tmp_path / "server.py"
284
+ server_file.write_text("# Server")
285
+
286
+ entrypoint, deployment, environment = load_fastmcp_config(config_file)
287
+
288
+ # Check that UV args are built with resolved paths
289
+ assert environment is not None
290
+ uv_args = environment.build_uv_args(["fastmcp", "run", "server.py"])
291
+
292
+ assert "--with-requirements" in uv_args
293
+ assert "--project" in uv_args
294
+ # Path should be resolved relative to config file
295
+ req_idx = uv_args.index("--with-requirements") + 1
296
+ assert (
297
+ Path(uv_args[req_idx]).is_absolute() or uv_args[req_idx] == "requirements.txt"
298
+ )
tests/deprecated/test_dependencies.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for deprecated dependencies parameter.
2
+
3
+ This entire file can be deleted when the dependencies parameter is removed (deprecated in v2.11.4).
4
+ """
5
+
6
+ import warnings
7
+
8
+ import pytest
9
+
10
+ from fastmcp import FastMCP
11
+
12
+
13
+ def test_dependencies_parameter_deprecated():
14
+ """Test that using the dependencies parameter raises a deprecation warning."""
15
+
16
+ with pytest.warns(DeprecationWarning, match="deprecated as of FastMCP 2.11.4"):
17
+ server = FastMCP("Test Server", dependencies=["pandas", "numpy"])
18
+
19
+ # Should still work for backward compatibility
20
+ assert server.dependencies == ["pandas", "numpy"]
21
+
22
+
23
+ def test_no_warning_without_dependencies():
24
+ """Test that no warning is raised when dependencies are not used."""
25
+
26
+ with warnings.catch_warnings():
27
+ warnings.simplefilter("error") # Turn warnings into errors
28
+ server = FastMCP("Test Server") # Should not raise
29
+
30
+ assert server.dependencies == [] # Should use default empty list