Skip to content

Commit 58b6687

Browse files
author
Henry Lee
committed
feat: support env vars in dev command
1 parent 3d7b311 commit 58b6687

3 files changed

Lines changed: 79 additions & 22 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,9 @@ uv run mcp dev server.py
11741174
# Add dependencies
11751175
uv run mcp dev server.py --with pandas --with numpy
11761176

1177+
# Load environment variables
1178+
uv run mcp dev server.py --env-var API_KEY=abc123 --env-file .env
1179+
11771180
# Mount local code
11781181
uv run mcp dev server.py --with-editable .
11791182
```

src/mcp/cli/cli.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,30 @@ def _parse_env_var(env_var: str) -> tuple[str, str]: # pragma: no cover
6262
return key.strip(), value.strip()
6363

6464

65+
def _collect_env_vars(env_file: Path | None, env_vars: list[str]) -> dict[str, str] | None:
66+
"""Collect environment variables from a .env file and CLI KEY=VALUE pairs."""
67+
if not env_file and not env_vars:
68+
return None
69+
70+
env_dict: dict[str, str] = {}
71+
if env_file:
72+
if dotenv:
73+
try:
74+
env_dict |= {k: v for k, v in dotenv.dotenv_values(env_file).items() if v is not None}
75+
except (OSError, ValueError):
76+
logger.exception("Failed to load .env file")
77+
sys.exit(1)
78+
else:
79+
logger.error("python-dotenv is not installed. Cannot load .env file.")
80+
sys.exit(1)
81+
82+
for env_var in env_vars:
83+
key, value = _parse_env_var(env_var)
84+
env_dict[key] = value
85+
86+
return env_dict
87+
88+
6589
def _build_uv_command(
6690
file_spec: str,
6791
with_editable: Path | None = None,
@@ -241,9 +265,30 @@ def dev(
241265
help="Additional packages to install",
242266
),
243267
] = [],
268+
env_vars: Annotated[
269+
list[str],
270+
typer.Option(
271+
"--env-var",
272+
"-v",
273+
help="Environment variables in KEY=VALUE format",
274+
),
275+
] = [],
276+
env_file: Annotated[
277+
Path | None,
278+
typer.Option(
279+
"--env-file",
280+
"-f",
281+
help="Load environment variables from a .env file",
282+
exists=True,
283+
file_okay=True,
284+
dir_okay=False,
285+
resolve_path=True,
286+
),
287+
] = None,
244288
) -> None: # pragma: no cover
245289
"""Run an MCP server with the MCP Inspector."""
246290
file, server_object = _parse_file_path(file_spec)
291+
env_dict = _collect_env_vars(env_file, env_vars)
247292

248293
logger.debug(
249294
"Starting dev server",
@@ -252,6 +297,8 @@ def dev(
252297
"server_object": server_object,
253298
"with_editable": str(with_editable) if with_editable else None,
254299
"with_packages": with_packages,
300+
"env_file": str(env_file) if env_file else None,
301+
"env_vars": list(env_dict) if env_dict else [],
255302
},
256303
)
257304

@@ -273,11 +320,14 @@ def dev(
273320

274321
# Run the MCP Inspector command with shell=True on Windows
275322
shell = sys.platform == "win32"
323+
process_env = dict(os.environ.items())
324+
if env_dict:
325+
process_env.update(env_dict)
276326
process = subprocess.run(
277327
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
278328
check=True,
279329
shell=shell,
280-
env=dict(os.environ.items()), # Convert to list of tuples for env update
330+
env=process_env,
281331
)
282332
sys.exit(process.returncode)
283333
except subprocess.CalledProcessError as e:
@@ -452,26 +502,7 @@ def install(
452502
if server_dependencies:
453503
with_packages = list(set(with_packages + server_dependencies))
454504

455-
# Process environment variables if provided
456-
env_dict: dict[str, str] | None = None
457-
if env_file or env_vars:
458-
env_dict = {}
459-
# Load from .env file if specified
460-
if env_file:
461-
if dotenv:
462-
try:
463-
env_dict |= {k: v for k, v in dotenv.dotenv_values(env_file).items() if v is not None}
464-
except (OSError, ValueError):
465-
logger.exception("Failed to load .env file")
466-
sys.exit(1)
467-
else:
468-
logger.error("python-dotenv is not installed. Cannot load .env file.")
469-
sys.exit(1)
470-
471-
# Add command line environment variables
472-
for env_var in env_vars:
473-
key, value = _parse_env_var(env_var)
474-
env_dict[key] = value
505+
env_dict = _collect_env_vars(env_file, env_vars)
475506

476507
if claude.update_claude_config(
477508
file_spec,

tests/cli/test_utils.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
import pytest
77

8-
from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path # type: ignore[reportPrivateUsage]
8+
from mcp.cli.cli import ( # type: ignore[reportPrivateUsage]
9+
_build_uv_command,
10+
_collect_env_vars,
11+
_get_npx_command,
12+
_parse_file_path,
13+
)
914

1015

1116
@pytest.mark.parametrize(
@@ -69,6 +74,24 @@ def test_build_uv_command_adds_editable_and_packages():
6974
]
7075

7176

77+
def test_collect_env_vars_returns_none_without_inputs():
78+
"""Should not allocate an env block when no env sources were provided."""
79+
assert _collect_env_vars(None, []) is None
80+
81+
82+
def test_collect_env_vars_from_cli_values():
83+
"""CLI env vars should be parsed as KEY=VALUE pairs."""
84+
assert _collect_env_vars(None, ["API_KEY=abc123", "EMPTY="]) == {"API_KEY": "abc123", "EMPTY": ""}
85+
86+
87+
def test_collect_env_vars_file_then_cli_override(tmp_path: Path):
88+
"""CLI env vars should override values loaded from a .env file."""
89+
env_file = tmp_path / ".env"
90+
env_file.write_text("API_KEY=file-value\nKEEP=from-file\n", encoding="utf-8")
91+
92+
assert _collect_env_vars(env_file, ["API_KEY=cli-value"]) == {"API_KEY": "cli-value", "KEEP": "from-file"}
93+
94+
7295
def test_get_npx_unix_like(monkeypatch: pytest.MonkeyPatch):
7396
"""Should return "npx" on unix-like systems."""
7497
monkeypatch.setattr(sys, "platform", "linux")

0 commit comments

Comments
 (0)