svkrishna commited on
Commit
a17b63b
·
unverified ·
2 Parent(s): d817b84c35ac3a

Merge branch 'jlowin:main' into main

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/marvin.yml +3 -1
  2. .github/workflows/run-static.yml +3 -6
  3. .pre-commit-config.yaml +8 -3
  4. AGENTS.md +3 -3
  5. docs/deployment/server-configuration.mdx +539 -0
  6. docs/docs.json +1 -0
  7. docs/integrations/claude-code.mdx +14 -9
  8. docs/integrations/claude-desktop.mdx +13 -8
  9. docs/integrations/cursor.mdx +28 -8
  10. docs/integrations/mcp-json-configuration.mdx +17 -7
  11. docs/patterns/cli.mdx +56 -13
  12. docs/schemas/fastmcp_config/latest.json +1 -0
  13. docs/schemas/fastmcp_config/v1.json +1 -0
  14. docs/servers/auth/oauth-proxy.mdx +1 -1
  15. docs/servers/server.mdx +2 -6
  16. examples/atproto_mcp/fastmcp.json +9 -0
  17. examples/atproto_mcp/src/atproto_mcp/server.py +1 -6
  18. examples/fastmcp_config/env_interpolation_example.json +20 -0
  19. examples/fastmcp_config/fastmcp.json +11 -0
  20. examples/fastmcp_config/full_example.fastmcp.json +30 -0
  21. examples/fastmcp_config/server.py +39 -0
  22. examples/fastmcp_config/simple.fastmcp.json +7 -0
  23. examples/fastmcp_config_demo/README.md +55 -0
  24. examples/fastmcp_config_demo/fastmcp.json +15 -0
  25. examples/fastmcp_config_demo/server.py +70 -0
  26. examples/memory.fastmcp.json +12 -0
  27. examples/mount_example.fastmcp.json +4 -0
  28. examples/mount_example.py +1 -3
  29. examples/screenshot.fastmcp.json +7 -0
  30. examples/smart_home/hub.fastmcp.json +9 -0
  31. examples/smart_home/lights.fastmcp.json +9 -0
  32. justfile +2 -2
  33. pyproject.toml +16 -11
  34. src/fastmcp/cli/cli.py +298 -10
  35. src/fastmcp/cli/install/cursor.py +126 -1
  36. src/fastmcp/cli/install/shared.py +39 -3
  37. src/fastmcp/cli/run.py +136 -7
  38. src/fastmcp/client/transports.py +36 -16
  39. src/fastmcp/experimental/utilities/openapi/parser.py +5 -1
  40. src/fastmcp/mcp_config.py +15 -12
  41. src/fastmcp/server/context.py +1 -1
  42. src/fastmcp/server/http.py +1 -1
  43. src/fastmcp/server/server.py +19 -1
  44. src/fastmcp/tools/tool_transform.py +1 -1
  45. src/fastmcp/utilities/fastmcp_config/__init__.py +21 -0
  46. src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py +678 -0
  47. src/fastmcp/utilities/fastmcp_config/v1/schema.json +361 -0
  48. tests/cli/test_config.py +513 -0
  49. tests/cli/test_cursor.py +1 -0
  50. tests/cli/test_fastmcp_config_integration.py +355 -0
.github/workflows/marvin.yml CHANGED
@@ -4,6 +4,7 @@ on:
4
  issue_comment: { types: [created] }
5
  pull_request_review_comment: { types: [created] }
6
  pull_request_review: { types: [submitted] }
 
7
  issues: { types: [opened, edited, assigned, labeled] }
8
  discussion: { types: [created, edited, labeled] }
9
  discussion_comment: { types: [created] }
@@ -22,6 +23,7 @@ jobs:
22
  (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/marvin')) ||
23
  (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/marvin')) ||
24
  (github.event_name == 'pull_request_review' && contains(github.event.review.body, '/marvin')) ||
 
25
  (github.event_name == 'issues' && contains(github.event.issue.body, '/marvin')) ||
26
  (github.event_name == 'discussion' && contains(github.event.discussion.body, '/marvin')) ||
27
  (github.event_name == 'discussion_comment' && contains(github.event.comment.body, '/marvin')) ||
@@ -70,6 +72,6 @@ jobs:
70
  mode: tag
71
  trigger_phrase: "/marvin"
72
  allowed_bots: "*"
73
- allowed_tools: "WebSearch,WebFetch,Bash(uv:*),Bash(pre-commit:*),Bash(pytest:*),Bash(ruff:*),Bash(pyright:*),Bash(git:*),Bash(gh:*),mcp__github__add_issue_comment,mcp__github__create_issue,mcp__github__get_issue,mcp__github__list_issues,mcp__github__search_issues,mcp__github__update_issue,mcp__github__update_issue_comment,mcp__github__create_pull_request,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__list_pull_requests,mcp__github__update_pull_request,mcp__github__update_pull_request_branch,mcp__github__update_pull_request_comment,mcp__github__merge_pull_request"
74
  additional_permissions: |
75
  actions: read
 
4
  issue_comment: { types: [created] }
5
  pull_request_review_comment: { types: [created] }
6
  pull_request_review: { types: [submitted] }
7
+ pull_request: { types: [opened, edited] }
8
  issues: { types: [opened, edited, assigned, labeled] }
9
  discussion: { types: [created, edited, labeled] }
10
  discussion_comment: { types: [created] }
 
23
  (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/marvin')) ||
24
  (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/marvin')) ||
25
  (github.event_name == 'pull_request_review' && contains(github.event.review.body, '/marvin')) ||
26
+ (github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/marvin')) ||
27
  (github.event_name == 'issues' && contains(github.event.issue.body, '/marvin')) ||
28
  (github.event_name == 'discussion' && contains(github.event.discussion.body, '/marvin')) ||
29
  (github.event_name == 'discussion_comment' && contains(github.event.comment.body, '/marvin')) ||
 
72
  mode: tag
73
  trigger_phrase: "/marvin"
74
  allowed_bots: "*"
75
+ allowed_tools: "WebSearch,WebFetch,Bash(uv:*),Bash(pre-commit:*),Bash(pytest:*),Bash(ruff:*),Bash(ty:*),Bash(git:*),Bash(gh:*),mcp__github__add_issue_comment,mcp__github__create_issue,mcp__github__get_issue,mcp__github__list_issues,mcp__github__search_issues,mcp__github__update_issue,mcp__github__update_issue_comment,mcp__github__create_pull_request,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__list_pull_requests,mcp__github__update_pull_request,mcp__github__update_pull_request_branch,mcp__github__update_pull_request_comment,mcp__github__merge_pull_request"
76
  additional_permissions: |
77
  actions: read
.github/workflows/run-static.yml CHANGED
@@ -36,12 +36,9 @@ jobs:
36
  with:
37
  enable-cache: true
38
  cache-dependency-glob: "uv.lock"
39
- - name: Set up Python
40
- uses: actions/setup-python@v5
41
- with:
42
- python-version: "3.12"
43
  - name: Install dependencies
44
- run: uv sync --dev
45
  - name: Check lockfile is up to date
46
  run: |
47
  if ! uv lock --check; then
@@ -51,6 +48,6 @@ jobs:
51
  fi
52
  echo "✅ Lockfile is up to date"
53
  - name: Run pre-commit
54
- uses: pre-commit/action@v3.0.1
55
  env:
56
  SKIP: no-commit-to-branch
 
36
  with:
37
  enable-cache: true
38
  cache-dependency-glob: "uv.lock"
39
+
 
 
 
40
  - name: Install dependencies
41
+ run: uv sync
42
  - name: Check lockfile is up to date
43
  run: |
44
  if ! uv lock --check; then
 
48
  fi
49
  echo "✅ Lockfile is up to date"
50
  - name: Run pre-commit
51
+ run: uv run pre-commit run --all-files
52
  env:
53
  SKIP: no-commit-to-branch
.pre-commit-config.yaml CHANGED
@@ -22,11 +22,16 @@ repos:
22
  # Run the formatter.
23
  - id: ruff-format
24
 
25
- - repo: https://github.com/northisup/pyright-pretty
26
- rev: v0.1.0
27
  hooks:
28
- - id: pyright-pretty
 
 
 
 
29
  files: ^src/|^tests/
 
 
30
 
31
  - repo: https://github.com/pre-commit/pre-commit-hooks
32
  rev: v4.3.0
 
22
  # Run the formatter.
23
  - id: ruff-format
24
 
25
+ - repo: local
 
26
  hooks:
27
+ - id: ty
28
+ name: type check
29
+ entry: ty check
30
+ language: system
31
+ types: [python]
32
  files: ^src/|^tests/
33
+ pass_filenames: false
34
+ require_serial: true
35
 
36
  - repo: https://github.com/pre-commit/pre-commit-hooks
37
  rev: v4.3.0
AGENTS.md CHANGED
@@ -10,7 +10,7 @@ FastMCP is a comprehensive Python framework (Python ≥3.10) for building Model
10
 
11
  ```bash
12
  uv sync # Install dependencies
13
- uv run pre-commit run --all-files # Ruff + Prettier + Pyright
14
  uv run pytest # Run full test suite
15
  ```
16
 
@@ -222,7 +222,7 @@ uv sync # Installs all deps including dev tools
222
  ### Validation Commands (Run Frequently)
223
 
224
  - **Linting**: `uv run ruff check` (or with `--fix`)
225
- - **Type Checking**: `uv run pyright`
226
  - **All Checks**: `uv run pre-commit run --all-files`
227
 
228
  ### Testing
@@ -247,5 +247,5 @@ uv sync # Installs all deps including dev tools
247
 
248
  1. **Dependencies**: Always `uv sync` first
249
  2. **Pre-commit fails**: Run `uv run pre-commit run --all-files` to see failures
250
- 3. **Type errors**: Use `uv run pyright` directly, check `pyproject.toml` config
251
  4. **Test timeouts**: Default 3s - optimize or mark as integration tests
 
10
 
11
  ```bash
12
  uv sync # Install dependencies
13
+ uv run pre-commit run --all-files # Ruff + Prettier + ty
14
  uv run pytest # Run full test suite
15
  ```
16
 
 
222
  ### Validation Commands (Run Frequently)
223
 
224
  - **Linting**: `uv run ruff check` (or with `--fix`)
225
+ - **Type Checking**: `uv run ty check`
226
  - **All Checks**: `uv run pre-commit run --all-files`
227
 
228
  ### Testing
 
247
 
248
  1. **Dependencies**: Always `uv sync` first
249
  2. **Pre-commit fails**: Run `uv run pre-commit run --all-files` to see failures
250
+ 3. **Type errors**: Use `uv run ty check` directly, check `pyproject.toml` config
251
  4. **Test timeouts**: Default 3s - optimize or mark as integration tests
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
@@ -47,6 +47,21 @@ The easiest way to install a FastMCP server in Cursor is using the `fastmcp inst
47
  fastmcp install cursor server.py
48
  ```
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
51
 
52
  ```bash
@@ -84,17 +99,22 @@ fastmcp install cursor server.py --with-requirements requirements.txt
84
  fastmcp install cursor server.py --with-editable ./my-local-package
85
  ```
86
 
87
- Alternatively, you can specify dependencies directly in your server code:
88
 
89
- ```python server.py
90
- from fastmcp import FastMCP
91
-
92
- mcp = FastMCP(
93
- name="Dice Roller",
94
- dependencies=["pandas", "requests"]
95
- )
 
 
 
 
96
  ```
97
 
 
98
  #### Python Version and Project Configuration
99
 
100
  Control your server's Python environment with these options:
 
47
  fastmcp install cursor server.py
48
  ```
49
 
50
+ #### Workspace Installation
51
+ <VersionBadge version="2.12.0" />
52
+
53
+ By default, FastMCP installs servers globally for Cursor. You can also install servers to project-specific workspaces using the `--workspace` flag:
54
+
55
+ ```bash
56
+ # Install to current directory's .cursor/ folder
57
+ fastmcp install cursor server.py --workspace .
58
+
59
+ # Install to specific workspace
60
+ fastmcp install cursor server.py --workspace /path/to/project
61
+ ```
62
+
63
+ This creates a `.cursor/mcp.json` configuration file in the specified workspace directory, allowing different projects to have their own MCP server configurations.
64
+
65
  The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
66
 
67
  ```bash
 
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/auth/oauth-proxy.mdx CHANGED
@@ -165,7 +165,7 @@ The `OAuthProxy` class provides the complete proxy implementation:
165
 
166
  ```python
167
  from fastmcp import FastMCP
168
- from fastmcp.server.auth.providers.proxy import OAuthProxy
169
  from fastmcp.server.auth.providers.jwt import JWTVerifier
170
 
171
  # Configure token validation for your provider
 
165
 
166
  ```python
167
  from fastmcp import FastMCP
168
+ from fastmcp.server.auth.proxy import OAuthProxy
169
  from fastmcp.server.auth.providers.jwt import JWTVerifier
170
 
171
  # Configure token validation for your provider
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
+ }
justfile CHANGED
@@ -6,9 +6,9 @@ build:
6
  test: build
7
  uv run --frozen pytest -xvs tests
8
 
9
- # Run pyright on all files
10
  typecheck:
11
- uv run --frozen pyright
12
 
13
  # Serve documentation locally
14
  docs:
 
6
  test: build
7
  uv run --frozen pytest -xvs tests
8
 
9
+ # Run ty type checker on all files
10
  typecheck:
11
+ uv run --frozen ty check
12
 
13
  # Serve documentation locally
14
  docs:
pyproject.toml CHANGED
@@ -50,7 +50,7 @@ dev = [
50
  "psutil",
51
  "pyinstrument>=5.0.2",
52
  "pyperclip>=1.9.0",
53
- "pyright>=1.1.389",
54
  "pytest>=8.3.3",
55
  "pytest-asyncio>=0.23.5",
56
  "pytest-cov>=6.1.1",
@@ -114,18 +114,23 @@ python_files = ["test_*.py", "*_test.py"]
114
  python_classes = ["Test*"]
115
  python_functions = ["test_*"]
116
 
117
- [tool.pyright]
118
  include = ["src", "tests"]
119
  exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"]
120
- pythonVersion = "3.10"
121
- pythonPlatform = "Darwin"
122
- typeCheckingMode = "basic"
123
- reportMissingImports = true
124
- reportMissingTypeStubs = false
125
- useLibraryCodeForTypes = true
126
- venvPath = "."
127
- venv = ".venv"
128
- strict = ["src/fastmcp/server/server.py"]
 
 
 
 
 
129
 
130
  [tool.ruff.lint]
131
  extend-select = ["I", "UP"]
 
50
  "psutil",
51
  "pyinstrument>=5.0.2",
52
  "pyperclip>=1.9.0",
53
+ "ty>=0.0.1a19",
54
  "pytest>=8.3.3",
55
  "pytest-asyncio>=0.23.5",
56
  "pytest-cov>=6.1.1",
 
114
  python_classes = ["Test*"]
115
  python_functions = ["test_*"]
116
 
117
+ [tool.ty.src]
118
  include = ["src", "tests"]
119
  exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"]
120
+
121
+ [tool.ty.environment]
122
+ python-version = "3.10"
123
+
124
+ [tool.ty.rules]
125
+ # Rules with too many errors to fix right now (40+ each)
126
+ invalid-argument-type = "ignore" # 40 errors
127
+ no-matching-overload = "ignore" # 126 errors
128
+ unknown-argument = "ignore" # 61 errors
129
+ unresolved-attribute = "ignore" # 60 errors
130
+
131
+ # Rules with moderate errors that need more investigation
132
+ call-non-callable = "ignore" # 7 errors
133
+ missing-argument = "ignore" # 23 errors
134
 
135
  [tool.ruff.lint]
136
  extend-select = ["I", "UP"]
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/cursor.py CHANGED
@@ -9,7 +9,7 @@ from typing import Annotated
9
  import cyclopts
10
  from rich import print
11
 
12
- from fastmcp.mcp_config import StdioMCPServer
13
  from fastmcp.utilities.logging import get_logger
14
 
15
  from .shared import process_common_args
@@ -64,6 +64,106 @@ def open_deeplink(deeplink: str) -> bool:
64
  return False
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def install_cursor(
68
  file: Path,
69
  server_object: str | None,
@@ -75,6 +175,7 @@ def install_cursor(
75
  python_version: str | None = None,
76
  with_requirements: Path | None = None,
77
  project: Path | None = None,
 
78
  ) -> bool:
79
  """Install FastMCP server in Cursor.
80
 
@@ -88,6 +189,7 @@ def install_cursor(
88
  python_version: Optional Python version to use
89
  with_requirements: Optional requirements file to install from
90
  project: Optional project directory to run within
 
91
 
92
  Returns:
93
  True if installation was successful, False otherwise
@@ -127,6 +229,21 @@ def install_cursor(
127
  # Add fastmcp run command
128
  args.extend(["fastmcp", "run", server_spec])
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  # Create server configuration
131
  server_config = StdioMCPServer(
132
  command="uv",
@@ -211,6 +328,13 @@ async def cursor_command(
211
  help="Run the command within the given project directory",
212
  ),
213
  ] = None,
 
 
 
 
 
 
 
214
  ) -> None:
215
  """Install an MCP server in Cursor.
216
 
@@ -231,6 +355,7 @@ async def cursor_command(
231
  python_version=python,
232
  with_requirements=with_requirements,
233
  project=project,
 
234
  )
235
 
236
  if not success:
 
9
  import cyclopts
10
  from rich import print
11
 
12
+ from fastmcp.mcp_config import StdioMCPServer, update_config_file
13
  from fastmcp.utilities.logging import get_logger
14
 
15
  from .shared import process_common_args
 
64
  return False
65
 
66
 
67
+ def install_cursor_workspace(
68
+ file: Path,
69
+ server_object: str | None,
70
+ name: str,
71
+ workspace_path: Path,
72
+ *,
73
+ with_editable: Path | None = None,
74
+ with_packages: list[str] | None = None,
75
+ env_vars: dict[str, str] | None = None,
76
+ python_version: str | None = None,
77
+ with_requirements: Path | None = None,
78
+ project: Path | None = None,
79
+ ) -> bool:
80
+ """Install FastMCP server to workspace-specific Cursor configuration.
81
+
82
+ Args:
83
+ file: Path to the server file
84
+ server_object: Optional server object name (for :object suffix)
85
+ name: Name for the server in Cursor
86
+ workspace_path: Path to the workspace directory
87
+ with_editable: Optional directory to install in editable mode
88
+ with_packages: Optional list of additional packages to install
89
+ env_vars: Optional dictionary of environment variables
90
+ python_version: Optional Python version to use
91
+ with_requirements: Optional requirements file to install from
92
+ project: Optional project directory to run within
93
+
94
+ Returns:
95
+ True if installation was successful, False otherwise
96
+ """
97
+ # Ensure workspace path is absolute and exists
98
+ workspace_path = workspace_path.resolve()
99
+ if not workspace_path.exists():
100
+ print(f"[red]Workspace directory does not exist: {workspace_path}[/red]")
101
+ return False
102
+
103
+ # Create .cursor directory in workspace
104
+ cursor_dir = workspace_path / ".cursor"
105
+ cursor_dir.mkdir(exist_ok=True)
106
+
107
+ config_file = cursor_dir / "mcp.json"
108
+
109
+ # Build uv run command
110
+ args = ["run"]
111
+
112
+ # Add Python version if specified
113
+ if python_version:
114
+ args.extend(["--python", python_version])
115
+
116
+ # Add project if specified
117
+ if project:
118
+ args.extend(["--project", str(project)])
119
+
120
+ # Collect all packages in a set to deduplicate
121
+ packages = {"fastmcp"}
122
+ if with_packages:
123
+ packages.update(pkg for pkg in with_packages if pkg)
124
+
125
+ # Add all packages with --with
126
+ for pkg in sorted(packages):
127
+ args.extend(["--with", pkg])
128
+
129
+ if with_editable:
130
+ args.extend(["--with-editable", str(with_editable)])
131
+
132
+ if with_requirements:
133
+ args.extend(["--with-requirements", str(with_requirements)])
134
+
135
+ # Build server spec from parsed components
136
+ if server_object:
137
+ server_spec = f"{file.resolve()}:{server_object}"
138
+ else:
139
+ server_spec = str(file.resolve())
140
+
141
+ # Add fastmcp run command
142
+ args.extend(["fastmcp", "run", server_spec])
143
+
144
+ # Create server configuration
145
+ server_config = StdioMCPServer(
146
+ command="uv",
147
+ args=args,
148
+ env=env_vars or {},
149
+ )
150
+
151
+ try:
152
+ # Create the config file if it doesn't exist
153
+ if not config_file.exists():
154
+ config_file.write_text('{"mcpServers": {}}')
155
+
156
+ # Update configuration with the new server
157
+ update_config_file(config_file, name, server_config)
158
+ print(
159
+ f"[green]Successfully installed '{name}' to workspace at {workspace_path}[/green]"
160
+ )
161
+ return True
162
+ except Exception as e:
163
+ print(f"[red]Failed to install server to workspace: {e}[/red]")
164
+ return False
165
+
166
+
167
  def install_cursor(
168
  file: Path,
169
  server_object: str | None,
 
175
  python_version: str | None = None,
176
  with_requirements: Path | None = None,
177
  project: Path | None = None,
178
+ workspace: Path | None = None,
179
  ) -> bool:
180
  """Install FastMCP server in Cursor.
181
 
 
189
  python_version: Optional Python version to use
190
  with_requirements: Optional requirements file to install from
191
  project: Optional project directory to run within
192
+ workspace: Optional workspace directory for project-specific installation
193
 
194
  Returns:
195
  True if installation was successful, False otherwise
 
229
  # Add fastmcp run command
230
  args.extend(["fastmcp", "run", server_spec])
231
 
232
+ # If workspace is specified, install to workspace-specific config
233
+ if workspace:
234
+ return install_cursor_workspace(
235
+ file=file,
236
+ server_object=server_object,
237
+ name=name,
238
+ workspace_path=workspace,
239
+ with_editable=with_editable,
240
+ with_packages=with_packages,
241
+ env_vars=env_vars,
242
+ python_version=python_version,
243
+ with_requirements=with_requirements,
244
+ project=project,
245
+ )
246
+
247
  # Create server configuration
248
  server_config = StdioMCPServer(
249
  command="uv",
 
328
  help="Run the command within the given project directory",
329
  ),
330
  ] = None,
331
+ workspace: Annotated[
332
+ Path | None,
333
+ cyclopts.Parameter(
334
+ "--workspace",
335
+ help="Install to workspace directory (will create .cursor/ inside it) instead of using deeplink",
336
+ ),
337
+ ] = None,
338
  ) -> None:
339
  """Install an MCP server in Cursor.
340
 
 
355
  python_version=python,
356
  with_requirements=with_requirements,
357
  project=project,
358
+ workspace=workspace,
359
  )
360
 
361
  if not success:
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,18 +95,19 @@ 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)
101
 
102
  # Handle module:object syntax
103
- if ":" in server_or_factory:
104
  module_name, object_name = server_or_factory.split(":", 1)
105
  try:
106
  server_module = importlib.import_module(module_name)
@@ -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)
108
 
109
  # Handle module:object syntax
110
+ if server_or_factory and ":" in server_or_factory:
111
  module_name, object_name = server_or_factory.split(":", 1)
112
  try:
113
  server_module = importlib.import_module(module_name)
 
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/experimental/utilities/openapi/parser.py CHANGED
@@ -549,7 +549,11 @@ class OpenAPIParser(
549
  return
550
 
551
  # Add this schema and recursively find its dependencies
552
- if schema_name not in collected and schema_name in all_schemas:
 
 
 
 
553
  collected.add(schema_name)
554
  # Recursively find dependencies of this schema
555
  find_refs(all_schemas[schema_name])
 
549
  return
550
 
551
  # Add this schema and recursively find its dependencies
552
+ if (
553
+ collected is not None
554
+ and schema_name not in collected
555
+ and schema_name in all_schemas
556
+ ):
557
  collected.add(schema_name)
558
  # Recursively find dependencies of this schema
559
  find_refs(all_schemas[schema_name])
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/context.py CHANGED
@@ -44,7 +44,7 @@ from fastmcp.utilities.types import get_cached_typeadapter
44
  logger = get_logger(__name__)
45
 
46
  T = TypeVar("T")
47
- _current_context: ContextVar[Context | None] = ContextVar("context", default=None)
48
  _flush_lock = asyncio.Lock()
49
 
50
 
 
44
  logger = get_logger(__name__)
45
 
46
  T = TypeVar("T")
47
+ _current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
48
  _flush_lock = asyncio.Lock()
49
 
50
 
src/fastmcp/server/http.py CHANGED
@@ -64,7 +64,7 @@ class StreamableHTTPASGIApp:
64
  raise
65
 
66
 
67
- _current_http_request: ContextVar[Request | None] = ContextVar(
68
  "http_request",
69
  default=None,
70
  )
 
64
  raise
65
 
66
 
67
+ _current_http_request: ContextVar[Request | None] = ContextVar( # type: ignore[assignment]
68
  "http_request",
69
  default=None,
70
  )
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
@@ -224,7 +225,24 @@ class FastMCP(Generic[LifespanResultT]):
224
 
225
  # Set up MCP protocol handlers
226
  self._setup_handlers()
227
- self.dependencies = dependencies or fastmcp.settings.server_dependencies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
  self.include_fastmcp_meta = (
230
  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
 
225
 
226
  # Set up MCP protocol handlers
227
  self._setup_handlers()
228
+
229
+ # Handle dependencies with deprecation warning
230
+ # TODO: Remove dependencies parameter (deprecated in v2.11.4)
231
+ if dependencies is not None:
232
+ import warnings
233
+
234
+ warnings.warn(
235
+ "The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
236
+ "Please specify dependencies in a fastmcp.json configuration file instead:\n"
237
+ '{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
238
+ f"{json.dumps(dependencies)}\n }}\n}}\n"
239
+ "See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
240
+ DeprecationWarning,
241
+ stacklevel=2,
242
+ )
243
+ self.dependencies = (
244
+ dependencies or fastmcp.settings.server_dependencies
245
+ ) # TODO: Remove (deprecated in v2.11.4)
246
 
247
  self.include_fastmcp_meta = (
248
  include_fastmcp_meta
src/fastmcp/tools/tool_transform.py CHANGED
@@ -29,7 +29,7 @@ logger = get_logger(__name__)
29
 
30
 
31
  # Context variable to store current transformed tool
32
- _current_tool: ContextVar[TransformedTool | None] = ContextVar(
33
  "_current_tool", default=None
34
  )
35
 
 
29
 
30
 
31
  # Context variable to store current transformed tool
32
+ _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[assignment]
33
  "_current_tool", default=None
34
  )
35
 
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_cursor.py CHANGED
@@ -330,6 +330,7 @@ class TestCursorCommand:
330
  python_version=None,
331
  with_requirements=None,
332
  project=None,
 
333
  )
334
  mock_exit.assert_not_called()
335
 
 
330
  python_version=None,
331
  with_requirements=None,
332
  project=None,
333
+ workspace=None,
334
  )
335
  mock_exit.assert_not_called()
336
 
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