mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-21 07:26:50 +00:00
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:
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user