Fix env resolution in MCP config lists (#2556)

* Fix env resolution in MCP config lists

* fix:unset env variable and consistent function

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Yuyi Ao
2026-05-20 16:27:00 -07:00
committed by GitHub
parent b6b3650e50
commit 9afeaf66bc
2 changed files with 42 additions and 19 deletions
@@ -141,7 +141,7 @@ class ExtensionsConfig(BaseModel):
try: try:
with open(resolved_path, encoding="utf-8") as f: with open(resolved_path, encoding="utf-8") as f:
config_data = json.load(f) config_data = json.load(f)
cls.resolve_env_variables(config_data) config_data = cls.resolve_env_variables(config_data)
return cls.model_validate(config_data) return cls.model_validate(config_data)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ValueError(f"Extensions config file at {resolved_path} is not valid JSON: {e}") from e raise ValueError(f"Extensions config file at {resolved_path} is not valid JSON: {e}") from e
@@ -149,7 +149,7 @@ class ExtensionsConfig(BaseModel):
raise RuntimeError(f"Failed to load extensions config from {resolved_path}: {e}") from e raise RuntimeError(f"Failed to load extensions config from {resolved_path}: {e}") from e
@classmethod @classmethod
def resolve_env_variables(cls, config: dict[str, Any]) -> dict[str, Any]: def resolve_env_variables(cls, config: Any) -> Any:
"""Recursively resolve environment variables in the config. """Recursively resolve environment variables in the config.
Environment variables are resolved using the `os.getenv` function. Example: $OPENAI_API_KEY Environment variables are resolved using the `os.getenv` function. Example: $OPENAI_API_KEY
@@ -160,23 +160,26 @@ class ExtensionsConfig(BaseModel):
Returns: Returns:
The config with environment variables resolved. The config with environment variables resolved.
""" """
for key, value in config.items(): if isinstance(config, str):
if isinstance(value, str): if not config.startswith("$"):
if value.startswith("$"): return config
env_value = os.getenv(value[1:]) env_value = os.getenv(config[1:])
if env_value is None: if env_value is None:
# Unresolved placeholder — store empty string so downstream # Unresolved placeholder — store empty string so downstream
# consumers (e.g. MCP servers) don't receive the literal "$VAR" # consumers (e.g. MCP servers) don't receive the literal "$VAR"
# token as an actual environment value. # token as an actual environment value.
config[key] = "" return ""
else: return env_value
config[key] = env_value
else: if isinstance(config, dict):
config[key] = value return {key: cls.resolve_env_variables(value) for key, value in config.items()}
elif isinstance(value, dict):
config[key] = cls.resolve_env_variables(value) if isinstance(config, list):
elif isinstance(value, list): return [cls.resolve_env_variables(item) for item in config]
config[key] = [cls.resolve_env_variables(item) if isinstance(item, dict) else item for item in value]
if isinstance(config, tuple):
return tuple(cls.resolve_env_variables(item) for item in config)
return config return config
def get_enabled_mcp_servers(self) -> dict[str, McpServerConfig]: def get_enabled_mcp_servers(self) -> dict[str, McpServerConfig]:
+20
View File
@@ -24,6 +24,26 @@ def test_build_server_params_stdio_success():
} }
def test_extensions_config_resolves_env_variables_inside_nested_collections(monkeypatch):
monkeypatch.setenv("MCP_TOKEN", "secret")
monkeypatch.delenv("MISSING_TOKEN", raising=False)
raw_config = {
"args": ["--token", "$MCP_TOKEN", {"nested": ["$MCP_TOKEN", "$MISSING_TOKEN"]}],
"tuple_args": ("$MCP_TOKEN", "$MISSING_TOKEN"),
"env": {"API_KEY": "$MCP_TOKEN"},
"enabled": True,
"timeout": 30,
}
resolved = ExtensionsConfig.resolve_env_variables(raw_config)
assert resolved["args"] == ["--token", "secret", {"nested": ["secret", ""]}]
assert resolved["tuple_args"] == ("secret", "")
assert resolved["env"] == {"API_KEY": "secret"}
assert resolved["enabled"] is True
assert resolved["timeout"] == 30
def test_build_server_params_stdio_requires_command(): def test_build_server_params_stdio_requires_command():
config = McpServerConfig(type="stdio", command=None) config = McpServerConfig(type="stdio", command=None)