From 9afeaf66bc1b8fcd190ebf233525510248888d4a Mon Sep 17 00:00:00 2001 From: Yuyi Ao Date: Wed, 20 May 2026 16:27:00 -0700 Subject: [PATCH] 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 --- .../deerflow/config/extensions_config.py | 41 ++++++++++--------- backend/tests/test_mcp_client_config.py | 20 +++++++++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/backend/packages/harness/deerflow/config/extensions_config.py b/backend/packages/harness/deerflow/config/extensions_config.py index a2daa71f4..425da12b8 100644 --- a/backend/packages/harness/deerflow/config/extensions_config.py +++ b/backend/packages/harness/deerflow/config/extensions_config.py @@ -141,7 +141,7 @@ class ExtensionsConfig(BaseModel): try: with open(resolved_path, encoding="utf-8") as 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) except json.JSONDecodeError as 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 @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. Environment variables are resolved using the `os.getenv` function. Example: $OPENAI_API_KEY @@ -160,23 +160,26 @@ class ExtensionsConfig(BaseModel): Returns: The config with environment variables resolved. """ - for key, value in config.items(): - if isinstance(value, str): - if value.startswith("$"): - env_value = os.getenv(value[1:]) - if env_value is None: - # Unresolved placeholder — store empty string so downstream - # consumers (e.g. MCP servers) don't receive the literal "$VAR" - # token as an actual environment value. - config[key] = "" - else: - config[key] = env_value - else: - config[key] = value - elif isinstance(value, dict): - config[key] = cls.resolve_env_variables(value) - elif isinstance(value, list): - config[key] = [cls.resolve_env_variables(item) if isinstance(item, dict) else item for item in value] + if isinstance(config, str): + if not config.startswith("$"): + return config + env_value = os.getenv(config[1:]) + if env_value is None: + # Unresolved placeholder — store empty string so downstream + # consumers (e.g. MCP servers) don't receive the literal "$VAR" + # token as an actual environment value. + return "" + return env_value + + if isinstance(config, dict): + return {key: cls.resolve_env_variables(value) for key, value in config.items()} + + if isinstance(config, list): + return [cls.resolve_env_variables(item) for item in config] + + if isinstance(config, tuple): + return tuple(cls.resolve_env_variables(item) for item in config) + return config def get_enabled_mcp_servers(self) -> dict[str, McpServerConfig]: diff --git a/backend/tests/test_mcp_client_config.py b/backend/tests/test_mcp_client_config.py index 6d0083c0c..ca4d0de59 100644 --- a/backend/tests/test_mcp_client_config.py +++ b/backend/tests/test_mcp_client_config.py @@ -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(): config = McpServerConfig(type="stdio", command=None)