From 17ae03857f235769be10448101b9a719ed25c30f Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Mon, 2 Jun 2025 17:41:47 -0400 Subject: [PATCH] fix(mcp): authentication for mcp calls in remote runtime (#8856) --- openhands/core/config/mcp_config.py | 2 +- openhands/mcp/client.py | 10 +++++++++- openhands/runtime/base.py | 8 ++++++++ .../impl/action_execution/action_execution_client.py | 3 +-- openhands/runtime/impl/remote/remote_runtime.py | 10 ++++++++++ poetry.lock | 8 ++++---- pyproject.toml | 2 +- 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/openhands/core/config/mcp_config.py b/openhands/core/config/mcp_config.py index e8f52800b2..58d368060c 100644 --- a/openhands/core/config/mcp_config.py +++ b/openhands/core/config/mcp_config.py @@ -158,7 +158,7 @@ class OpenHandsMCPConfig: return MCPStdioServerConfig( name='tavily', command='npx', - args=['-y', 'tavily-mcp@0.1.4'], + args=['-y', 'tavily-mcp@0.2.1'], env={'TAVILY_API_KEY': app_config.search_api_key.get_secret_value()}, ) else: diff --git a/openhands/mcp/client.py b/openhands/mcp/client.py index f6ffcbdd1e..d4062a1d36 100644 --- a/openhands/mcp/client.py +++ b/openhands/mcp/client.py @@ -45,7 +45,15 @@ class MCPClient(BaseModel): try: # Use asyncio.wait_for to enforce the timeout async def connect_with_timeout(): - headers = {'Authorization': f'Bearer {api_key}'} if api_key else {} + headers = ( + { + 'Authorization': f'Bearer {api_key}', + 's': api_key, # We need this for action execution server's MCP Router + 'X-Session-API-Key': api_key, # We need this for Remote Runtime + } + if api_key + else {} + ) if conversation_id: headers['X-OpenHands-Conversation-ID'] = conversation_id diff --git a/openhands/runtime/base.py b/openhands/runtime/base.py index 9325bc6d3b..d82a9786eb 100644 --- a/openhands/runtime/base.py +++ b/openhands/runtime/base.py @@ -887,6 +887,14 @@ fi """Zip all files in the sandbox and return a path in the local filesystem.""" raise NotImplementedError('This method is not implemented in the base class.') + # ==================================================================== + # Authentication + # ==================================================================== + + @property + def session_api_key(self) -> str | None: + return None + # ==================================================================== # VSCode # ==================================================================== diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py index dd13d24d3b..47c1534a76 100644 --- a/openhands/runtime/impl/action_execution/action_execution_client.py +++ b/openhands/runtime/impl/action_execution/action_execution_client.py @@ -436,8 +436,7 @@ class ActionExecutionClient(Runtime): updated_mcp_config.sse_servers.append( MCPSSEServerConfig( url=self.action_execution_server_url.rstrip('/') + '/sse', - # No API key by default. Child runtime can override this when appropriate - api_key=None, + api_key=self.session_api_key, ) ) diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index 0164118816..063bd236f2 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -98,6 +98,7 @@ class RemoteRuntime(ActionExecutionClient): self.session, ) self.available_hosts: dict[str, int] = {} + self._session_api_key: str | None = None def log(self, level: str, message: str, exc_info: bool | None = None) -> None: getattr(logger, level)( @@ -358,6 +359,15 @@ class RemoteRuntime(ActionExecutionClient): self.session.headers.update( {'X-Session-API-Key': start_response['session_api_key']} ) + self._session_api_key = start_response['session_api_key'] + self.log( + 'debug', + f'Session API key setted', + ) + + @property + def session_api_key(self) -> str | None: + return self._session_api_key @property def vscode_url(self) -> str | None: diff --git a/poetry.lock b/poetry.lock index a41c369b41..c3d74d6604 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2459,14 +2459,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "fastmcp" -version = "2.5.1" +version = "2.5.2" description = "The fast, Pythonic way to build MCP servers." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fastmcp-2.5.1-py3-none-any.whl", hash = "sha256:a6fe50693954a6aed89fc6e43f227dcd66e112e3d3a1d633ee22b4f435ee8aed"}, - {file = "fastmcp-2.5.1.tar.gz", hash = "sha256:0d10ec65a362ae4f78bdf3b639faf35b36cc0a1c8f5461a54fac906fe821b84d"}, + {file = "fastmcp-2.5.2-py3-none-any.whl", hash = "sha256:4ea46ef35c1308b369eff7c8a10e4c9639bed046fd646449c1227ac7c3856d83"}, + {file = "fastmcp-2.5.2.tar.gz", hash = "sha256:761c92fb54f561136f631d7d98b4920152978f6f0a66a4cef689a7983fd05c8b"}, ] [package.dependencies] @@ -11741,4 +11741,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.12,<3.14" -content-hash = "35d7c81745aee985169bd32fff6c02fbdb56d2902e630f89aa6e99f09f3c1c4e" +content-hash = "42d590343d06314e3e2b0b47c82eae65cd1255d1fed0b76a229c702add0a4f55" diff --git a/pyproject.toml b/pyproject.toml index 787a2548e3..2ebbe82485 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ prompt-toolkit = "^3.0.50" poetry = "^2.1.2" anyio = "4.9.0" pythonnet = "*" -fastmcp = "^2.3.3" +fastmcp = "^2.5.2" mcpm = "1.12.0" [tool.poetry.group.dev.dependencies]