fastmcp 2.3.3__py3-none-any.whl → 2.3.5__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.
fastmcp/cli/cli.py CHANGED
@@ -17,6 +17,7 @@ from typer import Context, Exit
17
17
 
18
18
  import fastmcp
19
19
  from fastmcp.cli import claude
20
+ from fastmcp.cli import run as run_module
20
21
  from fastmcp.utilities.logging import get_logger
21
22
 
22
23
  logger = get_logger("cli")
@@ -58,7 +59,7 @@ def _parse_env_var(env_var: str) -> tuple[str, str]:
58
59
 
59
60
 
60
61
  def _build_uv_command(
61
- file_spec: str,
62
+ server_spec: str,
62
63
  with_editable: Path | None = None,
63
64
  with_packages: list[str] | None = None,
64
65
  ) -> list[str]:
@@ -76,106 +77,10 @@ def _build_uv_command(
76
77
  cmd.extend(["--with", pkg])
77
78
 
78
79
  # Add mcp run command
79
- cmd.extend(["fastmcp", "run", file_spec])
80
+ cmd.extend(["fastmcp", "run", server_spec])
80
81
  return cmd
81
82
 
82
83
 
83
- def _parse_file_path(file_spec: str) -> tuple[Path, str | None]:
84
- """Parse a file path that may include a server object specification.
85
-
86
- Args:
87
- file_spec: Path to file, optionally with :object suffix
88
-
89
- Returns:
90
- Tuple of (file_path, server_object)
91
- """
92
- # First check if we have a Windows path (e.g., C:\...)
93
- has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"
94
-
95
- # Split on the last colon, but only if it's not part of the Windows drive letter
96
- # and there's actually another colon in the string after the drive letter
97
- if ":" in (file_spec[2:] if has_windows_drive else file_spec):
98
- file_str, server_object = file_spec.rsplit(":", 1)
99
- else:
100
- file_str, server_object = file_spec, None
101
-
102
- # Resolve the file path
103
- file_path = Path(file_str).expanduser().resolve()
104
- if not file_path.exists():
105
- logger.error(f"File not found: {file_path}")
106
- sys.exit(1)
107
- if not file_path.is_file():
108
- logger.error(f"Not a file: {file_path}")
109
- sys.exit(1)
110
-
111
- return file_path, server_object
112
-
113
-
114
- def _import_server(file: Path, server_object: str | None = None):
115
- """Import a MCP server from a file.
116
-
117
- Args:
118
- file: Path to the file
119
- server_object: Optional object name in format "module:object" or just "object"
120
-
121
- Returns:
122
- The server object
123
- """
124
- # Add parent directory to Python path so imports can be resolved
125
- file_dir = str(file.parent)
126
- if file_dir not in sys.path:
127
- sys.path.insert(0, file_dir)
128
-
129
- # Import the module
130
- spec = importlib.util.spec_from_file_location("server_module", file)
131
- if not spec or not spec.loader:
132
- logger.error("Could not load module", extra={"file": str(file)})
133
- sys.exit(1)
134
-
135
- module = importlib.util.module_from_spec(spec)
136
- spec.loader.exec_module(module)
137
-
138
- # If no object specified, try common server names
139
- if not server_object:
140
- # Look for the most common server object names
141
- for name in ["mcp", "server", "app"]:
142
- if hasattr(module, name):
143
- return getattr(module, name)
144
-
145
- logger.error(
146
- f"No server object found in {file}. Please either:\n"
147
- "1. Use a standard variable name (mcp, server, or app)\n"
148
- "2. Specify the object name with file:object syntax",
149
- extra={"file": str(file)},
150
- )
151
- sys.exit(1)
152
-
153
- # Handle module:object syntax
154
- if ":" in server_object:
155
- module_name, object_name = server_object.split(":", 1)
156
- try:
157
- server_module = importlib.import_module(module_name)
158
- server = getattr(server_module, object_name, None)
159
- except ImportError:
160
- logger.error(
161
- f"Could not import module '{module_name}'",
162
- extra={"file": str(file)},
163
- )
164
- sys.exit(1)
165
- else:
166
- # Just object name
167
- server = getattr(module, server_object, None)
168
-
169
- if server is None:
170
- logger.error(
171
- f"Server object '{server_object}' not found",
172
- extra={"file": str(file)},
173
- )
174
- sys.exit(1)
175
-
176
- return server
177
-
178
-
179
84
  @app.command()
180
85
  def version(ctx: Context):
181
86
  if ctx.resilient_parsing:
@@ -201,7 +106,7 @@ def version(ctx: Context):
201
106
 
202
107
  @app.command()
203
108
  def dev(
204
- file_spec: str = typer.Argument(
109
+ server_spec: str = typer.Argument(
205
110
  ...,
206
111
  help="Python file to run, optionally with :object suffix",
207
112
  ),
@@ -246,7 +151,7 @@ def dev(
246
151
  ] = None,
247
152
  ) -> None:
248
153
  """Run a MCP server with the MCP Inspector."""
249
- file, server_object = _parse_file_path(file_spec)
154
+ file, server_object = run_module.parse_file_path(server_spec)
250
155
 
251
156
  logger.debug(
252
157
  "Starting dev server",
@@ -262,8 +167,8 @@ def dev(
262
167
 
263
168
  try:
264
169
  # Import server to get dependencies
265
- server = _import_server(file, server_object)
266
- if hasattr(server, "dependencies"):
170
+ server = run_module.import_server(file, server_object)
171
+ if hasattr(server, "dependencies") and server.dependencies is not None:
267
172
  with_packages = list(set(with_packages + server.dependencies))
268
173
 
269
174
  env_vars = {}
@@ -285,7 +190,7 @@ def dev(
285
190
  if inspector_version:
286
191
  inspector_cmd += f"@{inspector_version}"
287
192
 
288
- uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
193
+ uv_cmd = _build_uv_command(server_spec, with_editable, with_packages)
289
194
 
290
195
  # Run the MCP Inspector command with shell=True on Windows
291
196
  shell = sys.platform == "win32"
@@ -318,9 +223,9 @@ def dev(
318
223
 
319
224
  @app.command()
320
225
  def run(
321
- file_spec: str = typer.Argument(
226
+ server_spec: str = typer.Argument(
322
227
  ...,
323
- help="Python file to run, optionally with :object suffix",
228
+ help="Python file, object specification (file:obj), or URL",
324
229
  ),
325
230
  transport: Annotated[
326
231
  str | None,
@@ -354,22 +259,20 @@ def run(
354
259
  ),
355
260
  ] = None,
356
261
  ) -> None:
357
- """Run a MCP server.
262
+ """Run a MCP server or connect to a remote one.
358
263
 
359
- The server can be specified in two ways:
360
- 1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n
361
- 2. Import approach: server.py:app - imports and runs the specified server object.\n\n
264
+ The server can be specified in three ways:
265
+ 1. Module approach: server.py - runs the module directly, looking for an object named mcp/server/app.\n
266
+ 2. Import approach: server.py:app - imports and runs the specified server object.\n
267
+ 3. URL approach: http://server-url - connects to a remote server and creates a proxy.\n\n
362
268
 
363
269
  Note: This command runs the server directly. You are responsible for ensuring
364
270
  all dependencies are available.
365
271
  """
366
- file, server_object = _parse_file_path(file_spec)
367
-
368
272
  logger.debug(
369
- "Running server",
273
+ "Running server or client",
370
274
  extra={
371
- "file": str(file),
372
- "server_object": server_object,
275
+ "server_spec": server_spec,
373
276
  "transport": transport,
374
277
  "host": host,
375
278
  "port": port,
@@ -378,29 +281,18 @@ def run(
378
281
  )
379
282
 
380
283
  try:
381
- # Import and get server object
382
- server = _import_server(file, server_object)
383
-
384
- logger.info(f'Found server "{server.name}" in {file}')
385
-
386
- # Run the server
387
- kwargs = {}
388
- if transport:
389
- kwargs["transport"] = transport
390
- if host:
391
- kwargs["host"] = host
392
- if port:
393
- kwargs["port"] = port
394
- if log_level:
395
- kwargs["log_level"] = log_level
396
-
397
- server.run(**kwargs)
398
-
284
+ run_module.run_command(
285
+ server_spec=server_spec,
286
+ transport=transport,
287
+ host=host,
288
+ port=port,
289
+ log_level=log_level,
290
+ )
399
291
  except Exception as e:
400
292
  logger.error(
401
- f"Failed to run server: {e}",
293
+ f"Failed to run: {e}",
402
294
  extra={
403
- "file": str(file),
295
+ "server_spec": server_spec,
404
296
  "error": str(e),
405
297
  },
406
298
  )
@@ -409,7 +301,7 @@ def run(
409
301
 
410
302
  @app.command()
411
303
  def install(
412
- file_spec: str = typer.Argument(
304
+ server_spec: str = typer.Argument(
413
305
  ...,
414
306
  help="Python file to run, optionally with :object suffix",
415
307
  ),
@@ -466,7 +358,7 @@ def install(
466
358
  Environment variables are preserved once added and only updated if new values
467
359
  are explicitly provided.
468
360
  """
469
- file, server_object = _parse_file_path(file_spec)
361
+ file, server_object = run_module.parse_file_path(server_spec)
470
362
 
471
363
  logger.debug(
472
364
  "Installing server",
@@ -489,7 +381,7 @@ def install(
489
381
  server = None
490
382
  if not name:
491
383
  try:
492
- server = _import_server(file, server_object)
384
+ server = run_module.import_server(file, server_object)
493
385
  name = server.name
494
386
  except (ImportError, ModuleNotFoundError) as e:
495
387
  logger.debug(
@@ -526,7 +418,7 @@ def install(
526
418
  env_dict[key] = value
527
419
 
528
420
  if claude.update_claude_config(
529
- file_spec,
421
+ server_spec,
530
422
  name,
531
423
  with_editable=with_editable,
532
424
  with_packages=with_packages,
fastmcp/cli/run.py ADDED
@@ -0,0 +1,179 @@
1
+ """FastMCP run command implementation."""
2
+
3
+ import importlib.util
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ from fastmcp.utilities.logging import get_logger
10
+
11
+ logger = get_logger("cli.run")
12
+
13
+ TransportType = Literal["stdio", "streamable-http", "sse"]
14
+
15
+
16
+ def is_url(path: str) -> bool:
17
+ """Check if a string is a URL."""
18
+ url_pattern = re.compile(r"^https?://")
19
+ return bool(url_pattern.match(path))
20
+
21
+
22
+ def parse_file_path(server_spec: str) -> tuple[Path, str | None]:
23
+ """Parse a file path that may include a server object specification.
24
+
25
+ Args:
26
+ server_spec: Path to file, optionally with :object suffix
27
+
28
+ Returns:
29
+ Tuple of (file_path, server_object)
30
+ """
31
+ # First check if we have a Windows path (e.g., C:\...)
32
+ has_windows_drive = len(server_spec) > 1 and server_spec[1] == ":"
33
+
34
+ # Split on the last colon, but only if it's not part of the Windows drive letter
35
+ # and there's actually another colon in the string after the drive letter
36
+ if ":" in (server_spec[2:] if has_windows_drive else server_spec):
37
+ file_str, server_object = server_spec.rsplit(":", 1)
38
+ else:
39
+ file_str, server_object = server_spec, None
40
+
41
+ # Resolve the file path
42
+ file_path = Path(file_str).expanduser().resolve()
43
+ if not file_path.exists():
44
+ logger.error(f"File not found: {file_path}")
45
+ sys.exit(1)
46
+ if not file_path.is_file():
47
+ logger.error(f"Not a file: {file_path}")
48
+ sys.exit(1)
49
+
50
+ return file_path, server_object
51
+
52
+
53
+ def import_server(file: Path, server_object: str | None = None) -> Any:
54
+ """Import a MCP server from a file.
55
+
56
+ Args:
57
+ file: Path to the file
58
+ server_object: Optional object name in format "module:object" or just "object"
59
+
60
+ Returns:
61
+ The server object
62
+ """
63
+ # Add parent directory to Python path so imports can be resolved
64
+ file_dir = str(file.parent)
65
+ if file_dir not in sys.path:
66
+ sys.path.insert(0, file_dir)
67
+
68
+ # Import the module
69
+ spec = importlib.util.spec_from_file_location("server_module", file)
70
+ if not spec or not spec.loader:
71
+ logger.error("Could not load module", extra={"file": str(file)})
72
+ sys.exit(1)
73
+
74
+ module = importlib.util.module_from_spec(spec)
75
+ spec.loader.exec_module(module)
76
+
77
+ # If no object specified, try common server names
78
+ if not server_object:
79
+ # Look for the most common server object names
80
+ for name in ["mcp", "server", "app"]:
81
+ if hasattr(module, name):
82
+ return getattr(module, name)
83
+
84
+ logger.error(
85
+ f"No server object found in {file}. Please either:\n"
86
+ "1. Use a standard variable name (mcp, server, or app)\n"
87
+ "2. Specify the object name with file:object syntax",
88
+ extra={"file": str(file)},
89
+ )
90
+ sys.exit(1)
91
+
92
+ # Handle module:object syntax
93
+ if ":" in server_object:
94
+ module_name, object_name = server_object.split(":", 1)
95
+ try:
96
+ server_module = importlib.import_module(module_name)
97
+ server = getattr(server_module, object_name, None)
98
+ except ImportError:
99
+ logger.error(
100
+ f"Could not import module '{module_name}'",
101
+ extra={"file": str(file)},
102
+ )
103
+ sys.exit(1)
104
+ else:
105
+ # Just object name
106
+ server = getattr(module, server_object, None)
107
+
108
+ if server is None:
109
+ logger.error(
110
+ f"Server object '{server_object}' not found",
111
+ extra={"file": str(file)},
112
+ )
113
+ sys.exit(1)
114
+
115
+ return server
116
+
117
+
118
+ def create_client_server(url: str) -> Any:
119
+ """Create a FastMCP server from a client URL.
120
+
121
+ Args:
122
+ url: The URL to connect to
123
+
124
+ Returns:
125
+ A FastMCP server instance
126
+ """
127
+ try:
128
+ import fastmcp
129
+
130
+ client = fastmcp.Client(url)
131
+ server = fastmcp.FastMCP.from_client(client)
132
+ return server
133
+ except Exception as e:
134
+ logger.error(f"Failed to create client for URL {url}: {e}")
135
+ sys.exit(1)
136
+
137
+
138
+ def run_command(
139
+ server_spec: str,
140
+ transport: str | None = None,
141
+ host: str | None = None,
142
+ port: int | None = None,
143
+ log_level: str | None = None,
144
+ ) -> None:
145
+ """Run a MCP server or connect to a remote one.
146
+
147
+ Args:
148
+ server_spec: Python file, object specification (file:obj), or URL
149
+ transport: Transport protocol to use
150
+ host: Host to bind to when using http transport
151
+ port: Port to bind to when using http transport
152
+ log_level: Log level
153
+ """
154
+ if is_url(server_spec):
155
+ # Handle URL case
156
+ server = create_client_server(server_spec)
157
+ logger.debug(f"Created client proxy server for {server_spec}")
158
+ else:
159
+ # Handle file case
160
+ file, server_object = parse_file_path(server_spec)
161
+ server = import_server(file, server_object)
162
+ logger.debug(f'Found server "{server.name}" in {file}')
163
+
164
+ # Run the server
165
+ kwargs = {}
166
+ if transport:
167
+ kwargs["transport"] = transport
168
+ if host:
169
+ kwargs["host"] = host
170
+ if port:
171
+ kwargs["port"] = port
172
+ if log_level:
173
+ kwargs["log_level"] = log_level
174
+
175
+ try:
176
+ server.run(**kwargs)
177
+ except Exception as e:
178
+ logger.error(f"Failed to run server: {e}")
179
+ sys.exit(1)
@@ -9,6 +9,7 @@ from .transports import (
9
9
  UvxStdioTransport,
10
10
  NpxStdioTransport,
11
11
  FastMCPTransport,
12
+ StreamableHttpTransport,
12
13
  )
13
14
 
14
15
  __all__ = [
@@ -22,4 +23,5 @@ __all__ = [
22
23
  "UvxStdioTransport",
23
24
  "NpxStdioTransport",
24
25
  "FastMCPTransport",
26
+ "StreamableHttpTransport",
25
27
  ]