11"""Tests for project-level .mcp.json discovery and merge behavior."""
22
33import json
4+ import logging
45from pathlib import Path
56
67import pytest
7- from pydantic import SecretStr
88
9- from openhands .sdk import Agent , LLM
9+ from openhands .sdk import Agent
1010from openhands .sdk .conversation .impl .local_conversation import LocalConversation
11- from openhands .sdk .mcp .project_config import find_project_mcp_json , try_load_project_mcp_config
12- from openhands .sdk .plugin import PluginSource , merge_mcp_configs
11+ from openhands .sdk .mcp .merge import merge_mcp_configs
12+ from openhands .sdk .mcp .project_config import (
13+ _find_project_mcp_json ,
14+ load_project_mcp_config ,
15+ )
16+ from openhands .sdk .plugin import PluginSource
17+ from openhands .sdk .testing import TestLLM
1318
1419
1520def _minimal_mcp_file () -> dict :
@@ -26,7 +31,7 @@ def test_find_project_mcp_json_prefers_openhands_dir(tmp_path: Path) -> None:
2631 preferred ["mcpServers" ]["proj" ]["args" ] = ["preferred" ]
2732 (oh / ".mcp.json" ).write_text (json .dumps (preferred ))
2833
29- found = find_project_mcp_json (root )
34+ found = _find_project_mcp_json (root )
3035 assert found == oh / ".mcp.json"
3136
3237
@@ -35,7 +40,7 @@ def test_find_project_mcp_json_falls_back_to_root(tmp_path: Path) -> None:
3540 root .mkdir ()
3641 (root / ".mcp.json" ).write_text (json .dumps (_minimal_mcp_file ()))
3742
38- assert find_project_mcp_json (root ) == root / ".mcp.json"
43+ assert _find_project_mcp_json (root ) == root / ".mcp.json"
3944
4045
4146def test_merge_mcp_configs_overlay_wins () -> None :
@@ -47,15 +52,37 @@ def test_merge_mcp_configs_overlay_wins() -> None:
4752
4853
4954@pytest .fixture
50- def mock_llm ():
51- return LLM ( model = "test/model" , api_key = SecretStr ( "test-key" ) )
55+ def mock_llm () -> TestLLM :
56+ return TestLLM . from_messages ([] )
5257
5358
5459@pytest .fixture
55- def basic_agent (mock_llm ) :
60+ def basic_agent (mock_llm : TestLLM ) -> Agent :
5661 return Agent (llm = mock_llm , tools = [])
5762
5863
64+ def test_load_project_mcp_config_expands_env_vars (
65+ tmp_path : Path , monkeypatch : pytest .MonkeyPatch
66+ ) -> None :
67+ ws = tmp_path / "ws"
68+ ws .mkdir ()
69+ monkeypatch .setenv ("OH_PROJECT_MCP_TEST" , "expanded-cmd" )
70+ cfg = {
71+ "mcpServers" : {
72+ "s" : {
73+ "command" : "${OH_PROJECT_MCP_TEST}" ,
74+ "args" : ["${MISSING:-default-arg}" ],
75+ }
76+ }
77+ }
78+ (ws / ".mcp.json" ).write_text (json .dumps (cfg ))
79+
80+ loaded = load_project_mcp_config (ws )
81+ assert loaded is not None
82+ assert loaded ["mcpServers" ]["s" ]["command" ] == "expanded-cmd"
83+ assert loaded ["mcpServers" ]["s" ]["args" ] == ["default-arg" ]
84+
85+
5986def test_trust_project_mcp_merges_under_user_config (
6087 tmp_path : Path , basic_agent : Agent , monkeypatch : pytest .MonkeyPatch
6188) -> None :
@@ -148,15 +175,13 @@ def test_project_mcp_layer_before_plugin(
148175 conv .close ()
149176
150177
151- def test_try_load_project_mcp_config_invalid_json_logs (
178+ def test_load_project_mcp_config_invalid_json_logs (
152179 tmp_path : Path , caplog : pytest .LogCaptureFixture
153180) -> None :
154181 ws = tmp_path / "ws"
155182 ws .mkdir ()
156183 (ws / ".mcp.json" ).write_text ("not json" )
157184
158- import logging
159-
160185 with caplog .at_level (logging .WARNING ):
161- assert try_load_project_mcp_config (ws ) is None
186+ assert load_project_mcp_config (ws ) is None
162187 assert "Ignoring invalid project MCP config" in caplog .text
0 commit comments