From c8a1e833475ddbe2fba5fe9e871eb10e1a19c0ad Mon Sep 17 00:00:00 2001 From: Mohit Bhalotia Date: Thu, 20 Aug 2026 10:22:00 +0000 Subject: [PATCH] [verified] fix: recover MCP config for ACP presets --- .../automation/presets/plugin/sdk_main.py | 16 ++++++- .../automation/presets/prompt/sdk_main.py | 16 ++++++- tests/test_preset_router.py | 44 +++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/openhands/automation/presets/plugin/sdk_main.py b/openhands/automation/presets/plugin/sdk_main.py index 7c259b15..85c5927e 100644 --- a/openhands/automation/presets/plugin/sdk_main.py +++ b/openhands/automation/presets/plugin/sdk_main.py @@ -160,6 +160,20 @@ def _normalize_mcp_config(raw_mcp_config): return raw_mcp_config +def _get_workspace_mcp_config(workspace): + raw_mcp_config = workspace.get_mcp_config() + if raw_mcp_config: + return _normalize_mcp_config(raw_mcp_config) + + # openhands-sdk 1.42.1 returns {} for ACPAgentSettings even though those + # settings contain mcp_config. Recover it until the SDK accepts ACP settings. + fetch_agent_settings = getattr(workspace, "_fetch_agent_settings", None) + if not callable(fetch_agent_settings): + return {} + settings = fetch_agent_settings() + return _normalize_mcp_config(getattr(settings, "mcp_config", {})) + + def _build_conversation_title(event_context) -> str | None: """Build a descriptive conversation title from the automation event context. @@ -384,7 +398,7 @@ def _build_conversation_title(event_context) -> str | None: print("\n=== GET_MCP_CONFIG ===") mcp_config = {} try: - mcp_config = _normalize_mcp_config(workspace.get_mcp_config()) + mcp_config = _get_workspace_mcp_config(workspace) if mcp_config: print(f" servers: {list(mcp_config.keys())}") else: diff --git a/openhands/automation/presets/prompt/sdk_main.py b/openhands/automation/presets/prompt/sdk_main.py index c0014af5..8f5c303b 100644 --- a/openhands/automation/presets/prompt/sdk_main.py +++ b/openhands/automation/presets/prompt/sdk_main.py @@ -163,6 +163,20 @@ def _normalize_mcp_config(raw_mcp_config): return raw_mcp_config +def _get_workspace_mcp_config(workspace): + raw_mcp_config = workspace.get_mcp_config() + if raw_mcp_config: + return _normalize_mcp_config(raw_mcp_config) + + # openhands-sdk 1.42.1 returns {} for ACPAgentSettings even though those + # settings contain mcp_config. Recover it until the SDK accepts ACP settings. + fetch_agent_settings = getattr(workspace, "_fetch_agent_settings", None) + if not callable(fetch_agent_settings): + return {} + settings = fetch_agent_settings() + return _normalize_mcp_config(getattr(settings, "mcp_config", {})) + + def _build_conversation_title(event_context) -> str | None: """Build a descriptive conversation title from the automation event context. @@ -353,7 +367,7 @@ def _build_conversation_title(event_context) -> str | None: print("\n=== GET_MCP_CONFIG ===") mcp_config = {} try: - mcp_config = _normalize_mcp_config(workspace.get_mcp_config()) + mcp_config = _get_workspace_mcp_config(workspace) if mcp_config: print(f" servers: {list(mcp_config.keys())}") else: diff --git a/tests/test_preset_router.py b/tests/test_preset_router.py index 64109b82..bed17d18 100644 --- a/tests/test_preset_router.py +++ b/tests/test_preset_router.py @@ -76,6 +76,34 @@ def _load_preset_mcp_normalizer( return cast(Callable[[Any], Any], namespace["_normalize_mcp_config"]) +def _load_preset_workspace_mcp_loader(preset_name: str) -> Callable[[Any], Any]: + source_path = PRESETS_DIR / preset_name / "sdk_main.py" + module = ast.parse(source_path.read_text(), filename=str(source_path)) + function_nodes = { + node.name: node + for node in module.body + if isinstance(node, ast.FunctionDef) + and node.name in {"_normalize_mcp_config", "_get_workspace_mcp_config"} + } + assert "_get_workspace_mcp_config" in function_nodes, ( + f"{preset_name} preset must recover MCP config from ACP settings" + ) + namespace: dict[str, Any] = {"_coerce_mcp_config": coerce_mcp_config} + selected_nodes: list[ast.stmt] = [ + function_nodes["_normalize_mcp_config"], + function_nodes["_get_workspace_mcp_config"], + ] + for node in selected_nodes: + ast.fix_missing_locations(node) + exec( + compile( + ast.Module(body=selected_nodes, type_ignores=[]), str(source_path), "exec" + ), + namespace, + ) + return cast(Callable[[Any], Any], namespace["_get_workspace_mcp_config"]) + + def _load_preset_title_builder(preset_name: str) -> Callable[[Any], str | None]: source_path = PRESETS_DIR / preset_name / "sdk_main.py" module = ast.parse(source_path.read_text(), filename=str(source_path)) @@ -226,6 +254,22 @@ def test_preset_mcp_normalizer_unwraps_without_sdk_coercer(preset_name): assert normalize(None) == {} +@pytest.mark.parametrize("preset_name", ["prompt", "plugin"]) +def test_preset_recovers_mcp_config_from_acp_settings(preset_name): + load_mcp_config = _load_preset_workspace_mcp_loader(preset_name) + workspace = MagicMock() + workspace.get_mcp_config.return_value = {} + workspace._fetch_agent_settings.return_value.mcp_config = { + "Plane": {"command": "uvx", "args": ["plane-mcp-server", "stdio"]} + } + + mcp_config = load_mcp_config(workspace) + + assert list(mcp_config) == ["Plane"] + workspace.get_mcp_config.assert_called_once_with() + workspace._fetch_agent_settings.assert_called_once_with() + + _UTC_TIMESTAMP_RE = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC")