diff --git a/tools/mcp_oauth.py b/tools/mcp_oauth.py index fe5e07d7ecfe..225e21713170 100644 --- a/tools/mcp_oauth.py +++ b/tools/mcp_oauth.py @@ -186,8 +186,10 @@ def _serve(): code = result["auth_code"] or "" state = result["state"] if not code: - print(" Browser callback timed out. Paste the authorization code manually:") - code = input(" Code: ").strip() + raise TimeoutError( + "OAuth callback timed out — no authorization code received. " + "Re-authenticate with: hermes mcp auth " + ) return code, state @@ -244,6 +246,23 @@ def build_oauth_auth(server_name: str, server_url: str): ) +async def has_valid_token(server_name: str) -> bool: + """Return True if a non-expired OAuth token exists in storage for this server. + + Used at startup to skip interactive browser flows when no token is cached. + A token with no expiry information is assumed valid (server will reject it + if it has actually expired, which will surface as a connection error). + """ + storage = HermesTokenStorage(server_name) + tokens = await storage.get_tokens() + if tokens is None: + return False + if hasattr(tokens, "expires_at") and tokens.expires_at: + import time + return float(tokens.expires_at) > time.time() + 30 # 30s safety buffer + return True + + def remove_oauth_tokens(server_name: str) -> None: """Delete stored OAuth tokens and client info for a server.""" HermesTokenStorage(server_name).remove() diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index 32da32476868..2513cacfaacf 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -752,14 +752,23 @@ async def _run_http(self, config: dict): headers = dict(config.get("headers") or {}) connect_timeout = config.get("connect_timeout", _DEFAULT_CONNECT_TIMEOUT) - # OAuth 2.1 PKCE: build httpx.Auth handler using the MCP SDK + # OAuth 2.1 PKCE: build httpx.Auth handler using the MCP SDK. + # Only proceed if a valid cached token exists — initiating an interactive + # browser flow during startup would block Hermes for up to 120 seconds + # and deadlock the MCP event loop if the user is not present. _oauth_auth = None if self._auth_type == "oauth": try: - from tools.mcp_oauth import build_oauth_auth + from tools.mcp_oauth import build_oauth_auth, has_valid_token + if not await has_valid_token(self.name): + raise RuntimeError( + f"MCP server '{self.name}' requires OAuth but no valid token is " + f"cached. Authenticate first with: hermes mcp auth {self.name}" + ) _oauth_auth = build_oauth_auth(self.name, url) except Exception as exc: logger.warning("MCP OAuth setup failed for '%s': %s", self.name, exc) + raise sampling_kwargs = self._sampling.session_kwargs() if self._sampling else {} _http_kwargs: dict = {