fastmcp 0.3.1__tar.gz → 0.3.2__tar.gz
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-0.3.1 → fastmcp-0.3.2}/PKG-INFO +51 -7
- {fastmcp-0.3.1 → fastmcp-0.3.2}/README.md +49 -6
- {fastmcp-0.3.1 → fastmcp-0.3.2}/pyproject.toml +1 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/cli/claude.py +23 -17
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/cli/cli.py +53 -9
- fastmcp-0.3.2/tests/test_cli.py +215 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/uv.lock +3 -1
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.github/ai-labeler.yml +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.github/release.yml +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.github/workflows/ai-labeler.yml +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.github/workflows/lint.yml +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.github/workflows/publish.yml +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.github/workflows/run-tests.yml +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.gitignore +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.pre-commit-config.yaml +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/.python-version +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/LICENSE +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/docs/assets/demo-inspector.png +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/examples/desktop.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/examples/echo.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/examples/readme-quickstart.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/examples/screenshot.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/examples/simple_echo.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/cli/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/exceptions.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/prompts/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/prompts/base.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/prompts/manager.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/prompts/prompt_manager.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/resources/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/resources/base.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/resources/resource_manager.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/resources/templates.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/resources/types.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/server.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/tools/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/tools/base.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/tools/tool_manager.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/utilities/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/utilities/logging.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/src/fastmcp/utilities/types.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/prompts/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/prompts/test_base.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/prompts/test_manager.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/resources/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/resources/test_file_resources.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/resources/test_function_resources.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/resources/test_resource_manager.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/resources/test_resource_template.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/resources/test_resources.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/servers/__init__.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/servers/test_file_server.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/test_server.py +0 -0
- {fastmcp-0.3.1 → fastmcp-0.3.2}/tests/test_tool_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A more ergonomic interface for MCP servers
|
|
5
5
|
Author: Jeremiah Lowin
|
|
6
6
|
License: Apache-2.0
|
|
@@ -9,6 +9,7 @@ Requires-Dist: httpx>=0.26.0
|
|
|
9
9
|
Requires-Dist: mcp<2.0.0,>=1.0.0
|
|
10
10
|
Requires-Dist: pydantic-settings>=2.6.1
|
|
11
11
|
Requires-Dist: pydantic<3.0.0,>=2.5.3
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
12
13
|
Requires-Dist: typer>=0.9.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: copychat>=0.5.2; extra == 'dev'
|
|
@@ -30,24 +31,34 @@ Description-Content-Type: text/markdown
|
|
|
30
31
|
[](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml)
|
|
31
32
|
[](https://github.com/jlowin/fastmcp/blob/main/LICENSE)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
The fast, Pythonic way to build MCP servers
|
|
34
35
|
|
|
35
36
|
</div>
|
|
36
37
|
|
|
37
|
-
FastMCP makes building MCP servers simple and intuitive. Create tools, expose resources, and define prompts with clean, Pythonic code:
|
|
38
|
+
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers are a new, standardized way to provide context and tools to your LLMs, and FastMCP makes building MCP servers simple and intuitive. Create tools, expose resources, and define prompts with clean, Pythonic code:
|
|
38
39
|
|
|
39
40
|
```python
|
|
41
|
+
# demo.py
|
|
42
|
+
|
|
40
43
|
from fastmcp import FastMCP
|
|
41
44
|
|
|
45
|
+
|
|
42
46
|
mcp = FastMCP("Demo 🚀")
|
|
43
47
|
|
|
48
|
+
|
|
44
49
|
@mcp.tool()
|
|
45
50
|
def add(a: int, b: int) -> int:
|
|
46
51
|
"""Add two numbers"""
|
|
47
52
|
return a + b
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
That's it!
|
|
55
|
+
That's it! Give Claude access to the server by running:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
fastmcp install demo.py
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
FastMCP handles all the complex protocol details and server management, so you can focus on building great tools. It's designed to be high-level and Pythonic - in most cases, decorating a function is all you need.
|
|
51
62
|
|
|
52
63
|
|
|
53
64
|
### Key features:
|
|
@@ -76,7 +87,9 @@ That's it! FastMCP handles all the complex protocol details and server managemen
|
|
|
76
87
|
- [Context](#context)
|
|
77
88
|
- [Deployment](#deployment)
|
|
78
89
|
- [Development](#development)
|
|
90
|
+
- [Environment Variables](#environment-variables)
|
|
79
91
|
- [Claude Desktop](#claude-desktop)
|
|
92
|
+
- [Environment Variables](#environment-variables-1)
|
|
80
93
|
- [Examples](#examples)
|
|
81
94
|
- [Echo Server](#echo-server)
|
|
82
95
|
- [SQLite Explorer](#sqlite-explorer)
|
|
@@ -319,6 +332,10 @@ fastmcp dev server.py --with pandas --with numpy
|
|
|
319
332
|
fastmcp dev server.py --with-editable .
|
|
320
333
|
```
|
|
321
334
|
|
|
335
|
+
#### Environment Variables
|
|
336
|
+
|
|
337
|
+
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)).
|
|
338
|
+
|
|
322
339
|
### Claude Desktop
|
|
323
340
|
|
|
324
341
|
Install your server in Claude Desktop:
|
|
@@ -331,9 +348,6 @@ fastmcp install server.py --name "My Server"
|
|
|
331
348
|
|
|
332
349
|
# With dependencies
|
|
333
350
|
fastmcp install server.py --with pandas --with numpy
|
|
334
|
-
|
|
335
|
-
# Replace an existing server
|
|
336
|
-
fastmcp install server.py --force
|
|
337
351
|
```
|
|
338
352
|
|
|
339
353
|
The server name in Claude will be:
|
|
@@ -341,8 +355,38 @@ The server name in Claude will be:
|
|
|
341
355
|
2. The `name` from your FastMCP instance
|
|
342
356
|
3. The filename if the server can't be imported
|
|
343
357
|
|
|
358
|
+
#### Environment Variables
|
|
359
|
+
|
|
360
|
+
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:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# Single env var
|
|
364
|
+
fastmcp install server.py -e API_KEY=abc123
|
|
365
|
+
|
|
366
|
+
# Multiple env vars
|
|
367
|
+
fastmcp install server.py -e API_KEY=abc123 -e OTHER_VAR=value
|
|
368
|
+
|
|
369
|
+
# Load from .env file
|
|
370
|
+
fastmcp install server.py -f .env
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Environment variables persist across reinstalls and are only updated when new values are provided:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
# First install
|
|
377
|
+
fastmcp install server.py -e FOO=bar -e BAZ=123
|
|
378
|
+
|
|
379
|
+
# Second install - FOO and BAZ are preserved
|
|
380
|
+
fastmcp install server.py -e NEW=value
|
|
381
|
+
|
|
382
|
+
# Third install - FOO gets new value, others preserved
|
|
383
|
+
fastmcp install server.py -e FOO=newvalue
|
|
384
|
+
```
|
|
385
|
+
|
|
344
386
|
## Examples
|
|
345
387
|
|
|
388
|
+
Here are a few examples of FastMCP servers. For more, see the `examples/` directory.
|
|
389
|
+
|
|
346
390
|
### Echo Server
|
|
347
391
|
A simple server demonstrating resources, tools, and prompts:
|
|
348
392
|
|
|
@@ -7,24 +7,34 @@
|
|
|
7
7
|
[](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml)
|
|
8
8
|
[](https://github.com/jlowin/fastmcp/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The fast, Pythonic way to build MCP servers
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
FastMCP makes building MCP servers simple and intuitive. Create tools, expose resources, and define prompts with clean, Pythonic code:
|
|
14
|
+
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers are a new, standardized way to provide context and tools to your LLMs, and FastMCP makes building MCP servers simple and intuitive. Create tools, expose resources, and define prompts with clean, Pythonic code:
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
|
+
# demo.py
|
|
18
|
+
|
|
17
19
|
from fastmcp import FastMCP
|
|
18
20
|
|
|
21
|
+
|
|
19
22
|
mcp = FastMCP("Demo 🚀")
|
|
20
23
|
|
|
24
|
+
|
|
21
25
|
@mcp.tool()
|
|
22
26
|
def add(a: int, b: int) -> int:
|
|
23
27
|
"""Add two numbers"""
|
|
24
28
|
return a + b
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
That's it!
|
|
31
|
+
That's it! Give Claude access to the server by running:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
fastmcp install demo.py
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
FastMCP handles all the complex protocol details and server management, so you can focus on building great tools. It's designed to be high-level and Pythonic - in most cases, decorating a function is all you need.
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
### Key features:
|
|
@@ -53,7 +63,9 @@ That's it! FastMCP handles all the complex protocol details and server managemen
|
|
|
53
63
|
- [Context](#context)
|
|
54
64
|
- [Deployment](#deployment)
|
|
55
65
|
- [Development](#development)
|
|
66
|
+
- [Environment Variables](#environment-variables)
|
|
56
67
|
- [Claude Desktop](#claude-desktop)
|
|
68
|
+
- [Environment Variables](#environment-variables-1)
|
|
57
69
|
- [Examples](#examples)
|
|
58
70
|
- [Echo Server](#echo-server)
|
|
59
71
|
- [SQLite Explorer](#sqlite-explorer)
|
|
@@ -296,6 +308,10 @@ fastmcp dev server.py --with pandas --with numpy
|
|
|
296
308
|
fastmcp dev server.py --with-editable .
|
|
297
309
|
```
|
|
298
310
|
|
|
311
|
+
#### Environment Variables
|
|
312
|
+
|
|
313
|
+
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)).
|
|
314
|
+
|
|
299
315
|
### Claude Desktop
|
|
300
316
|
|
|
301
317
|
Install your server in Claude Desktop:
|
|
@@ -308,9 +324,6 @@ fastmcp install server.py --name "My Server"
|
|
|
308
324
|
|
|
309
325
|
# With dependencies
|
|
310
326
|
fastmcp install server.py --with pandas --with numpy
|
|
311
|
-
|
|
312
|
-
# Replace an existing server
|
|
313
|
-
fastmcp install server.py --force
|
|
314
327
|
```
|
|
315
328
|
|
|
316
329
|
The server name in Claude will be:
|
|
@@ -318,8 +331,38 @@ The server name in Claude will be:
|
|
|
318
331
|
2. The `name` from your FastMCP instance
|
|
319
332
|
3. The filename if the server can't be imported
|
|
320
333
|
|
|
334
|
+
#### Environment Variables
|
|
335
|
+
|
|
336
|
+
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:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
# Single env var
|
|
340
|
+
fastmcp install server.py -e API_KEY=abc123
|
|
341
|
+
|
|
342
|
+
# Multiple env vars
|
|
343
|
+
fastmcp install server.py -e API_KEY=abc123 -e OTHER_VAR=value
|
|
344
|
+
|
|
345
|
+
# Load from .env file
|
|
346
|
+
fastmcp install server.py -f .env
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Environment variables persist across reinstalls and are only updated when new values are provided:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# First install
|
|
353
|
+
fastmcp install server.py -e FOO=bar -e BAZ=123
|
|
354
|
+
|
|
355
|
+
# Second install - FOO and BAZ are preserved
|
|
356
|
+
fastmcp install server.py -e NEW=value
|
|
357
|
+
|
|
358
|
+
# Third install - FOO gets new value, others preserved
|
|
359
|
+
fastmcp install server.py -e FOO=newvalue
|
|
360
|
+
```
|
|
361
|
+
|
|
321
362
|
## Examples
|
|
322
363
|
|
|
364
|
+
Here are a few examples of FastMCP servers. For more, see the `examples/` directory.
|
|
365
|
+
|
|
323
366
|
### Echo Server
|
|
324
367
|
A simple server demonstrating resources, tools, and prompts:
|
|
325
368
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Optional, Dict
|
|
7
7
|
|
|
8
8
|
from ..utilities.logging import get_logger
|
|
9
9
|
|
|
@@ -30,16 +30,17 @@ def update_claude_config(
|
|
|
30
30
|
*,
|
|
31
31
|
with_editable: Optional[Path] = None,
|
|
32
32
|
with_packages: Optional[list[str]] = None,
|
|
33
|
-
|
|
33
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
34
34
|
) -> bool:
|
|
35
|
-
"""Add
|
|
35
|
+
"""Add or update a FastMCP server in Claude's configuration.
|
|
36
36
|
|
|
37
37
|
Args:
|
|
38
38
|
file_spec: Path to the server file, optionally with :object suffix
|
|
39
39
|
server_name: Name for the server in Claude's config
|
|
40
40
|
with_editable: Optional directory to install in editable mode
|
|
41
41
|
with_packages: Optional list of additional packages to install
|
|
42
|
-
|
|
42
|
+
env_vars: Optional dictionary of environment variables. These are merged with
|
|
43
|
+
any existing variables, with new values taking precedence.
|
|
43
44
|
"""
|
|
44
45
|
config_dir = get_claude_config_path()
|
|
45
46
|
if not config_dir:
|
|
@@ -54,18 +55,17 @@ def update_claude_config(
|
|
|
54
55
|
if "mcpServers" not in config:
|
|
55
56
|
config["mcpServers"] = {}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
58
|
+
# Always preserve existing env vars and merge with new ones
|
|
59
|
+
if (
|
|
60
|
+
server_name in config["mcpServers"]
|
|
61
|
+
and "env" in config["mcpServers"][server_name]
|
|
62
|
+
):
|
|
63
|
+
existing_env = config["mcpServers"][server_name]["env"]
|
|
64
|
+
if env_vars:
|
|
65
|
+
# New vars take precedence over existing ones
|
|
66
|
+
env_vars = {**existing_env, **env_vars}
|
|
67
|
+
else:
|
|
68
|
+
env_vars = existing_env
|
|
69
69
|
|
|
70
70
|
# Build uv run command
|
|
71
71
|
args = ["run", "--with", "fastmcp"]
|
|
@@ -89,11 +89,17 @@ def update_claude_config(
|
|
|
89
89
|
# Add fastmcp run command
|
|
90
90
|
args.extend(["fastmcp", "run", file_spec])
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
server_config = {
|
|
93
93
|
"command": "uv",
|
|
94
94
|
"args": args,
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
# Add environment variables if specified
|
|
98
|
+
if env_vars:
|
|
99
|
+
server_config["env"] = env_vars
|
|
100
|
+
|
|
101
|
+
config["mcpServers"][server_name] = server_config
|
|
102
|
+
|
|
97
103
|
config_file.write_text(json.dumps(config, indent=2))
|
|
98
104
|
logger.info(
|
|
99
105
|
f"Added server '{server_name}' to Claude config",
|
|
@@ -5,10 +5,11 @@ import importlib.util
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Optional, Tuple
|
|
8
|
+
from typing import Optional, Tuple, Dict
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
from typing_extensions import Annotated
|
|
12
|
+
import dotenv
|
|
12
13
|
|
|
13
14
|
from ..utilities.logging import get_logger
|
|
14
15
|
from . import claude
|
|
@@ -23,6 +24,17 @@ app = typer.Typer(
|
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _parse_env_var(env_var: str) -> Tuple[str, str]:
|
|
28
|
+
"""Parse environment variable string in format KEY=VALUE."""
|
|
29
|
+
if "=" not in env_var:
|
|
30
|
+
logger.error(
|
|
31
|
+
f"Invalid environment variable format: {env_var}. Must be KEY=VALUE"
|
|
32
|
+
)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
key, value = env_var.split("=", 1)
|
|
35
|
+
return key.strip(), value.strip()
|
|
36
|
+
|
|
37
|
+
|
|
26
38
|
def _build_uv_command(
|
|
27
39
|
file_spec: str,
|
|
28
40
|
with_editable: Optional[Path] = None,
|
|
@@ -304,16 +316,32 @@ def install(
|
|
|
304
316
|
help="Additional packages to install",
|
|
305
317
|
),
|
|
306
318
|
] = [],
|
|
307
|
-
|
|
308
|
-
|
|
319
|
+
env_vars: Annotated[
|
|
320
|
+
list[str],
|
|
309
321
|
typer.Option(
|
|
310
|
-
"--
|
|
322
|
+
"--env-var",
|
|
323
|
+
"-e",
|
|
324
|
+
help="Environment variables in KEY=VALUE format",
|
|
325
|
+
),
|
|
326
|
+
] = [],
|
|
327
|
+
env_file: Annotated[
|
|
328
|
+
Optional[Path],
|
|
329
|
+
typer.Option(
|
|
330
|
+
"--env-file",
|
|
311
331
|
"-f",
|
|
312
|
-
help="
|
|
332
|
+
help="Load environment variables from a .env file",
|
|
333
|
+
exists=True,
|
|
334
|
+
file_okay=True,
|
|
335
|
+
dir_okay=False,
|
|
336
|
+
resolve_path=True,
|
|
313
337
|
),
|
|
314
|
-
] =
|
|
338
|
+
] = None,
|
|
315
339
|
) -> None:
|
|
316
|
-
"""Install a FastMCP server in the Claude desktop app.
|
|
340
|
+
"""Install a FastMCP server in the Claude desktop app.
|
|
341
|
+
|
|
342
|
+
Environment variables are preserved once added and only updated if new values
|
|
343
|
+
are explicitly provided.
|
|
344
|
+
"""
|
|
317
345
|
file, server_object = _parse_file_path(file_spec)
|
|
318
346
|
|
|
319
347
|
logger.debug(
|
|
@@ -324,7 +352,6 @@ def install(
|
|
|
324
352
|
"server_object": server_object,
|
|
325
353
|
"with_editable": str(with_editable) if with_editable else None,
|
|
326
354
|
"with_packages": with_packages,
|
|
327
|
-
"force": force,
|
|
328
355
|
},
|
|
329
356
|
)
|
|
330
357
|
|
|
@@ -345,12 +372,29 @@ def install(
|
|
|
345
372
|
)
|
|
346
373
|
name = file.stem
|
|
347
374
|
|
|
375
|
+
# Process environment variables if provided
|
|
376
|
+
env_dict: Optional[Dict[str, str]] = None
|
|
377
|
+
if env_file or env_vars:
|
|
378
|
+
env_dict = {}
|
|
379
|
+
# Load from .env file if specified
|
|
380
|
+
if env_file:
|
|
381
|
+
try:
|
|
382
|
+
env_dict.update(dotenv.dotenv_values(env_file))
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.error(f"Failed to load .env file: {e}")
|
|
385
|
+
sys.exit(1)
|
|
386
|
+
|
|
387
|
+
# Add command line environment variables
|
|
388
|
+
for env_var in env_vars:
|
|
389
|
+
key, value = _parse_env_var(env_var)
|
|
390
|
+
env_dict[key] = value
|
|
391
|
+
|
|
348
392
|
if claude.update_claude_config(
|
|
349
393
|
file_spec,
|
|
350
394
|
name,
|
|
351
395
|
with_editable=with_editable,
|
|
352
396
|
with_packages=with_packages,
|
|
353
|
-
|
|
397
|
+
env_vars=env_dict,
|
|
354
398
|
):
|
|
355
399
|
logger.info(f"Successfully installed {name} in Claude app")
|
|
356
400
|
else:
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
|
8
|
+
|
|
9
|
+
from fastmcp.cli.cli import app, _parse_env_var
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_config(tmp_path):
|
|
14
|
+
"""Create a mock Claude config file."""
|
|
15
|
+
config = {"mcpServers": {}}
|
|
16
|
+
config_file = tmp_path / "claude_desktop_config.json"
|
|
17
|
+
config_file.write_text(json.dumps(config))
|
|
18
|
+
return config_file
|
|
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
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_env_file(tmp_path):
|
|
33
|
+
"""Create a mock .env file."""
|
|
34
|
+
env_file = tmp_path / ".env"
|
|
35
|
+
env_file.write_text("FOO=bar\nBAZ=123")
|
|
36
|
+
return env_file
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_parse_env_var():
|
|
40
|
+
"""Test parsing environment variables."""
|
|
41
|
+
assert _parse_env_var("FOO=bar") == ("FOO", "bar")
|
|
42
|
+
assert _parse_env_var("FOO=") == ("FOO", "")
|
|
43
|
+
assert _parse_env_var("FOO=bar baz") == ("FOO", "bar baz")
|
|
44
|
+
assert _parse_env_var("FOO = bar ") == ("FOO", "bar")
|
|
45
|
+
|
|
46
|
+
with pytest.raises(SystemExit):
|
|
47
|
+
_parse_env_var("invalid")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize(
|
|
51
|
+
"args,expected_env",
|
|
52
|
+
[
|
|
53
|
+
# Basic env var
|
|
54
|
+
(
|
|
55
|
+
["--env-var", "FOO=bar"],
|
|
56
|
+
{"FOO": "bar"},
|
|
57
|
+
),
|
|
58
|
+
# Multiple env vars
|
|
59
|
+
(
|
|
60
|
+
["--env-var", "FOO=bar", "--env-var", "BAZ=123"],
|
|
61
|
+
{"FOO": "bar", "BAZ": "123"},
|
|
62
|
+
),
|
|
63
|
+
# Env var with spaces
|
|
64
|
+
(
|
|
65
|
+
["--env-var", "FOO=bar baz"],
|
|
66
|
+
{"FOO": "bar baz"},
|
|
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
|
|
89
|
+
|
|
90
|
+
# Read the config file and check env vars
|
|
91
|
+
config = json.loads(mock_config.read_text())
|
|
92
|
+
assert "mcpServers" in config
|
|
93
|
+
assert len(config["mcpServers"]) == 1
|
|
94
|
+
server = next(iter(config["mcpServers"].values()))
|
|
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
|
|
117
|
+
|
|
118
|
+
# Read the config file and check env vars
|
|
119
|
+
config = json.loads(mock_config.read_text())
|
|
120
|
+
assert "mcpServers" in config
|
|
121
|
+
assert len(config["mcpServers"]) == 1
|
|
122
|
+
server = next(iter(config["mcpServers"].values()))
|
|
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 = {
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"test": {
|
|
132
|
+
"command": "uv",
|
|
133
|
+
"args": [
|
|
134
|
+
"run",
|
|
135
|
+
"--with",
|
|
136
|
+
"fastmcp",
|
|
137
|
+
"fastmcp",
|
|
138
|
+
"run",
|
|
139
|
+
str(mock_server_file),
|
|
140
|
+
],
|
|
141
|
+
"env": {"FOO": "bar", "BAZ": "123"},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
mock_config.write_text(json.dumps(config))
|
|
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
|
|
165
|
+
|
|
166
|
+
# Read the config file and check env vars are preserved
|
|
167
|
+
config = json.loads(mock_config.read_text())
|
|
168
|
+
server = next(iter(config["mcpServers"].values()))
|
|
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 = {
|
|
176
|
+
"mcpServers": {
|
|
177
|
+
"test": {
|
|
178
|
+
"command": "uv",
|
|
179
|
+
"args": [
|
|
180
|
+
"run",
|
|
181
|
+
"--with",
|
|
182
|
+
"fastmcp",
|
|
183
|
+
"fastmcp",
|
|
184
|
+
"run",
|
|
185
|
+
str(mock_server_file),
|
|
186
|
+
],
|
|
187
|
+
"env": {"FOO": "bar", "BAZ": "123"},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
mock_config.write_text(json.dumps(config))
|
|
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
|
|
211
|
+
|
|
212
|
+
# Read the config file and check env var was updated
|
|
213
|
+
config = json.loads(mock_config.read_text())
|
|
214
|
+
server = next(iter(config["mcpServers"].values()))
|
|
215
|
+
assert server["env"] == {"FOO": "newvalue", "BAZ": "123"}
|
|
@@ -228,13 +228,14 @@ wheels = [
|
|
|
228
228
|
|
|
229
229
|
[[package]]
|
|
230
230
|
name = "fastmcp"
|
|
231
|
-
version = "0.3.
|
|
231
|
+
version = "0.3.2.dev0+g5656200.d20241201"
|
|
232
232
|
source = { editable = "." }
|
|
233
233
|
dependencies = [
|
|
234
234
|
{ name = "httpx" },
|
|
235
235
|
{ name = "mcp" },
|
|
236
236
|
{ name = "pydantic" },
|
|
237
237
|
{ name = "pydantic-settings" },
|
|
238
|
+
{ name = "python-dotenv" },
|
|
238
239
|
{ name = "typer" },
|
|
239
240
|
]
|
|
240
241
|
|
|
@@ -263,6 +264,7 @@ requires-dist = [
|
|
|
263
264
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.3" },
|
|
264
265
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.5" },
|
|
265
266
|
{ name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6.1" },
|
|
267
|
+
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
|
266
268
|
{ name = "ruff", marker = "extra == 'dev'" },
|
|
267
269
|
{ name = "typer", specifier = ">=0.9.0" },
|
|
268
270
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|