fastmcp 2.11.3__py3-none-any.whl → 2.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/oauth_callback.py +91 -91
  13. fastmcp/client/sampling.py +12 -4
  14. fastmcp/client/transports.py +139 -64
  15. fastmcp/experimental/sampling/__init__.py +0 -0
  16. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  17. fastmcp/experimental/sampling/handlers/base.py +21 -0
  18. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  19. fastmcp/experimental/server/openapi/routing.py +0 -2
  20. fastmcp/experimental/server/openapi/server.py +0 -2
  21. fastmcp/experimental/utilities/openapi/parser.py +5 -1
  22. fastmcp/mcp_config.py +40 -20
  23. fastmcp/prompts/prompt_manager.py +2 -0
  24. fastmcp/resources/resource_manager.py +4 -0
  25. fastmcp/server/auth/__init__.py +2 -0
  26. fastmcp/server/auth/auth.py +2 -1
  27. fastmcp/server/auth/oauth_proxy.py +1047 -0
  28. fastmcp/server/auth/providers/azure.py +270 -0
  29. fastmcp/server/auth/providers/github.py +287 -0
  30. fastmcp/server/auth/providers/google.py +305 -0
  31. fastmcp/server/auth/providers/jwt.py +24 -12
  32. fastmcp/server/auth/providers/workos.py +256 -2
  33. fastmcp/server/auth/redirect_validation.py +65 -0
  34. fastmcp/server/context.py +91 -41
  35. fastmcp/server/elicitation.py +60 -1
  36. fastmcp/server/http.py +3 -3
  37. fastmcp/server/middleware/logging.py +66 -28
  38. fastmcp/server/proxy.py +2 -0
  39. fastmcp/server/sampling/handler.py +19 -0
  40. fastmcp/server/server.py +76 -15
  41. fastmcp/settings.py +16 -1
  42. fastmcp/tools/tool.py +22 -9
  43. fastmcp/tools/tool_manager.py +2 -0
  44. fastmcp/tools/tool_transform.py +39 -10
  45. fastmcp/utilities/auth.py +34 -0
  46. fastmcp/utilities/cli.py +148 -15
  47. fastmcp/utilities/components.py +2 -1
  48. fastmcp/utilities/inspect.py +166 -37
  49. fastmcp/utilities/json_schema_type.py +4 -2
  50. fastmcp/utilities/logging.py +4 -1
  51. fastmcp/utilities/mcp_config.py +47 -18
  52. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  53. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  54. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  55. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  59. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  60. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  61. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  62. fastmcp/utilities/tests.py +7 -2
  63. fastmcp/utilities/types.py +15 -2
  64. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
  65. fastmcp-2.12.0.dist-info/RECORD +129 -0
  66. fastmcp-2.11.3.dist-info/RECORD +0 -108
  67. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
  68. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
  69. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -10,6 +10,7 @@ import pyperclip
10
10
  from rich import print
11
11
 
12
12
  from fastmcp.utilities.logging import get_logger
13
+ from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
13
14
 
14
15
  from .shared import process_common_args
15
16
 
@@ -21,7 +22,7 @@ def install_mcp_json(
21
22
  server_object: str | None,
22
23
  name: str,
23
24
  *,
24
- with_editable: Path | None = None,
25
+ with_editable: list[Path] | None = None,
25
26
  with_packages: list[str] | None = None,
26
27
  env_vars: dict[str, str] | None = None,
27
28
  copy: bool = False,
@@ -35,7 +36,7 @@ def install_mcp_json(
35
36
  file: Path to the server file
36
37
  server_object: Optional server object name (for :object suffix)
37
38
  name: Name for the server in MCP config
38
- with_editable: Optional directory to install in editable mode
39
+ with_editable: Optional list of directories to install in editable mode
39
40
  with_packages: Optional list of additional packages to install
40
41
  env_vars: Optional dictionary of environment variables
41
42
  copy: If True, copy to clipboard instead of printing to stdout
@@ -47,45 +48,34 @@ def install_mcp_json(
47
48
  True if generation was successful, False otherwise
48
49
  """
49
50
  try:
50
- # Build uv run command
51
- args = ["run"]
52
-
53
- # Add Python version if specified
54
- if python_version:
55
- args.extend(["--python", python_version])
56
-
57
- # Add project if specified
58
- if project:
59
- args.extend(["--project", str(project)])
60
-
61
- # Collect all packages in a set to deduplicate
62
- packages = {"fastmcp"}
51
+ # Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
52
+ deduplicated_packages = None
63
53
  if with_packages:
64
- packages.update(pkg for pkg in with_packages if pkg)
65
-
66
- # Add all packages with --with
67
- for pkg in sorted(packages):
68
- args.extend(["--with", pkg])
69
-
70
- if with_editable:
71
- args.extend(["--with-editable", str(with_editable)])
72
-
73
- if with_requirements:
74
- args.extend(["--with-requirements", str(with_requirements)])
75
-
54
+ deduplicated = list(dict.fromkeys(with_packages))
55
+ deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
56
+ if not deduplicated_packages:
57
+ deduplicated_packages = None
58
+
59
+ env_config = UVEnvironment(
60
+ python=python_version,
61
+ dependencies=deduplicated_packages,
62
+ requirements=str(with_requirements) if with_requirements else None,
63
+ project=str(project) if project else None,
64
+ editable=[str(p) for p in with_editable] if with_editable else None,
65
+ )
76
66
  # Build server spec from parsed components
77
67
  if server_object:
78
68
  server_spec = f"{file.resolve()}:{server_object}"
79
69
  else:
80
70
  server_spec = str(file.resolve())
81
71
 
82
- # Add fastmcp run command
83
- args.extend(["fastmcp", "run", server_spec])
72
+ # Build the full command
73
+ full_command = env_config.build_command(["fastmcp", "run", server_spec])
84
74
 
85
75
  # Build MCP server configuration
86
76
  server_config = {
87
- "command": "uv",
88
- "args": args,
77
+ "command": full_command[0],
78
+ "args": full_command[1:],
89
79
  }
90
80
 
91
81
  # Add environment variables if provided
@@ -124,28 +114,29 @@ async def mcp_json_command(
124
114
  ),
125
115
  ] = None,
126
116
  with_editable: Annotated[
127
- Path | None,
117
+ list[Path] | None,
128
118
  cyclopts.Parameter(
129
- name=["--with-editable", "-e"],
130
- help="Directory with pyproject.toml to install in editable mode",
119
+ "--with-editable",
120
+ help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
121
+ negative="",
131
122
  ),
132
123
  ] = None,
133
124
  with_packages: Annotated[
134
- list[str],
125
+ list[str] | None,
135
126
  cyclopts.Parameter(
136
127
  "--with",
137
- help="Additional packages to install",
138
- negative=False,
128
+ help="Additional packages to install (can be used multiple times)",
129
+ negative="",
139
130
  ),
140
- ] = [],
131
+ ] = None,
141
132
  env_vars: Annotated[
142
- list[str],
133
+ list[str] | None,
143
134
  cyclopts.Parameter(
144
135
  "--env",
145
- help="Environment variables in KEY=VALUE format",
146
- negative=False,
136
+ help="Environment variables in KEY=VALUE format (can be used multiple times)",
137
+ negative="",
147
138
  ),
148
- ] = [],
139
+ ] = None,
149
140
  env_file: Annotated[
150
141
  Path | None,
151
142
  cyclopts.Parameter(
@@ -158,7 +149,7 @@ async def mcp_json_command(
158
149
  cyclopts.Parameter(
159
150
  "--copy",
160
151
  help="Copy configuration to clipboard instead of printing to stdout",
161
- negative=False,
152
+ negative="",
162
153
  ),
163
154
  ] = False,
164
155
  python: Annotated[
@@ -188,6 +179,10 @@ async def mcp_json_command(
188
179
  Args:
189
180
  server_spec: Python file to install, optionally with :object suffix
190
181
  """
182
+ # Convert None to empty lists for list parameters
183
+ with_editable = with_editable or []
184
+ with_packages = with_packages or []
185
+ env_vars = env_vars or []
191
186
  file, server_object, name, packages, env_dict = await process_common_args(
192
187
  server_spec, server_name, with_packages, env_vars, env_file
193
188
  )
@@ -1,13 +1,16 @@
1
1
  """Shared utilities for install commands."""
2
2
 
3
+ import json
3
4
  import sys
4
5
  from pathlib import Path
5
6
 
6
7
  from dotenv import dotenv_values
8
+ from pydantic import ValidationError
7
9
  from rich import print
8
10
 
9
- from fastmcp.cli.run import import_server, parse_file_path
10
11
  from fastmcp.utilities.logging import get_logger
12
+ from fastmcp.utilities.mcp_server_config import MCPServerConfig
13
+ from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
11
14
 
12
15
  logger = get_logger(__name__)
13
16
 
@@ -26,13 +29,57 @@ def parse_env_var(env_var: str) -> tuple[str, str]:
26
29
  async def process_common_args(
27
30
  server_spec: str,
28
31
  server_name: str | None,
29
- with_packages: list[str],
30
- env_vars: list[str],
32
+ with_packages: list[str] | None,
33
+ env_vars: list[str] | None,
31
34
  env_file: Path | None,
32
35
  ) -> 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
+ """Process common arguments shared by all install commands.
37
+
38
+ Handles both fastmcp.json config files and traditional file.py:object syntax.
39
+ """
40
+ # Convert None to empty lists for list parameters
41
+ with_packages = with_packages or []
42
+ env_vars = env_vars or []
43
+ # Create MCPServerConfig from server_spec
44
+ config = None
45
+ if server_spec.endswith(".json"):
46
+ config_path = Path(server_spec).resolve()
47
+ if not config_path.exists():
48
+ print(f"[red]Configuration file not found: {config_path}[/red]")
49
+ sys.exit(1)
50
+
51
+ try:
52
+ with open(config_path) as f:
53
+ data = json.load(f)
54
+
55
+ # Check if it's an MCPConfig (has mcpServers key)
56
+ if "mcpServers" in data:
57
+ # MCPConfig files aren't supported for install
58
+ print("[red]MCPConfig files are not supported for installation[/red]")
59
+ sys.exit(1)
60
+ else:
61
+ # It's a MCPServerConfig
62
+ config = MCPServerConfig.from_file(config_path)
63
+
64
+ # Merge packages from config if not overridden
65
+ if config.environment.dependencies:
66
+ # Merge with CLI packages (CLI takes precedence)
67
+ config_packages = list(config.environment.dependencies)
68
+ with_packages = list(set(with_packages + config_packages))
69
+ except (json.JSONDecodeError, ValidationError) as e:
70
+ print(f"[red]Invalid configuration file: {e}[/red]")
71
+ sys.exit(1)
72
+ else:
73
+ # Create config from file path
74
+ source = FileSystemSource(path=server_spec)
75
+ config = MCPServerConfig(source=source)
76
+
77
+ # Extract file and server_object from the source
78
+ # The FileSystemSource handles parsing path:object syntax
79
+ file = Path(config.source.path).resolve()
80
+ server_object = (
81
+ config.source.entrypoint if hasattr(config.source, "entrypoint") else None
82
+ )
36
83
 
37
84
  logger.debug(
38
85
  "Installing server",
@@ -49,7 +96,7 @@ async def process_common_args(
49
96
  server = None
50
97
  if not name:
51
98
  try:
52
- server = await import_server(file, server_object)
99
+ server = await config.source.load_server()
53
100
  name = server.name
54
101
  except (ImportError, ModuleNotFoundError) as e:
55
102
  logger.debug(
@@ -59,8 +106,18 @@ async def process_common_args(
59
106
  name = file.stem
60
107
 
61
108
  # Get server dependencies if available
109
+ # TODO: Remove dependencies handling (deprecated in v2.11.4)
62
110
  server_dependencies = getattr(server, "dependencies", []) if server else []
63
111
  if server_dependencies:
112
+ import warnings
113
+
114
+ warnings.warn(
115
+ "Server uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). "
116
+ "Please migrate to fastmcp.json configuration file. "
117
+ "See https://gofastmcp.com/docs/deployment/server-configuration for details.",
118
+ DeprecationWarning,
119
+ stacklevel=2,
120
+ )
64
121
  with_packages = list(set(with_packages + server_dependencies))
65
122
 
66
123
  # Process environment variables if provided
fastmcp/cli/run.py CHANGED
@@ -1,12 +1,10 @@
1
1
  """FastMCP run command implementation with enhanced type hints."""
2
2
 
3
- import importlib.util
4
- import inspect
5
3
  import json
4
+ import os
6
5
  import re
7
6
  import subprocess
8
7
  import sys
9
- from functools import partial
10
8
  from pathlib import Path
11
9
  from typing import Any, Literal
12
10
 
@@ -14,6 +12,11 @@ from mcp.server.fastmcp import FastMCP as FastMCP1x
14
12
 
15
13
  from fastmcp.server.server import FastMCP
16
14
  from fastmcp.utilities.logging import get_logger
15
+ from fastmcp.utilities.mcp_server_config import (
16
+ MCPServerConfig,
17
+ )
18
+ from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
19
+ from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
17
20
 
18
21
  logger = get_logger("cli.run")
19
22
 
@@ -28,154 +31,6 @@ def is_url(path: str) -> bool:
28
31
  return bool(url_pattern.match(path))
29
32
 
30
33
 
31
- def parse_file_path(server_spec: str) -> tuple[Path, str | None]:
32
- """Parse a file path that may include a server object specification.
33
-
34
- Args:
35
- server_spec: Path to file, optionally with :object suffix
36
-
37
- Returns:
38
- Tuple of (file_path, server_object)
39
- """
40
- # First check if we have a Windows path (e.g., C:\...)
41
- has_windows_drive = len(server_spec) > 1 and server_spec[1] == ":"
42
-
43
- # Split on the last colon, but only if it's not part of the Windows drive letter
44
- # and there's actually another colon in the string after the drive letter
45
- if ":" in (server_spec[2:] if has_windows_drive else server_spec):
46
- file_str, server_object = server_spec.rsplit(":", 1)
47
- else:
48
- file_str, server_object = server_spec, None
49
-
50
- # Resolve the file path
51
- file_path = Path(file_str).expanduser().resolve()
52
- if not file_path.exists():
53
- logger.error(f"File not found: {file_path}")
54
- sys.exit(1)
55
- if not file_path.is_file():
56
- logger.error(f"Not a file: {file_path}")
57
- sys.exit(1)
58
-
59
- return file_path, server_object
60
-
61
-
62
- async def import_server(file: Path, server_or_factory: str | None = None) -> Any:
63
- """Import a MCP server from a file.
64
-
65
- Args:
66
- file: Path to the file
67
- server_or_factory: Optional object name in format "module:object" or just "object"
68
-
69
- Returns:
70
- The server object (or result of calling a factory function)
71
- """
72
- # Add parent directory to Python path so imports can be resolved
73
- file_dir = str(file.parent)
74
- if file_dir not in sys.path:
75
- sys.path.insert(0, file_dir)
76
-
77
- # Import the module
78
- spec = importlib.util.spec_from_file_location("server_module", file)
79
- if not spec or not spec.loader:
80
- logger.error("Could not load module", extra={"file": str(file)})
81
- sys.exit(1)
82
-
83
- assert spec is not None
84
- assert spec.loader is not None
85
-
86
- module = importlib.util.module_from_spec(spec)
87
- spec.loader.exec_module(module)
88
-
89
- # If no object specified, try common server names
90
- if not server_or_factory:
91
- # Look for common server instance names
92
- for name in ["mcp", "server", "app"]:
93
- if hasattr(module, name):
94
- obj = getattr(module, name)
95
- return await _resolve_server_or_factory(obj, file, name)
96
-
97
- logger.error(
98
- f"No server object found in {file}. Please either:\n"
99
- "1. Use a standard variable name (mcp, server, or app)\n"
100
- "2. Specify the object name with file:object syntax",
101
- extra={"file": str(file)},
102
- )
103
- sys.exit(1)
104
-
105
- assert server_or_factory is not None
106
-
107
- # Handle module:object syntax
108
- if ":" in server_or_factory:
109
- module_name, object_name = server_or_factory.split(":", 1)
110
- try:
111
- server_module = importlib.import_module(module_name)
112
- obj = getattr(server_module, object_name, None)
113
- except ImportError:
114
- logger.error(
115
- f"Could not import module '{module_name}'",
116
- extra={"file": str(file)},
117
- )
118
- sys.exit(1)
119
- else:
120
- # Just object name
121
- obj = getattr(module, server_or_factory, None)
122
-
123
- if obj is None:
124
- logger.error(
125
- f"Server object '{server_or_factory}' not found",
126
- extra={"file": str(file)},
127
- )
128
- sys.exit(1)
129
-
130
- return await _resolve_server_or_factory(obj, file, server_or_factory)
131
-
132
-
133
- async def _resolve_server_or_factory(obj: Any, file: Path, name: str) -> Any:
134
- """Resolve a server object or factory function to a server instance.
135
-
136
- Args:
137
- obj: The object that might be a server or factory function
138
- file: Path to the file for error messages
139
- name: Name of the object for error messages
140
-
141
- Returns:
142
- A server instance
143
- """
144
- # Check if it's a function or coroutine function
145
- if inspect.isfunction(obj) or inspect.iscoroutinefunction(obj):
146
- logger.debug(f"Found factory function '{name}' in {file}")
147
-
148
- try:
149
- if inspect.iscoroutinefunction(obj):
150
- # Async factory function
151
- server = await obj()
152
- else:
153
- # Sync factory function
154
- server = obj()
155
-
156
- # Validate the result is a FastMCP server
157
- if not isinstance(server, FastMCP | FastMCP1x):
158
- logger.error(
159
- f"Factory function '{name}' must return a FastMCP server instance, "
160
- f"got {type(server).__name__}",
161
- extra={"file": str(file)},
162
- )
163
- sys.exit(1)
164
-
165
- logger.debug(f"Factory function '{name}' created server: {server.name}")
166
- return server
167
-
168
- except Exception as e:
169
- logger.error(
170
- f"Failed to call factory function '{name}': {e}",
171
- extra={"file": str(file)},
172
- )
173
- sys.exit(1)
174
-
175
- # Not a function, return as-is (should be a server instance)
176
- return obj
177
-
178
-
179
34
  def run_with_uv(
180
35
  server_spec: str,
181
36
  python_version: str | None = None,
@@ -188,11 +43,16 @@ def run_with_uv(
188
43
  path: str | None = None,
189
44
  log_level: LogLevelType | None = None,
190
45
  show_banner: bool = True,
46
+ editable: str | list[str] | None = None,
191
47
  ) -> None:
192
48
  """Run a MCP server using uv run subprocess.
193
49
 
50
+ This function is called when we need to set up a Python environment with specific
51
+ dependencies before running the server. The config parsing and merging should already
52
+ be done by the caller.
53
+
194
54
  Args:
195
- server_spec: Python file, object specification (file:obj), or URL
55
+ server_spec: Python file, object specification (file:obj), config file, or URL
196
56
  python_version: Python version to use (e.g. "3.10")
197
57
  with_packages: Additional packages to install
198
58
  with_requirements: Requirements file to use
@@ -203,51 +63,49 @@ def run_with_uv(
203
63
  path: Path to bind to when using http transport
204
64
  log_level: Log level
205
65
  show_banner: Whether to show the server banner
66
+ editable: Editable package paths
206
67
  """
207
- cmd = ["uv", "run"]
208
-
209
- # Add Python version if specified
210
- if python_version:
211
- cmd.extend(["--python", python_version])
212
-
213
- # Add project if specified
214
- if project:
215
- cmd.extend(["--project", str(project)])
216
-
217
- # Add fastmcp package
218
- cmd.extend(["--with", "fastmcp"])
219
-
220
- # Add additional packages
221
- if with_packages:
222
- for pkg in with_packages:
223
- if pkg:
224
- cmd.extend(["--with", pkg])
225
-
226
- # Add requirements file
227
- if with_requirements:
228
- cmd.extend(["--with-requirements", str(with_requirements)])
229
68
 
230
- # Add fastmcp run command
231
- cmd.extend(["fastmcp", "run", server_spec])
232
-
233
- # Add transport options
69
+ # Build uv command using Environment.build_uv_run_command()
70
+ env_config = UVEnvironment(
71
+ python=python_version,
72
+ dependencies=with_packages if with_packages else None,
73
+ requirements=str(with_requirements.resolve()) if with_requirements else None,
74
+ project=str(project.resolve()) if project else None,
75
+ editable=editable
76
+ if isinstance(editable, list)
77
+ else ([editable] if editable else None),
78
+ )
79
+
80
+ # Build the inner fastmcp command (environment variable prevents infinite recursion)
81
+ inner_cmd = ["fastmcp", "run", server_spec]
82
+
83
+ # Add transport options to the inner command
234
84
  if transport:
235
- cmd.extend(["--transport", transport])
236
- if host:
237
- cmd.extend(["--host", host])
238
- if port:
239
- cmd.extend(["--port", str(port)])
240
- if path:
241
- cmd.extend(["--path", path])
85
+ inner_cmd.extend(["--transport", transport])
86
+ # Only add HTTP-specific options for non-stdio transports
87
+ if transport != "stdio":
88
+ if host:
89
+ inner_cmd.extend(["--host", host])
90
+ if port:
91
+ inner_cmd.extend(["--port", str(port)])
92
+ if path:
93
+ inner_cmd.extend(["--path", path])
242
94
  if log_level:
243
- cmd.extend(["--log-level", log_level])
95
+ inner_cmd.extend(["--log-level", log_level])
244
96
  if not show_banner:
245
- cmd.append("--no-banner")
97
+ inner_cmd.append("--no-banner")
98
+
99
+ # Build the full uv command
100
+ cmd = env_config.build_command(inner_cmd)
101
+
102
+ # Set marker to prevent infinite loops when subprocess calls FastMCP again
103
+ env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
246
104
 
247
105
  # Run the command
248
106
  logger.debug(f"Running command: {' '.join(cmd)}")
249
107
  try:
250
- process = subprocess.run(cmd, check=True)
108
+ process = subprocess.run(cmd, check=True, env=env)
251
109
  sys.exit(process.returncode)
252
110
  except subprocess.CalledProcessError as e:
253
111
  logger.error(f"Failed to run server: {e}")
@@ -285,30 +143,21 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
285
143
  return server
286
144
 
287
145
 
288
- async def import_server_with_args(
289
- file: Path,
290
- server_or_factory: str | None = None,
291
- server_args: list[str] | None = None,
292
- ) -> Any:
293
- """Import a server with optional command line arguments.
146
+ def load_mcp_server_config(config_path: Path) -> MCPServerConfig:
147
+ """Load a FastMCP configuration from a fastmcp.json file.
294
148
 
295
149
  Args:
296
- file: Path to the server file
297
- server_or_factory: Optional server object or factory function name
298
- server_args: Optional command line arguments to inject
150
+ config_path: Path to fastmcp.json file
299
151
 
300
152
  Returns:
301
- The imported server object
153
+ MCPServerConfig object
302
154
  """
303
- if server_args:
304
- original_argv = sys.argv[:]
305
- try:
306
- sys.argv = [str(file)] + server_args
307
- return await import_server(file, server_or_factory)
308
- finally:
309
- sys.argv = original_argv
310
- else:
311
- return await import_server(file, server_or_factory)
155
+ config = MCPServerConfig.from_file(config_path)
156
+
157
+ # Apply runtime settings from deployment config
158
+ config.deployment.apply_runtime_settings(config_path)
159
+
160
+ return config
312
161
 
313
162
 
314
163
  async def run_command(
@@ -321,11 +170,12 @@ async def run_command(
321
170
  server_args: list[str] | None = None,
322
171
  show_banner: bool = True,
323
172
  use_direct_import: bool = False,
173
+ skip_source: bool = False,
324
174
  ) -> None:
325
175
  """Run a MCP server or connect to a remote one.
326
176
 
327
177
  Args:
328
- server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
178
+ server_spec: Python file, object specification (file:obj), config file, or URL
329
179
  transport: Transport protocol to use
330
180
  host: Host to bind to when using http transport
331
181
  port: Port to bind to when using http transport
@@ -334,18 +184,73 @@ async def run_command(
334
184
  server_args: Additional arguments to pass to the server
335
185
  show_banner: Whether to show the server banner
336
186
  use_direct_import: Whether to use direct import instead of subprocess
187
+ skip_source: Whether to skip source preparation step
337
188
  """
189
+ # Special case: URLs
338
190
  if is_url(server_spec):
339
191
  # Handle URL case
340
192
  server = create_client_server(server_spec)
341
193
  logger.debug(f"Created client proxy server for {server_spec}")
194
+ # Special case: MCPConfig files (legacy)
342
195
  elif server_spec.endswith(".json"):
343
- server = create_mcp_config_server(Path(server_spec))
196
+ # Load JSON and check which type of config it is
197
+ config_path = Path(server_spec)
198
+ with open(config_path) as f:
199
+ data = json.load(f)
200
+
201
+ # Check if it's an MCPConfig first (has canonical mcpServers key)
202
+ if "mcpServers" in data:
203
+ # It's an MCP config
204
+ server = create_mcp_config_server(config_path)
205
+ else:
206
+ # It's a FastMCP config - load it properly
207
+ config = load_mcp_server_config(config_path)
208
+
209
+ # Merge deployment config with CLI arguments (CLI takes precedence)
210
+ transport = transport or config.deployment.transport
211
+ host = host or config.deployment.host
212
+ port = port or config.deployment.port
213
+ path = path or config.deployment.path
214
+ log_level = log_level or config.deployment.log_level
215
+ server_args = (
216
+ server_args if server_args is not None else config.deployment.args
217
+ )
218
+
219
+ # Prepare source only (environment is handled by uv run)
220
+ await config.prepare_source() if not skip_source else None
221
+
222
+ # Load the server using the source
223
+ from contextlib import nullcontext
224
+
225
+ from fastmcp.cli.cli import with_argv
226
+
227
+ # Use sys.argv context manager if deployment args specified
228
+ argv_context = with_argv(server_args) if server_args else nullcontext()
229
+
230
+ with argv_context:
231
+ server = await config.source.load_server()
232
+
233
+ logger.debug(f'Found server "{server.name}" from config {config_path}')
344
234
  else:
345
- # Handle file case
346
- file, server_or_factory = parse_file_path(server_spec)
347
- server = await import_server_with_args(file, server_or_factory, server_args)
348
- logger.debug(f'Found server "{server.name}" in {file}')
235
+ # Regular file case - create a MCPServerConfig with FileSystemSource
236
+ source = FileSystemSource(path=server_spec)
237
+ config = MCPServerConfig(source=source)
238
+
239
+ # Prepare source only (environment is handled by uv run)
240
+ await config.prepare_source() if not skip_source else None
241
+
242
+ # Load the server
243
+ from contextlib import nullcontext
244
+
245
+ from fastmcp.cli.cli import with_argv
246
+
247
+ # Use sys.argv context manager if server_args specified
248
+ argv_context = with_argv(server_args) if server_args else nullcontext()
249
+
250
+ with argv_context:
251
+ server = await config.source.load_server()
252
+
253
+ logger.debug(f'Found server "{server.name}" in {source.path}')
349
254
 
350
255
  # Run the server
351
256
 
@@ -363,8 +268,8 @@ async def run_command(
363
268
  kwargs["port"] = port
364
269
  if path:
365
270
  kwargs["path"] = path
366
- if log_level:
367
- kwargs["log_level"] = log_level
271
+ # Note: log_level is not currently supported by run_async
272
+ # TODO: Add log_level support to server.run_async
368
273
 
369
274
  if not show_banner:
370
275
  kwargs["show_banner"] = False
@@ -382,6 +287,8 @@ def run_v1_server(
382
287
  port: int | None = None,
383
288
  transport: TransportType | None = None,
384
289
  ) -> None:
290
+ from functools import partial
291
+
385
292
  if host:
386
293
  server.settings.host = host
387
294
  if port: