Skip to content

Commit 7ffda54

Browse files
refactor(sdk): centralize MCP config merge utilities
Move MCP config merge logic into sdk.mcp.merge and align project config helpers with clearer naming and constants. Co-authored-by: openhands <openhands@all-hands.dev>
1 parent ab94043 commit 7ffda54

5 files changed

Lines changed: 49 additions & 39 deletions

File tree

openhands-sdk/openhands/sdk/mcp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
MCPToolDefinition,
88
MCPToolExecutor,
99
)
10+
from openhands.sdk.mcp.merge import merge_mcp_configs
1011
from openhands.sdk.mcp.utils import (
1112
create_mcp_tools,
1213
)
1314

1415

1516
__all__ = [
17+
"merge_mcp_configs",
1618
"MCPClient",
1719
"MCPToolDefinition",
1820
"MCPToolAction",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Merge MCP configuration dictionaries."""
2+
3+
from typing import Any
4+
5+
6+
def merge_mcp_configs(
7+
base: dict[str, Any] | None,
8+
overlay: dict[str, Any] | None,
9+
) -> dict[str, Any]:
10+
"""Merge two MCP config dicts; overlay wins on key conflicts.
11+
12+
``mcpServers`` entries are merged by server name; other top-level keys use
13+
shallow overlay semantics (overlay wins).
14+
"""
15+
match (base, overlay):
16+
case (None, None):
17+
return {}
18+
case (None, _):
19+
return dict(overlay)
20+
case (_, None):
21+
return dict(base)
22+
23+
result = {**base, **overlay}
24+
if "mcpServers" in base and "mcpServers" in overlay:
25+
result["mcpServers"] = {**base["mcpServers"], **overlay["mcpServers"]}
26+
return result

openhands-sdk/openhands/sdk/mcp/project_config.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
11
"""Project-level .mcp.json discovery and loading."""
22

3-
from __future__ import annotations
4-
53
from pathlib import Path
6-
from typing import Any
4+
from typing import Any, Final
75

86
from openhands.sdk.context.skills.exceptions import SkillValidationError
97
from openhands.sdk.context.skills.utils import load_mcp_config
108
from openhands.sdk.logger import get_logger
119

1210
logger = get_logger(__name__)
1311

12+
_PROJECT_MCP_CANDIDATES: Final[tuple[str, ...]] = (
13+
".openhands/.mcp.json",
14+
".mcp.json",
15+
)
16+
1417

15-
def find_project_mcp_json(project_dir: Path) -> Path | None:
18+
def _find_project_mcp_json(project_dir: Path) -> Path | None:
1619
"""Return the first project MCP config path if present.
1720
18-
Preference order: ``.openhands/.mcp.json``, then root ``.mcp.json``.
21+
Preference order follows ``_PROJECT_MCP_CANDIDATES``.
1922
"""
20-
for candidate in (
21-
project_dir / ".openhands" / ".mcp.json",
22-
project_dir / ".mcp.json",
23-
):
23+
for rel in _PROJECT_MCP_CANDIDATES:
24+
candidate = project_dir / rel
2425
if candidate.is_file():
2526
return candidate
2627
return None
2728

2829

29-
def try_load_project_mcp_config(project_dir: Path) -> dict[str, Any] | None:
30-
"""Load and validate project ``.mcp.json``, or return None if missing or invalid."""
31-
path = find_project_mcp_json(project_dir)
30+
def load_project_mcp_config(project_dir: Path) -> dict[str, Any] | None:
31+
"""Load and validate project ``.mcp.json``.
32+
33+
Uses ``load_mcp_config`` from skills (variable expansion, ``MCPConfig``
34+
validation). Returns ``None`` if no file exists, or if the file is
35+
invalid (logged and ignored).
36+
"""
37+
path = _find_project_mcp_json(project_dir)
3238
if path is None:
3339
return None
3440
try:

openhands-sdk/openhands/sdk/plugin/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
update_plugin,
2929
)
3030
from openhands.sdk.plugin.loader import load_plugins
31-
from openhands.sdk.plugin.plugin import Plugin, merge_mcp_configs
31+
from openhands.sdk.mcp.merge import merge_mcp_configs
32+
from openhands.sdk.plugin.plugin import Plugin
3233
from openhands.sdk.plugin.source import (
3334
GitHubURLComponents,
3435
is_local_path,

openhands-sdk/openhands/sdk/plugin/plugin.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
PluginAuthor,
2323
PluginManifest,
2424
)
25+
from openhands.sdk.mcp.merge import merge_mcp_configs
2526
from openhands.sdk.subagent.schema import AgentDefinition
2627

2728

@@ -30,32 +31,6 @@
3031

3132
logger = get_logger(__name__)
3233

33-
34-
def merge_mcp_configs(
35-
base: dict[str, Any] | None,
36-
overlay: dict[str, Any] | None,
37-
) -> dict[str, Any]:
38-
"""Merge two MCP config dicts; overlay wins on key conflicts."""
39-
if base is None and overlay is None:
40-
return {}
41-
if base is None:
42-
return dict(overlay) if overlay is not None else {}
43-
if overlay is None:
44-
return dict(base)
45-
46-
result = dict(base)
47-
if "mcpServers" in overlay:
48-
existing_servers = result.get("mcpServers", {})
49-
result["mcpServers"] = {
50-
**existing_servers,
51-
**overlay["mcpServers"],
52-
}
53-
for key, value in overlay.items():
54-
if key != "mcpServers":
55-
result[key] = value
56-
return result
57-
58-
5934
# Directories to check for plugin manifest
6035
PLUGIN_MANIFEST_DIRS = [".plugin", ".claude-plugin"]
6136
PLUGIN_MANIFEST_FILE = "plugin.json"

0 commit comments

Comments
 (0)