diff --git a/openhands-workspace/openhands/workspace/cloud/workspace.py b/openhands-workspace/openhands/workspace/cloud/workspace.py index d0dd2b7e71..802413de59 100644 --- a/openhands-workspace/openhands/workspace/cloud/workspace.py +++ b/openhands-workspace/openhands/workspace/cloud/workspace.py @@ -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 @@ -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() @@ -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 diff --git a/tests/workspace/test_cloud_workspace_sdk_settings.py b/tests/workspace/test_cloud_workspace_sdk_settings.py index e7e54990b3..5cfbe60262 100644 --- a/tests/workspace/test_cloud_workspace_sdk_settings.py +++ b/tests/workspace/test_cloud_workspace_sdk_settings.py @@ -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": { @@ -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}, )