Jeremiah Lowin commited on
Commit
7c466d5
·
1 Parent(s): 8c74b9c
pyproject.toml CHANGED
@@ -4,6 +4,7 @@ dynamic = ["version"]
4
  description = "An ergonomic MCP interface"
5
  authors = [{ name = "Jeremiah Lowin" }]
6
  dependencies = [
 
7
  "mcp>=1.6.0,<2.0.0",
8
  "rich>=13.9.4",
9
  "typer>=0.15.2",
 
4
  description = "An ergonomic MCP interface"
5
  authors = [{ name = "Jeremiah Lowin" }]
6
  dependencies = [
7
+ "dotenv>=0.9.9",
8
  "mcp>=1.6.0,<2.0.0",
9
  "rich>=13.9.4",
10
  "typer>=0.15.2",
src/fastmcp/cli/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """FastMCP CLI package."""
2
+
3
+ from .cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
src/fastmcp/cli/claude.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Claude app integration utilities."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from fastmcp.utilities.logging import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ def get_claude_config_path() -> Path | None:
15
+ """Get the Claude config directory based on platform."""
16
+ if sys.platform == "win32":
17
+ path = Path(Path.home(), "AppData", "Roaming", "Claude")
18
+ elif sys.platform == "darwin":
19
+ path = Path(Path.home(), "Library", "Application Support", "Claude")
20
+ elif sys.platform.startswith("linux"):
21
+ path = Path(
22
+ os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude"
23
+ )
24
+ else:
25
+ return None
26
+
27
+ if path.exists():
28
+ return path
29
+ return None
30
+
31
+
32
+ def update_claude_config(
33
+ file_spec: str,
34
+ server_name: str,
35
+ *,
36
+ with_editable: Path | None = None,
37
+ with_packages: list[str] | None = None,
38
+ env_vars: dict[str, str] | None = None,
39
+ ) -> bool:
40
+ """Add or update a FastMCP server in Claude's configuration.
41
+
42
+ Args:
43
+ file_spec: Path to the server file, optionally with :object suffix
44
+ server_name: Name for the server in Claude's config
45
+ with_editable: Optional directory to install in editable mode
46
+ with_packages: Optional list of additional packages to install
47
+ env_vars: Optional dictionary of environment variables. These are merged with
48
+ any existing variables, with new values taking precedence.
49
+
50
+ Raises:
51
+ RuntimeError: If Claude Desktop's config directory is not found, indicating
52
+ Claude Desktop may not be installed or properly set up.
53
+ """
54
+ config_dir = get_claude_config_path()
55
+ if not config_dir:
56
+ raise RuntimeError(
57
+ "Claude Desktop config directory not found. Please ensure Claude Desktop"
58
+ " is installed and has been run at least once to initialize its config."
59
+ )
60
+
61
+ config_file = config_dir / "claude_desktop_config.json"
62
+ if not config_file.exists():
63
+ try:
64
+ config_file.write_text("{}")
65
+ except Exception as e:
66
+ logger.error(
67
+ "Failed to create Claude config file",
68
+ extra={
69
+ "error": str(e),
70
+ "config_file": str(config_file),
71
+ },
72
+ )
73
+ return False
74
+
75
+ try:
76
+ config = json.loads(config_file.read_text())
77
+ if "mcpServers" not in config:
78
+ config["mcpServers"] = {}
79
+
80
+ # Always preserve existing env vars and merge with new ones
81
+ if (
82
+ server_name in config["mcpServers"]
83
+ and "env" in config["mcpServers"][server_name]
84
+ ):
85
+ existing_env = config["mcpServers"][server_name]["env"]
86
+ if env_vars:
87
+ # New vars take precedence over existing ones
88
+ env_vars = {**existing_env, **env_vars}
89
+ else:
90
+ env_vars = existing_env
91
+
92
+ # Build uv run command
93
+ args = ["run"]
94
+
95
+ # Collect all packages in a set to deduplicate
96
+ packages = {"fastmcp"}
97
+ if with_packages:
98
+ packages.update(pkg for pkg in with_packages if pkg)
99
+
100
+ # Add all packages with --with
101
+ for pkg in sorted(packages):
102
+ args.extend(["--with", pkg])
103
+
104
+ if with_editable:
105
+ args.extend(["--with-editable", str(with_editable)])
106
+
107
+ # Convert file path to absolute before adding to command
108
+ # Split off any :object suffix first
109
+ if ":" in file_spec:
110
+ file_path, server_object = file_spec.rsplit(":", 1)
111
+ file_spec = f"{Path(file_path).resolve()}:{server_object}"
112
+ else:
113
+ file_spec = str(Path(file_spec).resolve())
114
+
115
+ # Add fastmcp run command
116
+ args.extend(["fastmcp", "run", file_spec])
117
+
118
+ server_config: dict[str, Any] = {"command": "uv", "args": args}
119
+
120
+ # Add environment variables if specified
121
+ if env_vars:
122
+ server_config["env"] = env_vars
123
+
124
+ config["mcpServers"][server_name] = server_config
125
+
126
+ config_file.write_text(json.dumps(config, indent=2))
127
+ logger.info(
128
+ f"Added server '{server_name}' to Claude config",
129
+ extra={"config_file": str(config_file)},
130
+ )
131
+ return True
132
+ except Exception as e:
133
+ logger.error(
134
+ "Failed to update Claude config",
135
+ extra={
136
+ "error": str(e),
137
+ "config_file": str(config_file),
138
+ },
139
+ )
140
+ return False
src/fastmcp/cli/cli.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastmMCP CLI tools."""
2
+
3
+ import importlib.metadata
4
+ import importlib.util
5
+ import os
6
+ import platform
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Annotated
11
+
12
+ import dotenv
13
+ import typer
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+ from typer import Context, Exit
17
+
18
+ import fastmcp
19
+ from fastmcp.cli import claude
20
+ from fastmcp.utilities.logging import get_logger
21
+
22
+ logger = get_logger("cli")
23
+ console = Console()
24
+
25
+ app = typer.Typer(
26
+ name="fastmcp",
27
+ help="FastMCP CLI",
28
+ add_completion=False,
29
+ no_args_is_help=True, # Show help if no args provided
30
+ )
31
+
32
+
33
+ def _get_npx_command():
34
+ """Get the correct npx command for the current platform."""
35
+ if sys.platform == "win32":
36
+ # Try both npx.cmd and npx.exe on Windows
37
+ for cmd in ["npx.cmd", "npx.exe", "npx"]:
38
+ try:
39
+ subprocess.run(
40
+ [cmd, "--version"], check=True, capture_output=True, shell=True
41
+ )
42
+ return cmd
43
+ except subprocess.CalledProcessError:
44
+ continue
45
+ return None
46
+ return "npx" # On Unix-like systems, just use npx
47
+
48
+
49
+ def _parse_env_var(env_var: str) -> tuple[str, str]:
50
+ """Parse environment variable string in format KEY=VALUE."""
51
+ if "=" not in env_var:
52
+ logger.error(
53
+ f"Invalid environment variable format: {env_var}. Must be KEY=VALUE"
54
+ )
55
+ sys.exit(1)
56
+ key, value = env_var.split("=", 1)
57
+ return key.strip(), value.strip()
58
+
59
+
60
+ def _build_uv_command(
61
+ file_spec: str,
62
+ with_editable: Path | None = None,
63
+ with_packages: list[str] | None = None,
64
+ ) -> list[str]:
65
+ """Build the uv run command that runs a MCP server through mcp run."""
66
+ cmd = ["uv"]
67
+
68
+ cmd.extend(["run", "--with", "mcp"])
69
+
70
+ if with_editable:
71
+ cmd.extend(["--with-editable", str(with_editable)])
72
+
73
+ if with_packages:
74
+ for pkg in with_packages:
75
+ if pkg:
76
+ cmd.extend(["--with", pkg])
77
+
78
+ # Add mcp run command
79
+ cmd.extend(["mcp", "run", file_spec])
80
+ return cmd
81
+
82
+
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
+ @app.command()
180
+ def version(ctx: Context):
181
+ if ctx.resilient_parsing:
182
+ return
183
+
184
+ info = {
185
+ "FastMCP version": fastmcp.__version__,
186
+ "MCP version": importlib.metadata.version("mcp"),
187
+ "Python version": platform.python_version(),
188
+ "Platform": platform.platform(),
189
+ "FastMCP root path": f"~/{Path(__file__).resolve().parents[3].relative_to(Path.home())}",
190
+ }
191
+
192
+ g = Table.grid(padding=(0, 1))
193
+ g.add_column(style="bold", justify="left")
194
+ g.add_column(style="cyan", justify="right")
195
+ for k, v in info.items():
196
+ g.add_row(k + ":", str(v).replace("\n", " "))
197
+ console.print(g)
198
+
199
+ raise Exit()
200
+
201
+
202
+ @app.command()
203
+ def dev(
204
+ file_spec: str = typer.Argument(
205
+ ...,
206
+ help="Python file to run, optionally with :object suffix",
207
+ ),
208
+ with_editable: Annotated[
209
+ Path | None,
210
+ typer.Option(
211
+ "--with-editable",
212
+ "-e",
213
+ help="Directory containing pyproject.toml to install in editable mode",
214
+ exists=True,
215
+ file_okay=False,
216
+ resolve_path=True,
217
+ ),
218
+ ] = None,
219
+ with_packages: Annotated[
220
+ list[str],
221
+ typer.Option(
222
+ "--with",
223
+ help="Additional packages to install",
224
+ ),
225
+ ] = [],
226
+ ) -> None:
227
+ """Run a MCP server with the MCP Inspector."""
228
+ file, server_object = _parse_file_path(file_spec)
229
+
230
+ logger.debug(
231
+ "Starting dev server",
232
+ extra={
233
+ "file": str(file),
234
+ "server_object": server_object,
235
+ "with_editable": str(with_editable) if with_editable else None,
236
+ "with_packages": with_packages,
237
+ },
238
+ )
239
+
240
+ try:
241
+ # Import server to get dependencies
242
+ server = _import_server(file, server_object)
243
+ if hasattr(server, "dependencies"):
244
+ with_packages = list(set(with_packages + server.dependencies))
245
+
246
+ uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
247
+
248
+ # Get the correct npx command
249
+ npx_cmd = _get_npx_command()
250
+ if not npx_cmd:
251
+ logger.error(
252
+ "npx not found. Please ensure Node.js and npm are properly installed "
253
+ "and added to your system PATH."
254
+ )
255
+ sys.exit(1)
256
+
257
+ # Run the MCP Inspector command with shell=True on Windows
258
+ shell = sys.platform == "win32"
259
+ process = subprocess.run(
260
+ [npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
261
+ check=True,
262
+ shell=shell,
263
+ env=dict(os.environ.items()), # Convert to list of tuples for env update
264
+ )
265
+ sys.exit(process.returncode)
266
+ except subprocess.CalledProcessError as e:
267
+ logger.error(
268
+ "Dev server failed",
269
+ extra={
270
+ "file": str(file),
271
+ "error": str(e),
272
+ "returncode": e.returncode,
273
+ },
274
+ )
275
+ sys.exit(e.returncode)
276
+ except FileNotFoundError:
277
+ logger.error(
278
+ "npx not found. Please ensure Node.js and npm are properly installed "
279
+ "and added to your system PATH. You may need to restart your terminal "
280
+ "after installation.",
281
+ extra={"file": str(file)},
282
+ )
283
+ sys.exit(1)
284
+
285
+
286
+ @app.command()
287
+ def run(
288
+ file_spec: str = typer.Argument(
289
+ ...,
290
+ help="Python file to run, optionally with :object suffix",
291
+ ),
292
+ transport: Annotated[
293
+ str | None,
294
+ typer.Option(
295
+ "--transport",
296
+ "-t",
297
+ help="Transport protocol to use (stdio or sse)",
298
+ ),
299
+ ] = None,
300
+ ) -> None:
301
+ """Run a MCP server.
302
+
303
+ The server can be specified in two ways:\n
304
+ 1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n
305
+ 2. Import approach: server.py:app - imports and runs the specified server object.\n\n
306
+
307
+ Note: This command runs the server directly. You are responsible for ensuring
308
+ all dependencies are available.\n
309
+ For dependency management, use `mcp install` or `mcp dev` instead.
310
+ """ # noqa: E501
311
+ file, server_object = _parse_file_path(file_spec)
312
+
313
+ logger.debug(
314
+ "Running server",
315
+ extra={
316
+ "file": str(file),
317
+ "server_object": server_object,
318
+ "transport": transport,
319
+ },
320
+ )
321
+
322
+ try:
323
+ # Import and get server object
324
+ server = _import_server(file, server_object)
325
+
326
+ # Run the server
327
+ kwargs = {}
328
+ if transport:
329
+ kwargs["transport"] = transport
330
+
331
+ server.run(**kwargs)
332
+
333
+ except Exception as e:
334
+ logger.error(
335
+ f"Failed to run server: {e}",
336
+ extra={
337
+ "file": str(file),
338
+ "error": str(e),
339
+ },
340
+ )
341
+ sys.exit(1)
342
+
343
+
344
+ @app.command()
345
+ def install(
346
+ file_spec: str = typer.Argument(
347
+ ...,
348
+ help="Python file to run, optionally with :object suffix",
349
+ ),
350
+ server_name: Annotated[
351
+ str | None,
352
+ typer.Option(
353
+ "--name",
354
+ "-n",
355
+ help="Custom name for the server (defaults to server's name attribute or"
356
+ " file name)",
357
+ ),
358
+ ] = None,
359
+ with_editable: Annotated[
360
+ Path | None,
361
+ typer.Option(
362
+ "--with-editable",
363
+ "-e",
364
+ help="Directory containing pyproject.toml to install in editable mode",
365
+ exists=True,
366
+ file_okay=False,
367
+ resolve_path=True,
368
+ ),
369
+ ] = None,
370
+ with_packages: Annotated[
371
+ list[str],
372
+ typer.Option(
373
+ "--with",
374
+ help="Additional packages to install",
375
+ ),
376
+ ] = [],
377
+ env_vars: Annotated[
378
+ list[str],
379
+ typer.Option(
380
+ "--env-var",
381
+ "-v",
382
+ help="Environment variables in KEY=VALUE format",
383
+ ),
384
+ ] = [],
385
+ env_file: Annotated[
386
+ Path | None,
387
+ typer.Option(
388
+ "--env-file",
389
+ "-f",
390
+ help="Load environment variables from a .env file",
391
+ exists=True,
392
+ file_okay=True,
393
+ dir_okay=False,
394
+ resolve_path=True,
395
+ ),
396
+ ] = None,
397
+ ) -> None:
398
+ """Install a MCP server in the Claude desktop app.
399
+
400
+ Environment variables are preserved once added and only updated if new values
401
+ are explicitly provided.
402
+ """
403
+ file, server_object = _parse_file_path(file_spec)
404
+
405
+ logger.debug(
406
+ "Installing server",
407
+ extra={
408
+ "file": str(file),
409
+ "server_name": server_name,
410
+ "server_object": server_object,
411
+ "with_editable": str(with_editable) if with_editable else None,
412
+ "with_packages": with_packages,
413
+ },
414
+ )
415
+
416
+ if not claude.get_claude_config_path():
417
+ logger.error("Claude app not found")
418
+ sys.exit(1)
419
+
420
+ # Try to import server to get its name, but fall back to file name if dependencies
421
+ # missing
422
+ name = server_name
423
+ server = None
424
+ if not name:
425
+ try:
426
+ server = _import_server(file, server_object)
427
+ name = server.name
428
+ except (ImportError, ModuleNotFoundError) as e:
429
+ logger.debug(
430
+ "Could not import server (likely missing dependencies), using file"
431
+ " name",
432
+ extra={"error": str(e)},
433
+ )
434
+ name = file.stem
435
+
436
+ # Get server dependencies if available
437
+ server_dependencies = getattr(server, "dependencies", []) if server else []
438
+ if server_dependencies:
439
+ with_packages = list(set(with_packages + server_dependencies))
440
+
441
+ # Process environment variables if provided
442
+ env_dict: dict[str, str] | None = None
443
+ if env_file or env_vars:
444
+ env_dict = {}
445
+ # Load from .env file if specified
446
+ if env_file:
447
+ try:
448
+ env_dict |= {
449
+ k: v
450
+ for k, v in dotenv.dotenv_values(env_file).items()
451
+ if v is not None
452
+ }
453
+ except Exception as e:
454
+ logger.error(f"Failed to load .env file: {e}")
455
+ sys.exit(1)
456
+
457
+ # Add command line environment variables
458
+ for env_var in env_vars:
459
+ key, value = _parse_env_var(env_var)
460
+ env_dict[key] = value
461
+
462
+ if claude.update_claude_config(
463
+ file_spec,
464
+ name,
465
+ with_editable=with_editable,
466
+ with_packages=with_packages,
467
+ env_vars=env_dict,
468
+ ):
469
+ logger.info(f"Successfully installed {name} in Claude app")
470
+ else:
471
+ logger.error(f"Failed to install {name} in Claude app")
472
+ sys.exit(1)
uv.lock CHANGED
@@ -187,6 +187,17 @@ wheels = [
187
  { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
188
  ]
189
 
 
 
 
 
 
 
 
 
 
 
 
190
  [[package]]
191
  name = "exceptiongroup"
192
  version = "1.2.2"
@@ -229,9 +240,10 @@ wheels = [
229
 
230
  [[package]]
231
  name = "fastmcp"
232
- version = "0.4.2.dev28+g728aeec.d20250408"
233
  source = { editable = "." }
234
  dependencies = [
 
235
  { name = "mcp" },
236
  { name = "rich" },
237
  { name = "typer" },
@@ -255,6 +267,7 @@ dev = [
255
 
256
  [package.metadata]
257
  requires-dist = [
 
258
  { name = "mcp", specifier = ">=1.6.0,<2.0.0" },
259
  { name = "rich", specifier = ">=13.9.4" },
260
  { name = "typer", specifier = ">=0.15.2" },
 
187
  { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
188
  ]
189
 
190
+ [[package]]
191
+ name = "dotenv"
192
+ version = "0.9.9"
193
+ source = { registry = "https://pypi.org/simple" }
194
+ dependencies = [
195
+ { name = "python-dotenv" },
196
+ ]
197
+ wheels = [
198
+ { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 },
199
+ ]
200
+
201
  [[package]]
202
  name = "exceptiongroup"
203
  version = "1.2.2"
 
240
 
241
  [[package]]
242
  name = "fastmcp"
243
+ version = "0.4.2.dev32+g9b48745.d20250410"
244
  source = { editable = "." }
245
  dependencies = [
246
+ { name = "dotenv" },
247
  { name = "mcp" },
248
  { name = "rich" },
249
  { name = "typer" },
 
267
 
268
  [package.metadata]
269
  requires-dist = [
270
+ { name = "dotenv", specifier = ">=0.9.9" },
271
  { name = "mcp", specifier = ">=1.6.0,<2.0.0" },
272
  { name = "rich", specifier = ">=13.9.4" },
273
  { name = "typer", specifier = ">=0.15.2" },