Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions openhands-workspace/openhands/workspace/cloud/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,10 +682,16 @@ def get_secrets(self, names: list[str] | None = None) -> dict[str, LookupSecret]
def get_mcp_config(self) -> dict[str, Any]:
"""Fetch MCP configuration from the user's SaaS account.

Calls ``GET /api/v1/users/me`` to retrieve the user's MCP configuration
and transforms it into the format expected by the SDK Agent and
Calls ``GET /api/v1/users/me?expose_secrets=true`` to retrieve the
user's MCP configuration in the format expected by the SDK Agent and
``fastmcp.mcp_config.MCPConfig``.

The API returns ``mcp_config`` in the standard ``fastmcp.MCPConfig``
format with a ``mcpServers`` dict. This method passes that through
directly. For backward compatibility, it also supports transforming
the legacy format (``sse_servers``, ``shttp_servers``, ``stdio_servers``
arrays) if encountered.

Returns:
A dictionary with ``mcpServers`` key containing server configurations
(compatible with ``MCPConfig.model_validate()``), or an empty dict
Expand All @@ -711,6 +717,7 @@ def get_mcp_config(self) -> dict[str, Any]:
resp = self._send_api_request(
"GET",
f"{self.cloud_api_url}/api/v1/users/me",
params={"expose_secrets": "true"},
headers={"X-Session-API-Key": self._session_api_key or ""},
)
data = resp.json()
Expand All @@ -719,6 +726,11 @@ def get_mcp_config(self) -> dict[str, Any]:
if not mcp_config_data:
return {}

# Standard format: API returns mcpServers dict directly (fastmcp.MCPConfig)
if "mcpServers" in mcp_config_data:
return mcp_config_data

# Legacy format: transform sse_servers/shttp_servers/stdio_servers arrays
mcp_servers: dict[str, dict[str, Any]] = {}

# Transform SSE servers → RemoteMCPServer format
Expand Down
53 changes: 52 additions & 1 deletion tests/workspace/test_cloud_workspace_sdk_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,58 @@ def test_get_mcp_config_returns_empty_when_no_config(self, mock_workspace):

assert mcp_config == {}

def test_get_mcp_config_passthrough_mcpservers_format(self, mock_workspace):
"""get_mcp_config passes through mcpServers format from API directly.

This is the standard fastmcp.MCPConfig format returned by the API since
April 2025. The method should recognize this format and return it as-is.
"""
mock_response = MagicMock()
mock_response.json.return_value = {
"mcp_config": {
"mcpServers": {
"hubspot": {
"url": "https://mcp.example.com/hubspot",
"transport": "streamable-http",
"headers": {"Authorization": "Bearer secret-key"},
},
"slack": {
"url": "https://mcp.example.com/slack",
"transport": "sse",
},
}
}
}
mock_response.raise_for_status = MagicMock()

with patch.object(
mock_workspace, "_send_api_request", return_value=mock_response
) as mock_req:
mcp_config = mock_workspace.get_mcp_config()

# Verify expose_secrets is passed
mock_req.assert_called_once_with(
"GET",
f"{CLOUD_URL}/api/v1/users/me",
params={"expose_secrets": "true"},
headers={"X-Session-API-Key": SESSION_KEY},
)

# Should pass through the mcpServers format directly
assert "mcpServers" in mcp_config
assert len(mcp_config["mcpServers"]) == 2
assert (
mcp_config["mcpServers"]["hubspot"]["url"]
== "https://mcp.example.com/hubspot"
)
assert (
mcp_config["mcpServers"]["hubspot"]["headers"]["Authorization"]
== "Bearer secret-key"
)
assert mcp_config["mcpServers"]["slack"]["transport"] == "sse"

def test_get_mcp_config_transforms_sse_servers(self, mock_workspace):
"""get_mcp_config correctly transforms SSE servers."""
"""get_mcp_config correctly transforms legacy SSE servers format."""
mock_response = MagicMock()
mock_response.json.return_value = {
"mcp_config": {
Expand All @@ -232,6 +282,7 @@ def test_get_mcp_config_transforms_sse_servers(self, mock_workspace):
mock_req.assert_called_once_with(
"GET",
f"{CLOUD_URL}/api/v1/users/me",
params={"expose_secrets": "true"},
headers={"X-Session-API-Key": SESSION_KEY},
)

Expand Down
Loading