fastmcp 0.3.0__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.
Files changed (56) hide show
  1. {fastmcp-0.3.0 → fastmcp-0.3.2}/PKG-INFO +83 -17
  2. {fastmcp-0.3.0 → fastmcp-0.3.2}/README.md +81 -16
  3. {fastmcp-0.3.0 → fastmcp-0.3.2}/pyproject.toml +1 -0
  4. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/cli/claude.py +23 -17
  5. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/cli/cli.py +57 -13
  6. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/server.py +5 -2
  7. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/utilities/logging.py +6 -5
  8. fastmcp-0.3.2/tests/test_cli.py +215 -0
  9. {fastmcp-0.3.0 → fastmcp-0.3.2}/uv.lock +3 -1
  10. {fastmcp-0.3.0 → fastmcp-0.3.2}/.github/ai-labeler.yml +0 -0
  11. {fastmcp-0.3.0 → fastmcp-0.3.2}/.github/release.yml +0 -0
  12. {fastmcp-0.3.0 → fastmcp-0.3.2}/.github/workflows/ai-labeler.yml +0 -0
  13. {fastmcp-0.3.0 → fastmcp-0.3.2}/.github/workflows/lint.yml +0 -0
  14. {fastmcp-0.3.0 → fastmcp-0.3.2}/.github/workflows/publish.yml +0 -0
  15. {fastmcp-0.3.0 → fastmcp-0.3.2}/.github/workflows/run-tests.yml +0 -0
  16. {fastmcp-0.3.0 → fastmcp-0.3.2}/.gitignore +0 -0
  17. {fastmcp-0.3.0 → fastmcp-0.3.2}/.pre-commit-config.yaml +0 -0
  18. {fastmcp-0.3.0 → fastmcp-0.3.2}/.python-version +0 -0
  19. {fastmcp-0.3.0 → fastmcp-0.3.2}/LICENSE +0 -0
  20. {fastmcp-0.3.0 → fastmcp-0.3.2}/docs/assets/demo-inspector.png +0 -0
  21. {fastmcp-0.3.0 → fastmcp-0.3.2}/examples/desktop.py +0 -0
  22. {fastmcp-0.3.0 → fastmcp-0.3.2}/examples/echo.py +0 -0
  23. {fastmcp-0.3.0 → fastmcp-0.3.2}/examples/readme-quickstart.py +0 -0
  24. {fastmcp-0.3.0 → fastmcp-0.3.2}/examples/screenshot.py +0 -0
  25. {fastmcp-0.3.0 → fastmcp-0.3.2}/examples/simple_echo.py +0 -0
  26. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/__init__.py +0 -0
  27. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/cli/__init__.py +0 -0
  28. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/exceptions.py +0 -0
  29. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/prompts/__init__.py +0 -0
  30. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/prompts/base.py +0 -0
  31. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/prompts/manager.py +0 -0
  32. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/prompts/prompt_manager.py +0 -0
  33. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/resources/__init__.py +0 -0
  34. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/resources/base.py +0 -0
  35. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/resources/resource_manager.py +0 -0
  36. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/resources/templates.py +0 -0
  37. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/resources/types.py +0 -0
  38. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/tools/__init__.py +0 -0
  39. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/tools/base.py +0 -0
  40. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/tools/tool_manager.py +0 -0
  41. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/utilities/__init__.py +0 -0
  42. {fastmcp-0.3.0 → fastmcp-0.3.2}/src/fastmcp/utilities/types.py +0 -0
  43. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/__init__.py +0 -0
  44. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/prompts/__init__.py +0 -0
  45. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/prompts/test_base.py +0 -0
  46. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/prompts/test_manager.py +0 -0
  47. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/resources/__init__.py +0 -0
  48. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/resources/test_file_resources.py +0 -0
  49. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/resources/test_function_resources.py +0 -0
  50. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/resources/test_resource_manager.py +0 -0
  51. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/resources/test_resource_template.py +0 -0
  52. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/resources/test_resources.py +0 -0
  53. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/servers/__init__.py +0 -0
  54. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/servers/test_file_server.py +0 -0
  55. {fastmcp-0.3.0 → fastmcp-0.3.2}/tests/test_server.py +0 -0
  56. {fastmcp-0.3.0 → 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.0
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'
@@ -22,7 +23,7 @@ Requires-Dist: ruff; extra == 'dev'
22
23
  Description-Content-Type: text/markdown
23
24
 
24
25
  <!-- omit in toc -->
25
- # FastMCP
26
+ # FastMCP 🚀
26
27
 
27
28
  <div align="center">
28
29
 
@@ -30,17 +31,46 @@ Description-Content-Type: text/markdown
30
31
  [![Tests](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml)
31
32
  [![License](https://img.shields.io/github/license/jlowin/fastmcp.svg)](https://github.com/jlowin/fastmcp/blob/main/LICENSE)
32
33
 
34
+ The fast, Pythonic way to build MCP servers
35
+
33
36
  </div>
34
37
 
35
- FastMCP is a high-level, intuitive framework for building [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers with Python. While MCP is a powerful protocol that enables LLMs to interact with local data and tools in a secure, standardized way, the specification can be cumbersome to implement directly. FastMCP lets you build fully compliant MCP servers in the most Pythonic way possible - in many cases, simply decorating a function is all that's required.
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:
39
+
40
+ ```python
41
+ # demo.py
42
+
43
+ from fastmcp import FastMCP
44
+
45
+
46
+ mcp = FastMCP("Demo 🚀")
47
+
48
+
49
+ @mcp.tool()
50
+ def add(a: int, b: int) -> int:
51
+ """Add two numbers"""
52
+ return a + b
53
+ ```
54
+
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.
62
+
63
+
64
+ ### Key features:
65
+ * **Fast**: High-level interface means less code and faster development
66
+ * **Simple**: Build MCP servers with minimal boilerplate
67
+ * **Pythonic**: Feels natural to Python developers
68
+ * **Complete***: FastMCP aims to provide a full implementation of the core MCP specification
36
69
 
37
- 🚧 *Note: FastMCP is under active development, as is the low-level MCP Python SDK* 🏗️
70
+ (\*emphasis on *aims*)
71
+
72
+ 🚨 🚧 🏗️ *FastMCP is under active development, as is the MCP specification itself. Core features are working but some advanced capabilities are still in progress.*
38
73
 
39
- Key features:
40
- * **Intuitive**: Designed to feel familiar to Python developers, with powerful type hints and editor support
41
- * **Simple**: Build compliant MCP servers with minimal boilerplate
42
- * **Fast**: High-performance async implementation
43
- * **Full-featured**: Complete implementation of the MCP specification
44
74
 
45
75
  <!-- omit in toc -->
46
76
  ## Table of Contents
@@ -57,7 +87,9 @@ Key features:
57
87
  - [Context](#context)
58
88
  - [Deployment](#deployment)
59
89
  - [Development](#development)
90
+ - [Environment Variables](#environment-variables)
60
91
  - [Claude Desktop](#claude-desktop)
92
+ - [Environment Variables](#environment-variables-1)
61
93
  - [Examples](#examples)
62
94
  - [Echo Server](#echo-server)
63
95
  - [SQLite Explorer](#sqlite-explorer)
@@ -113,19 +145,21 @@ fastmcp install server.py
113
145
  fastmcp dev server.py
114
146
  ```
115
147
 
116
- ![MCP Inspector](docs/images/mcp-inspector.png)
148
+ ![MCP Inspector](/docs/assets/demo-inspector.png)
117
149
 
118
150
  ## What is MCP?
119
151
 
120
152
  The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:
121
153
 
122
- - Expose data through **Resources** (like GET endpoints)
123
- - Provide functionality through **Tools** (like POST endpoints)
154
+ - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
155
+ - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
124
156
  - Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
157
+ - And more!
158
+
159
+ There is a low-level [Python SDK](https://github.com/modelcontextprotocol/python-sdk) available for implementing the protocol directly, but FastMCP aims to make that easier by providing a high-level, Pythonic interface.
125
160
 
126
161
  ## Core Concepts
127
162
 
128
- *Note: All code examples below assume you've created a FastMCP server instance called `mcp`.*
129
163
 
130
164
  ### Server
131
165
 
@@ -140,6 +174,7 @@ mcp = FastMCP("My App")
140
174
  # Configure host/port for HTTP transport (optional)
141
175
  mcp = FastMCP("My App", host="localhost", port=8000)
142
176
  ```
177
+ *Note: All of the following code examples assume you've created a FastMCP server instance called `mcp`, as shown above.*
143
178
 
144
179
  ### Resources
145
180
 
@@ -297,6 +332,10 @@ fastmcp dev server.py --with pandas --with numpy
297
332
  fastmcp dev server.py --with-editable .
298
333
  ```
299
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
+
300
339
  ### Claude Desktop
301
340
 
302
341
  Install your server in Claude Desktop:
@@ -309,9 +348,6 @@ fastmcp install server.py --name "My Server"
309
348
 
310
349
  # With dependencies
311
350
  fastmcp install server.py --with pandas --with numpy
312
-
313
- # Replace an existing server
314
- fastmcp install server.py --force
315
351
  ```
316
352
 
317
353
  The server name in Claude will be:
@@ -319,8 +355,38 @@ The server name in Claude will be:
319
355
  2. The `name` from your FastMCP instance
320
356
  3. The filename if the server can't be imported
321
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
+
322
386
  ## Examples
323
387
 
388
+ Here are a few examples of FastMCP servers. For more, see the `examples/` directory.
389
+
324
390
  ### Echo Server
325
391
  A simple server demonstrating resources, tools, and prompts:
326
392
 
@@ -382,4 +448,4 @@ Schema:
382
448
  {get_schema()}
383
449
 
384
450
  What insights can you provide about the structure and relationships?"""
385
- ```
451
+ ```
@@ -1,5 +1,5 @@
1
1
  <!-- omit in toc -->
2
- # FastMCP
2
+ # FastMCP 🚀
3
3
 
4
4
  <div align="center">
5
5
 
@@ -7,17 +7,46 @@
7
7
  [![Tests](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml)
8
8
  [![License](https://img.shields.io/github/license/jlowin/fastmcp.svg)](https://github.com/jlowin/fastmcp/blob/main/LICENSE)
9
9
 
10
+ The fast, Pythonic way to build MCP servers
11
+
10
12
  </div>
11
13
 
12
- FastMCP is a high-level, intuitive framework for building [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers with Python. While MCP is a powerful protocol that enables LLMs to interact with local data and tools in a secure, standardized way, the specification can be cumbersome to implement directly. FastMCP lets you build fully compliant MCP servers in the most Pythonic way possible - in many cases, simply decorating a function is all that's required.
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
+
16
+ ```python
17
+ # demo.py
18
+
19
+ from fastmcp import FastMCP
20
+
21
+
22
+ mcp = FastMCP("Demo 🚀")
23
+
24
+
25
+ @mcp.tool()
26
+ def add(a: int, b: int) -> int:
27
+ """Add two numbers"""
28
+ return a + b
29
+ ```
30
+
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.
38
+
39
+
40
+ ### Key features:
41
+ * **Fast**: High-level interface means less code and faster development
42
+ * **Simple**: Build MCP servers with minimal boilerplate
43
+ * **Pythonic**: Feels natural to Python developers
44
+ * **Complete***: FastMCP aims to provide a full implementation of the core MCP specification
13
45
 
14
- 🚧 *Note: FastMCP is under active development, as is the low-level MCP Python SDK* 🏗️
46
+ (\*emphasis on *aims*)
47
+
48
+ 🚨 🚧 🏗️ *FastMCP is under active development, as is the MCP specification itself. Core features are working but some advanced capabilities are still in progress.*
15
49
 
16
- Key features:
17
- * **Intuitive**: Designed to feel familiar to Python developers, with powerful type hints and editor support
18
- * **Simple**: Build compliant MCP servers with minimal boilerplate
19
- * **Fast**: High-performance async implementation
20
- * **Full-featured**: Complete implementation of the MCP specification
21
50
 
22
51
  <!-- omit in toc -->
23
52
  ## Table of Contents
@@ -34,7 +63,9 @@ Key features:
34
63
  - [Context](#context)
35
64
  - [Deployment](#deployment)
36
65
  - [Development](#development)
66
+ - [Environment Variables](#environment-variables)
37
67
  - [Claude Desktop](#claude-desktop)
68
+ - [Environment Variables](#environment-variables-1)
38
69
  - [Examples](#examples)
39
70
  - [Echo Server](#echo-server)
40
71
  - [SQLite Explorer](#sqlite-explorer)
@@ -90,19 +121,21 @@ fastmcp install server.py
90
121
  fastmcp dev server.py
91
122
  ```
92
123
 
93
- ![MCP Inspector](docs/images/mcp-inspector.png)
124
+ ![MCP Inspector](/docs/assets/demo-inspector.png)
94
125
 
95
126
  ## What is MCP?
96
127
 
97
128
  The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:
98
129
 
99
- - Expose data through **Resources** (like GET endpoints)
100
- - Provide functionality through **Tools** (like POST endpoints)
130
+ - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
131
+ - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
101
132
  - Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
133
+ - And more!
134
+
135
+ There is a low-level [Python SDK](https://github.com/modelcontextprotocol/python-sdk) available for implementing the protocol directly, but FastMCP aims to make that easier by providing a high-level, Pythonic interface.
102
136
 
103
137
  ## Core Concepts
104
138
 
105
- *Note: All code examples below assume you've created a FastMCP server instance called `mcp`.*
106
139
 
107
140
  ### Server
108
141
 
@@ -117,6 +150,7 @@ mcp = FastMCP("My App")
117
150
  # Configure host/port for HTTP transport (optional)
118
151
  mcp = FastMCP("My App", host="localhost", port=8000)
119
152
  ```
153
+ *Note: All of the following code examples assume you've created a FastMCP server instance called `mcp`, as shown above.*
120
154
 
121
155
  ### Resources
122
156
 
@@ -274,6 +308,10 @@ fastmcp dev server.py --with pandas --with numpy
274
308
  fastmcp dev server.py --with-editable .
275
309
  ```
276
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
+
277
315
  ### Claude Desktop
278
316
 
279
317
  Install your server in Claude Desktop:
@@ -286,9 +324,6 @@ fastmcp install server.py --name "My Server"
286
324
 
287
325
  # With dependencies
288
326
  fastmcp install server.py --with pandas --with numpy
289
-
290
- # Replace an existing server
291
- fastmcp install server.py --force
292
327
  ```
293
328
 
294
329
  The server name in Claude will be:
@@ -296,8 +331,38 @@ The server name in Claude will be:
296
331
  2. The `name` from your FastMCP instance
297
332
  3. The filename if the server can't be imported
298
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
+
299
362
  ## Examples
300
363
 
364
+ Here are a few examples of FastMCP servers. For more, see the `examples/` directory.
365
+
301
366
  ### Echo Server
302
367
  A simple server demonstrating resources, tools, and prompts:
303
368
 
@@ -359,4 +424,4 @@ Schema:
359
424
  {get_schema()}
360
425
 
361
426
  What insights can you provide about the structure and relationships?"""
362
- ```
427
+ ```
@@ -9,6 +9,7 @@ dependencies = [
9
9
  "pydantic-settings>=2.6.1",
10
10
  "pydantic>=2.5.3,<3.0.0",
11
11
  "typer>=0.9.0",
12
+ "python-dotenv>=1.0.1",
12
13
  ]
13
14
  requires-python = ">=3.10"
14
15
  readme = "README.md"
@@ -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
- force: bool = False,
33
+ env_vars: Optional[Dict[str, str]] = None,
34
34
  ) -> bool:
35
- """Add the MCP server to Claude's configuration.
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
- force: If True, replace existing server with same name
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
- if server_name in config["mcpServers"]:
58
- if not force:
59
- logger.warning(
60
- f"Server '{server_name}' already exists in Claude config. "
61
- "Use `--force` to replace.",
62
- extra={"config_file": str(config_file)},
63
- )
64
- return False
65
- logger.info(
66
- f"Replacing existing server '{server_name}' in Claude config",
67
- extra={"config_file": str(config_file)},
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
- config["mcpServers"][server_name] = {
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,15 +5,16 @@ 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
15
16
 
16
- logger = get_logger(__name__)
17
+ logger = get_logger("cli")
17
18
 
18
19
  app = typer.Typer(
19
20
  name="fastmcp",
@@ -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,
@@ -263,7 +275,7 @@ def run(
263
275
 
264
276
  except Exception as e:
265
277
  logger.error(
266
- "Failed to run server",
278
+ f"Failed to run server: {e}",
267
279
  extra={
268
280
  "file": str(file),
269
281
  "error": str(e),
@@ -304,16 +316,32 @@ def install(
304
316
  help="Additional packages to install",
305
317
  ),
306
318
  ] = [],
307
- force: Annotated[
308
- bool,
319
+ env_vars: Annotated[
320
+ list[str],
309
321
  typer.Option(
310
- "--force",
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="Replace existing server if one exists with the same name",
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
- ] = False,
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,14 +372,31 @@ 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
- force=force,
397
+ env_vars=env_dict,
354
398
  ):
355
- print(f"Successfully installed {name} in Claude app")
399
+ logger.info(f"Successfully installed {name} in Claude app")
356
400
  else:
357
- print(f"Failed to install {name} in Claude app")
401
+ logger.error(f"Failed to install {name} in Claude app")
358
402
  sys.exit(1)
@@ -53,7 +53,11 @@ class Settings(BaseSettings):
53
53
  For example, FASTMCP_DEBUG=true will set debug=True.
54
54
  """
55
55
 
56
- model_config: SettingsConfigDict = SettingsConfigDict(env_prefix="FASTMCP_")
56
+ model_config: SettingsConfigDict = SettingsConfigDict(
57
+ env_prefix="FASTMCP_",
58
+ env_file=".env",
59
+ extra="ignore",
60
+ )
57
61
 
58
62
  # Server settings
59
63
  debug: bool = False
@@ -400,7 +404,6 @@ class FastMCP:
400
404
  async def run_stdio_async(self) -> None:
401
405
  """Run the server using stdio transport."""
402
406
  async with stdio_server() as (read_stream, write_stream):
403
- logger.info(f'Starting "{self.name}"...')
404
407
  await self._mcp_server.run(
405
408
  read_stream,
406
409
  write_stream,
@@ -3,15 +3,17 @@
3
3
  import logging
4
4
  from typing import Literal
5
5
 
6
+ from rich.logging import RichHandler
7
+
6
8
 
7
9
  def get_logger(name: str) -> logging.Logger:
8
10
  """Get a logger nested under FastMCP namespace.
9
11
 
10
12
  Args:
11
- name: The name of the logger, which will be prefixed with 'FastMCP.'
13
+ name: the name of the logger, which will be prefixed with 'FastMCP.'
12
14
 
13
15
  Returns:
14
- A configured logger instance
16
+ a configured logger instance
15
17
  """
16
18
  return logging.getLogger(f"FastMCP.{name}")
17
19
 
@@ -22,9 +24,8 @@ def configure_logging(
22
24
  """Configure logging for FastMCP.
23
25
 
24
26
  Args:
25
- level: The log level to use
27
+ level: the log level to use
26
28
  """
27
29
  logging.basicConfig(
28
- level=level,
29
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
30
+ level=level, format="%(message)s", handlers=[RichHandler(rich_tracebacks=True)]
30
31
  )
@@ -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.2.1.dev56+g0fb37ec.d20241201"
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