Jeremiah Lowin commited on
Commit
ae4f592
·
1 Parent(s): 28d92a7

Support dependencies

Browse files
README.md CHANGED
@@ -62,15 +62,20 @@ FastMCP handles all the complex protocol details and server management, so you c
62
  - [Prompts](#prompts)
63
  - [Images](#images)
64
  - [Context](#context)
65
- - [Deployment](#deployment)
66
- - [Development](#development)
67
- - [Environment Variables](#environment-variables)
68
- - [Claude Desktop](#claude-desktop)
69
- - [Environment Variables](#environment-variables-1)
70
  - [Examples](#examples)
71
  - [Echo Server](#echo-server)
72
  - [SQLite Explorer](#sqlite-explorer)
73
  - [Contributing](#contributing)
 
 
 
 
 
74
 
75
  ## Installation
76
 
@@ -154,8 +159,10 @@ mcp = FastMCP("My App")
154
 
155
  # Configure host/port for HTTP transport (optional)
156
  mcp = FastMCP("My App", host="localhost", port=8000)
 
 
 
157
  ```
158
- *Note: All of the following code examples assume you've created a FastMCP server instance called `mcp`, as shown above.*
159
 
160
  ### Resources
161
 
@@ -284,84 +291,101 @@ The Context object provides:
284
  - Resource access through `read_resource()`
285
  - Request metadata via `request_id` and `client_id`
286
 
287
- ## Deployment
288
-
289
- The FastMCP CLI helps you develop and deploy MCP servers.
290
 
291
- Note that for all deployment commands, you are expected to provide the fully qualified path to your server object. For example, if you have a file `server.py` that contains a FastMCP server named `my_server`, you would provide `path/to/server.py:my_server`.
292
 
293
- If your server variable has one of the standard names (`mcp`, `server`, or `app`), you can omit the server name from the path and just provide the file: `path/to/server.py`.
294
 
295
- ### Development
296
 
297
- Test and debug your server with the MCP Inspector:
298
  ```bash
299
- # Provide the fully qualified path to your server
300
- fastmcp dev server.py:my_mcp_server
301
-
302
- # Or just the file if your server is named 'mcp', 'server', or 'app'
303
  fastmcp dev server.py
304
  ```
305
 
306
- Your server is run in an isolated environment, so you'll need to indicate any dependencies with the `--with` flag. FastMCP is automatically included. If you are working on a uv project, you can use the `--with-editable` flag to mount your current directory:
 
 
 
 
307
 
308
- ```bash
309
- # With additional packages
310
- fastmcp dev server.py --with pandas --with numpy
 
 
 
 
 
 
311
 
312
- # Using your project's dependencies and up-to-date code
313
- fastmcp dev server.py --with-editable .
314
- ```
315
 
316
- #### Environment Variables
317
 
318
- The MCP Inspector runs servers in an isolated environment. Environment variables must be set through the Inspector UI and are not inherited from your system. The Inspector does not currently support setting environment variables via command line (see [Issue #94](https://github.com/modelcontextprotocol/inspector/issues/94)).
319
-
320
- ### Claude Desktop
321
-
322
- Install your server in Claude Desktop:
323
  ```bash
324
- # Basic usage (name is taken from your FastMCP instance)
325
  fastmcp install server.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
- # With a custom name
328
- fastmcp install server.py --name "My Server"
329
 
330
- # With dependencies
331
- fastmcp install server.py --with pandas --with numpy
 
 
332
  ```
333
 
334
- The server name in Claude will be:
335
- 1. The `--name` parameter if provided
336
- 2. The `name` from your FastMCP instance
337
- 3. The filename if the server can't be imported
338
 
339
- #### Environment Variables
 
 
 
340
 
341
- Claude Desktop runs servers in an isolated environment. Environment variables from your system are NOT automatically available to the server - you must explicitly provide them during installation:
342
 
343
- ```bash
344
- # Single env var
345
- fastmcp install server.py -e API_KEY=abc123
346
 
347
- # Multiple env vars
348
- fastmcp install server.py -e API_KEY=abc123 -e OTHER_VAR=value
 
 
349
 
350
- # Load from .env file
351
- fastmcp install server.py -f .env
352
- ```
353
 
354
- Environment variables persist across reinstalls and are only updated when new values are provided:
355
 
356
  ```bash
357
- # First install
358
- fastmcp install server.py -e FOO=bar -e BAZ=123
359
-
360
- # Second install - FOO and BAZ are preserved
361
- fastmcp install server.py -e NEW=value
362
 
363
- # Third install - FOO gets new value, others preserved
364
- fastmcp install server.py -e FOO=newvalue
365
  ```
366
 
367
  ## Examples
@@ -437,11 +461,11 @@ What insights can you provide about the structure and relationships?"""
437
 
438
  <summary><h3>Open Developer Guide</h3></summary>
439
 
440
- #### Prerequisites
441
 
442
  FastMCP requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
443
 
444
- #### Installation
445
 
446
  Create a fork of this repository, then clone it:
447
 
@@ -460,7 +484,7 @@ uv sync --frozen --all-extras --dev
460
 
461
 
462
 
463
- #### Testing
464
 
465
  Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns.
466
 
@@ -471,7 +495,7 @@ Run tests from the root directory:
471
  pytest -vv
472
  ```
473
 
474
- #### Formatting
475
 
476
  FastMCP enforces a variety of required formats, which you can automatically enforce with pre-commit.
477
 
@@ -487,7 +511,7 @@ The hooks will now run on every commit (as well as on every PR). To run them man
487
  pre-commit run --all-files
488
  ```
489
 
490
- #### Opening a Pull Request
491
 
492
  Fork the repository and create a new branch:
493
 
 
62
  - [Prompts](#prompts)
63
  - [Images](#images)
64
  - [Context](#context)
65
+ - [Running Your Server](#running-your-server)
66
+ - [Development Mode (Recommended for Building \& Testing)](#development-mode-recommended-for-building--testing)
67
+ - [Claude Desktop Integration (For Regular Use)](#claude-desktop-integration-for-regular-use)
68
+ - [Direct Execution (For Advanced Use Cases)](#direct-execution-for-advanced-use-cases)
69
+ - [Server Object Names](#server-object-names)
70
  - [Examples](#examples)
71
  - [Echo Server](#echo-server)
72
  - [SQLite Explorer](#sqlite-explorer)
73
  - [Contributing](#contributing)
74
+ - [Prerequisites](#prerequisites)
75
+ - [Installation](#installation-1)
76
+ - [Testing](#testing)
77
+ - [Formatting](#formatting)
78
+ - [Opening a Pull Request](#opening-a-pull-request)
79
 
80
  ## Installation
81
 
 
159
 
160
  # Configure host/port for HTTP transport (optional)
161
  mcp = FastMCP("My App", host="localhost", port=8000)
162
+
163
+ # Specify dependencies for deployment and development
164
+ mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
165
  ```
 
166
 
167
  ### Resources
168
 
 
291
  - Resource access through `read_resource()`
292
  - Request metadata via `request_id` and `client_id`
293
 
294
+ ## Running Your Server
 
 
295
 
296
+ There are three main ways to use your FastMCP server, each suited for different stages of development:
297
 
298
+ ### Development Mode (Recommended for Building & Testing)
299
 
300
+ The fastest way to test and debug your server is with the MCP Inspector:
301
 
 
302
  ```bash
 
 
 
 
303
  fastmcp dev server.py
304
  ```
305
 
306
+ This launches a web interface where you can:
307
+ - Test your tools and resources interactively
308
+ - See detailed logs and error messages
309
+ - Monitor server performance
310
+ - Set environment variables for testing
311
 
312
+ During development, you can:
313
+ - Add dependencies with `--with`:
314
+ ```bash
315
+ fastmcp dev server.py --with pandas --with numpy
316
+ ```
317
+ - Mount your local code for live updates:
318
+ ```bash
319
+ fastmcp dev server.py --with-editable .
320
+ ```
321
 
322
+ ### Claude Desktop Integration (For Regular Use)
 
 
323
 
324
+ Once your server is ready, install it in Claude Desktop to use it with Claude:
325
 
 
 
 
 
 
326
  ```bash
 
327
  fastmcp install server.py
328
+ ```
329
+
330
+ Your server will run in an isolated environment with:
331
+ - Automatic installation of dependencies specified in your FastMCP instance:
332
+ ```python
333
+ mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
334
+ ```
335
+ - Custom naming via `--name`:
336
+ ```bash
337
+ fastmcp install server.py --name "My Analytics Server"
338
+ ```
339
+ - Environment variable management:
340
+ ```bash
341
+ # Set variables individually
342
+ fastmcp install server.py -e API_KEY=abc123 -e DB_URL=postgres://...
343
+
344
+ # Or load from a .env file
345
+ fastmcp install server.py -f .env
346
+ ```
347
+
348
+ ### Direct Execution (For Advanced Use Cases)
349
+
350
+ For advanced scenarios like custom deployments or running without Claude, you can execute your server directly:
351
 
352
+ ```python
353
+ from fastmcp import FastMCP
354
 
355
+ mcp = FastMCP("My App")
356
+
357
+ if __name__ == "__main__":
358
+ mcp.run()
359
  ```
360
 
361
+ Run it with:
362
+ ```bash
363
+ # Using the FastMCP CLI
364
+ fastmcp run server.py
365
 
366
+ # Or with Python/uv directly
367
+ python server.py
368
+ uv run python server.py
369
+ ```
370
 
 
371
 
372
+ Note: When running directly, you are responsible for ensuring all dependencies are available in your environment. Any dependencies specified on the FastMCP instance are ignored.
 
 
373
 
374
+ Choose this method when you need:
375
+ - Custom deployment configurations
376
+ - Integration with other services
377
+ - Direct control over the server lifecycle
378
 
379
+ ### Server Object Names
 
 
380
 
381
+ All FastMCP commands will look for a server object called `mcp`, `app`, or `server` in your file. If you have a different object name or multiple servers in one file, use the syntax `server.py:my_server`:
382
 
383
  ```bash
384
+ # Using a standard name
385
+ fastmcp run server.py
 
 
 
386
 
387
+ # Using a custom name
388
+ fastmcp run server.py:my_custom_server
389
  ```
390
 
391
  ## Examples
 
461
 
462
  <summary><h3>Open Developer Guide</h3></summary>
463
 
464
+ ### Prerequisites
465
 
466
  FastMCP requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
467
 
468
+ ### Installation
469
 
470
  Create a fork of this repository, then clone it:
471
 
 
484
 
485
 
486
 
487
+ ### Testing
488
 
489
  Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns.
490
 
 
495
  pytest -vv
496
  ```
497
 
498
+ ### Formatting
499
 
500
  FastMCP enforces a variety of required formats, which you can automatically enforce with pre-commit.
501
 
 
511
  pre-commit run --all-files
512
  ```
513
 
514
+ ### Opening a Pull Request
515
 
516
  Fork the repository and create a new branch:
517
 
examples/screenshot.py CHANGED
@@ -1,7 +1,3 @@
1
- # /// script
2
- # dependencies = ["fastmcp", "pyautogui", "Pillow"]
3
- # ///
4
-
5
  """
6
  FastMCP Screenshot Example
7
 
@@ -9,20 +5,24 @@ Give Claude a tool to capture and view screenshots.
9
  """
10
 
11
  import io
12
-
13
  from fastmcp import FastMCP, Image
14
 
 
15
  # Create server
16
- mcp = FastMCP("Screenshot Demo")
17
 
18
 
19
  @mcp.tool()
20
  def take_screenshot() -> Image:
21
- """Take a screenshot of the user's screen and return it as an image"""
 
 
 
22
  import pyautogui
23
 
24
- screenshot = pyautogui.screenshot()
25
  buffer = io.BytesIO()
 
26
  # if the file exceeds ~1MB, it will be rejected by Claude
 
27
  screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True)
28
  return Image(data=buffer.getvalue(), format="jpeg")
 
 
 
 
 
1
  """
2
  FastMCP Screenshot Example
3
 
 
5
  """
6
 
7
  import io
 
8
  from fastmcp import FastMCP, Image
9
 
10
+
11
  # Create server
12
+ mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"])
13
 
14
 
15
  @mcp.tool()
16
  def take_screenshot() -> Image:
17
+ """
18
+ Take a screenshot of the user's screen and return it as an image. Use
19
+ this tool anytime the user wants you to look at something they're doing.
20
+ """
21
  import pyautogui
22
 
 
23
  buffer = io.BytesIO()
24
+
25
  # if the file exceeds ~1MB, it will be rejected by Claude
26
+ screenshot = pyautogui.screenshot()
27
  screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True)
28
  return Image(data=buffer.getvalue(), format="jpeg")
src/fastmcp/cli/claude.py CHANGED
@@ -68,16 +68,20 @@ def update_claude_config(
68
  env_vars = existing_env
69
 
70
  # Build uv run command
71
- args = ["run", "--with", "fastmcp"]
 
 
 
 
 
 
 
 
 
72
 
73
  if with_editable:
74
  args.extend(["--with-editable", str(with_editable)])
75
 
76
- if with_packages:
77
- for pkg in with_packages:
78
- if pkg:
79
- args.extend(["--with", pkg])
80
-
81
  # Convert file path to absolute before adding to command
82
  # Split off any :object suffix first
83
  if ":" in file_spec:
 
68
  env_vars = existing_env
69
 
70
  # Build uv run command
71
+ args = ["run"]
72
+
73
+ # Collect all packages in a set to deduplicate
74
+ packages = {"fastmcp"}
75
+ if with_packages:
76
+ packages.update(pkg for pkg in with_packages if pkg)
77
+
78
+ # Add all packages with --with
79
+ for pkg in sorted(packages):
80
+ args.extend(["--with", pkg])
81
 
82
  if with_editable:
83
  args.extend(["--with-editable", str(with_editable)])
84
 
 
 
 
 
 
85
  # Convert file path to absolute before adding to command
86
  # Split off any :object suffix first
87
  if ":" in file_spec:
src/fastmcp/cli/cli.py CHANGED
@@ -193,6 +193,11 @@ def dev(
193
  )
194
 
195
  try:
 
 
 
 
 
196
  uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
197
  # Run the MCP Inspector command
198
  process = subprocess.run(
@@ -232,23 +237,16 @@ def run(
232
  help="Transport protocol to use (stdio or sse)",
233
  ),
234
  ] = None,
235
- with_editable: Annotated[
236
- Optional[Path],
237
- typer.Option(
238
- "--with-editable",
239
- "-e",
240
- help="Directory containing pyproject.toml to install in editable mode",
241
- exists=True,
242
- file_okay=False,
243
- resolve_path=True,
244
- ),
245
- ] = None,
246
  ) -> None:
247
  """Run a FastMCP server.
248
 
249
  The server can be specified in two ways:
250
  1. Module approach: server.py - runs the module directly, expecting a server.run() call
251
  2. Import approach: server.py:app - imports and runs the specified server object
 
 
 
 
252
  """
253
  file, server_object = _parse_file_path(file_spec)
254
 
@@ -258,7 +256,6 @@ def run(
258
  "file": str(file),
259
  "server_object": server_object,
260
  "transport": transport,
261
- "with_editable": str(with_editable) if with_editable else None,
262
  },
263
  )
264
 
@@ -361,6 +358,7 @@ def install(
361
 
362
  # Try to import server to get its name, but fall back to file name if dependencies missing
363
  name = server_name
 
364
  if not name:
365
  try:
366
  server = _import_server(file, server_object)
@@ -372,6 +370,11 @@ def install(
372
  )
373
  name = file.stem
374
 
 
 
 
 
 
375
  # Process environment variables if provided
376
  env_dict: Optional[Dict[str, str]] = None
377
  if env_file or env_vars:
 
193
  )
194
 
195
  try:
196
+ # Import server to get dependencies
197
+ server = _import_server(file, server_object)
198
+ if hasattr(server, "dependencies"):
199
+ with_packages = list(set(with_packages + server.dependencies))
200
+
201
  uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
202
  # Run the MCP Inspector command
203
  process = subprocess.run(
 
237
  help="Transport protocol to use (stdio or sse)",
238
  ),
239
  ] = None,
 
 
 
 
 
 
 
 
 
 
 
240
  ) -> None:
241
  """Run a FastMCP server.
242
 
243
  The server can be specified in two ways:
244
  1. Module approach: server.py - runs the module directly, expecting a server.run() call
245
  2. Import approach: server.py:app - imports and runs the specified server object
246
+
247
+ Note: This command runs the server directly. You are responsible for ensuring
248
+ all dependencies are available. For dependency management, use fastmcp install
249
+ or fastmcp dev instead.
250
  """
251
  file, server_object = _parse_file_path(file_spec)
252
 
 
256
  "file": str(file),
257
  "server_object": server_object,
258
  "transport": transport,
 
259
  },
260
  )
261
 
 
358
 
359
  # Try to import server to get its name, but fall back to file name if dependencies missing
360
  name = server_name
361
+ server = None
362
  if not name:
363
  try:
364
  server = _import_server(file, server_object)
 
370
  )
371
  name = file.stem
372
 
373
+ # Get server dependencies if available
374
+ server_dependencies = getattr(server, "dependencies", []) if server else []
375
+ if server_dependencies:
376
+ with_packages = list(set(with_packages + server_dependencies))
377
+
378
  # Process environment variables if provided
379
  env_dict: Optional[Dict[str, str]] = None
380
  if env_file or env_vars:
src/fastmcp/server.py CHANGED
@@ -9,6 +9,7 @@ from itertools import chain
9
  from typing import Any, Callable, Dict, Literal, Sequence
10
 
11
  import pydantic_core
 
12
  import uvicorn
13
  from mcp.server import Server as MCPServer
14
  from mcp.server.sse import SseServerTransport
@@ -76,6 +77,11 @@ class Settings(BaseSettings):
76
  # prompt settings
77
  warn_on_duplicate_prompts: bool = True
78
 
 
 
 
 
 
79
 
80
  class FastMCP:
81
  def __init__(self, name: str | None = None, **settings: Any):
@@ -90,6 +96,7 @@ class FastMCP:
90
  self._prompt_manager = PromptManager(
91
  warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
92
  )
 
93
 
94
  # Set up MCP protocol handlers
95
  self._setup_handlers()
 
9
  from typing import Any, Callable, Dict, Literal, Sequence
10
 
11
  import pydantic_core
12
+ from pydantic import Field
13
  import uvicorn
14
  from mcp.server import Server as MCPServer
15
  from mcp.server.sse import SseServerTransport
 
77
  # prompt settings
78
  warn_on_duplicate_prompts: bool = True
79
 
80
+ dependencies: list[str] = Field(
81
+ default_factory=list,
82
+ description="List of dependencies to install in the server environment",
83
+ )
84
+
85
 
86
  class FastMCP:
87
  def __init__(self, name: str | None = None, **settings: Any):
 
96
  self._prompt_manager = PromptManager(
97
  warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
98
  )
99
+ self.dependencies = self.settings.dependencies
100
 
101
  # Set up MCP protocol handlers
102
  self._setup_handlers()
tests/test_cli.py CHANGED
@@ -1,7 +1,7 @@
1
  """Tests for the FastMCP CLI."""
2
 
3
  import json
4
- from unittest.mock import Mock, patch
5
 
6
  import pytest
7
  from typer.testing import CliRunner
@@ -19,11 +19,13 @@ def mock_config(tmp_path):
19
 
20
 
21
  @pytest.fixture
22
- def mock_server_file(tmp_path):
23
- """Create a mock server file."""
24
  server_file = tmp_path / "server.py"
25
  server_file.write_text(
26
- "from fastmcp import Server\n" "server = Server(name='test')\n"
 
 
27
  )
28
  return server_file
29
 
@@ -67,22 +69,16 @@ def test_parse_env_var():
67
  ),
68
  ],
69
  )
70
- def test_install_with_env_vars(mock_config, mock_server_file, args, expected_env):
71
  """Test installing with environment variables."""
72
  runner = CliRunner()
73
 
74
- with (
75
- patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
76
- patch("fastmcp.cli.cli._import_server") as mock_import,
77
- ):
78
  mock_config_path.return_value = mock_config.parent
79
- mock_server = Mock()
80
- mock_server.name = "test" # Set name as an attribute
81
- mock_import.return_value = mock_server
82
 
83
  result = runner.invoke(
84
  app,
85
- ["install", str(mock_server_file)] + args,
86
  )
87
 
88
  assert result.exit_code == 0
@@ -95,22 +91,16 @@ def test_install_with_env_vars(mock_config, mock_server_file, args, expected_env
95
  assert server["env"] == expected_env
96
 
97
 
98
- def test_install_with_env_file(mock_config, mock_server_file, mock_env_file):
99
  """Test installing with environment variables from a file."""
100
  runner = CliRunner()
101
 
102
- with (
103
- patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
104
- patch("fastmcp.cli.cli._import_server") as mock_import,
105
- ):
106
  mock_config_path.return_value = mock_config.parent
107
- mock_server = Mock()
108
- mock_server.name = "test" # Set name as an attribute
109
- mock_import.return_value = mock_server
110
 
111
  result = runner.invoke(
112
  app,
113
- ["install", str(mock_server_file), "--env-file", str(mock_env_file)],
114
  )
115
 
116
  assert result.exit_code == 0
@@ -123,7 +113,7 @@ def test_install_with_env_file(mock_config, mock_server_file, mock_env_file):
123
  assert server["env"] == {"FOO": "bar", "BAZ": "123"}
124
 
125
 
126
- def test_install_preserves_existing_env_vars(mock_config, mock_server_file):
127
  """Test that installing preserves existing environment variables."""
128
  # Set up initial config with env vars
129
  config = {
@@ -136,7 +126,7 @@ def test_install_preserves_existing_env_vars(mock_config, mock_server_file):
136
  "fastmcp",
137
  "fastmcp",
138
  "run",
139
- str(mock_server_file),
140
  ],
141
  "env": {"FOO": "bar", "BAZ": "123"},
142
  }
@@ -146,19 +136,13 @@ def test_install_preserves_existing_env_vars(mock_config, mock_server_file):
146
 
147
  runner = CliRunner()
148
 
149
- with (
150
- patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
151
- patch("fastmcp.cli.cli._import_server") as mock_import,
152
- ):
153
  mock_config_path.return_value = mock_config.parent
154
- mock_server = Mock()
155
- mock_server.name = "test" # Set name as an attribute
156
- mock_import.return_value = mock_server
157
 
158
  # Install with a new env var
159
  result = runner.invoke(
160
  app,
161
- ["install", str(mock_server_file), "--env-var", "NEW=value"],
162
  )
163
 
164
  assert result.exit_code == 0
@@ -169,7 +153,7 @@ def test_install_preserves_existing_env_vars(mock_config, mock_server_file):
169
  assert server["env"] == {"FOO": "bar", "BAZ": "123", "NEW": "value"}
170
 
171
 
172
- def test_install_updates_existing_env_vars(mock_config, mock_server_file):
173
  """Test that installing updates existing environment variables."""
174
  # Set up initial config with env vars
175
  config = {
@@ -182,7 +166,7 @@ def test_install_updates_existing_env_vars(mock_config, mock_server_file):
182
  "fastmcp",
183
  "fastmcp",
184
  "run",
185
- str(mock_server_file),
186
  ],
187
  "env": {"FOO": "bar", "BAZ": "123"},
188
  }
@@ -192,19 +176,13 @@ def test_install_updates_existing_env_vars(mock_config, mock_server_file):
192
 
193
  runner = CliRunner()
194
 
195
- with (
196
- patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path,
197
- patch("fastmcp.cli.cli._import_server") as mock_import,
198
- ):
199
  mock_config_path.return_value = mock_config.parent
200
- mock_server = Mock()
201
- mock_server.name = "test" # Set name as an attribute
202
- mock_import.return_value = mock_server
203
 
204
  # Update an existing env var
205
  result = runner.invoke(
206
  app,
207
- ["install", str(mock_server_file), "--env-var", "FOO=newvalue"],
208
  )
209
 
210
  assert result.exit_code == 0
@@ -213,3 +191,101 @@ def test_install_updates_existing_env_vars(mock_config, mock_server_file):
213
  config = json.loads(mock_config.read_text())
214
  server = next(iter(config["mcpServers"].values()))
215
  assert server["env"] == {"FOO": "newvalue", "BAZ": "123"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Tests for the FastMCP CLI."""
2
 
3
  import json
4
+ from unittest.mock import patch
5
 
6
  import pytest
7
  from typer.testing import CliRunner
 
19
 
20
 
21
  @pytest.fixture
22
+ def server_file(tmp_path):
23
+ """Create a server file."""
24
  server_file = tmp_path / "server.py"
25
  server_file.write_text(
26
+ """from fastmcp import FastMCP
27
+ mcp = FastMCP("test")
28
+ """
29
  )
30
  return server_file
31
 
 
69
  ),
70
  ],
71
  )
72
+ def test_install_with_env_vars(mock_config, server_file, args, expected_env):
73
  """Test installing with environment variables."""
74
  runner = CliRunner()
75
 
76
+ with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
 
 
 
77
  mock_config_path.return_value = mock_config.parent
 
 
 
78
 
79
  result = runner.invoke(
80
  app,
81
+ ["install", str(server_file)] + args,
82
  )
83
 
84
  assert result.exit_code == 0
 
91
  assert server["env"] == expected_env
92
 
93
 
94
+ def test_install_with_env_file(mock_config, server_file, mock_env_file):
95
  """Test installing with environment variables from a file."""
96
  runner = CliRunner()
97
 
98
+ with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
 
 
 
99
  mock_config_path.return_value = mock_config.parent
 
 
 
100
 
101
  result = runner.invoke(
102
  app,
103
+ ["install", str(server_file), "--env-file", str(mock_env_file)],
104
  )
105
 
106
  assert result.exit_code == 0
 
113
  assert server["env"] == {"FOO": "bar", "BAZ": "123"}
114
 
115
 
116
+ def test_install_preserves_existing_env_vars(mock_config, server_file):
117
  """Test that installing preserves existing environment variables."""
118
  # Set up initial config with env vars
119
  config = {
 
126
  "fastmcp",
127
  "fastmcp",
128
  "run",
129
+ str(server_file),
130
  ],
131
  "env": {"FOO": "bar", "BAZ": "123"},
132
  }
 
136
 
137
  runner = CliRunner()
138
 
139
+ with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
 
 
 
140
  mock_config_path.return_value = mock_config.parent
 
 
 
141
 
142
  # Install with a new env var
143
  result = runner.invoke(
144
  app,
145
+ ["install", str(server_file), "--env-var", "NEW=value"],
146
  )
147
 
148
  assert result.exit_code == 0
 
153
  assert server["env"] == {"FOO": "bar", "BAZ": "123", "NEW": "value"}
154
 
155
 
156
+ def test_install_updates_existing_env_vars(mock_config, server_file):
157
  """Test that installing updates existing environment variables."""
158
  # Set up initial config with env vars
159
  config = {
 
166
  "fastmcp",
167
  "fastmcp",
168
  "run",
169
+ str(server_file),
170
  ],
171
  "env": {"FOO": "bar", "BAZ": "123"},
172
  }
 
176
 
177
  runner = CliRunner()
178
 
179
+ with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
 
 
 
180
  mock_config_path.return_value = mock_config.parent
 
 
 
181
 
182
  # Update an existing env var
183
  result = runner.invoke(
184
  app,
185
+ ["install", str(server_file), "--env-var", "FOO=newvalue"],
186
  )
187
 
188
  assert result.exit_code == 0
 
191
  config = json.loads(mock_config.read_text())
192
  server = next(iter(config["mcpServers"].values()))
193
  assert server["env"] == {"FOO": "newvalue", "BAZ": "123"}
194
+
195
+
196
+ def test_server_dependencies(mock_config, server_file):
197
+ """Test that server dependencies are correctly handled."""
198
+ # Create a server file with dependencies
199
+ server_file = server_file.parent / "server_with_deps.py"
200
+ server_file.write_text(
201
+ """from fastmcp import FastMCP
202
+ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
203
+ """
204
+ )
205
+
206
+ runner = CliRunner()
207
+
208
+ with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
209
+ mock_config_path.return_value = mock_config.parent
210
+
211
+ result = runner.invoke(app, ["install", str(server_file)])
212
+
213
+ assert result.exit_code == 0
214
+
215
+ # Read the config file and check dependencies were added as --with args
216
+ config = json.loads(mock_config.read_text())
217
+ server = next(iter(config["mcpServers"].values()))
218
+ assert "--with" in server["args"]
219
+ assert "pandas" in server["args"]
220
+ assert "numpy" in server["args"]
221
+
222
+
223
+ def test_server_dependencies_empty(mock_config, server_file):
224
+ """Test that server with no dependencies works correctly."""
225
+ runner = CliRunner()
226
+
227
+ with patch("fastmcp.cli.claude.get_claude_config_path") as mock_config_path:
228
+ mock_config_path.return_value = mock_config.parent
229
+
230
+ result = runner.invoke(app, ["install", str(server_file)])
231
+
232
+ assert result.exit_code == 0
233
+
234
+ # Read the config file and check only fastmcp is in --with args
235
+ config = json.loads(mock_config.read_text())
236
+ server = next(iter(config["mcpServers"].values()))
237
+ assert server["args"].count("--with") == 1
238
+ assert "fastmcp" in server["args"]
239
+
240
+
241
+ def test_dev_with_dependencies(mock_config, server_file):
242
+ """Test that dev command handles dependencies correctly."""
243
+ # Create a server file with dependencies
244
+ server_file = server_file.parent / "server_with_deps.py"
245
+ server_file.write_text(
246
+ """from fastmcp import FastMCP
247
+ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
248
+ """
249
+ )
250
+
251
+ runner = CliRunner()
252
+
253
+ with patch("subprocess.run") as mock_run:
254
+ mock_run.return_value.returncode = 0 # Set successful return code
255
+ result = runner.invoke(app, ["dev", str(server_file)])
256
+ assert result.exit_code == 0
257
+
258
+ # Check that dependencies were passed to subprocess.run
259
+ mock_run.assert_called_once()
260
+ args = mock_run.call_args[0][0]
261
+ assert "npx" in args
262
+ assert "@modelcontextprotocol/inspector" in args
263
+ assert "uv" in args
264
+ assert "run" in args
265
+ assert "--with" in args
266
+ assert "pandas" in args
267
+ assert "numpy" in args
268
+ assert "fastmcp" in args
269
+
270
+
271
+ def test_run_with_dependencies(mock_config, server_file):
272
+ """Test that run command does not handle dependencies."""
273
+ # Create a server file with dependencies
274
+ server_file = server_file.parent / "server_with_deps.py"
275
+ server_file.write_text(
276
+ """from fastmcp import FastMCP
277
+ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
278
+
279
+ if __name__ == "__main__":
280
+ mcp.run()
281
+ """
282
+ )
283
+
284
+ runner = CliRunner()
285
+
286
+ with patch("subprocess.run") as mock_run:
287
+ result = runner.invoke(app, ["run", str(server_file)])
288
+ assert result.exit_code == 0
289
+
290
+ # Run command should not call subprocess.run
291
+ mock_run.assert_not_called()