Skip to content
Closed
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
23 changes: 21 additions & 2 deletions tools/mcp_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <server-name>"
)
return code, state


Expand Down Expand Up @@ -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()
13 changes: 11 additions & 2 deletions tools/mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading