Skip to content

[Bug]: OAuth refresh_token erased on every token refresh - MCP servers die ~1h after login #62333

Description

@rastolz

Bug Description

Any OAuth-authenticated MCP server (e.g. Zoho MCP) stops working ~1 hour after
hermes mcp login. The next tool call fails with "MCP OAuth ... no cached
tokens found; run hermes mcp login interactively first". Only a full
re-authorization recovers it. It affects every such server, including idle,
single-process installs — silently breaking remote MCP integrations for all
users.

Steps to Reproduce

  1. hermes mcp login <oauth-server> (e.g. a Zoho MCP server); complete the
    browser OAuth.
  2. Confirm it works: hermes mcp test <server> → Connected; and
    mcp-tokens/<server>.json contains a refresh_token.
  3. Force the access token to expire — either wait ~1h or edit the token file
    and set expires_at to a past timestamp.
  4. Trigger any tool call (e.g. hermes mcp test <server>), which forces a
    token refresh.
  5. Re-inspect mcp-tokens/<server>.json.

Expected Behavior

The refresh_token persists across refreshes; the server keeps refreshing and
working indefinitely.

Actual Behavior

After the first refresh, refresh_token is gone from the token file
(only access_token remains). At the next expiry can_refresh_token() is
False, so no refresh happens and the server dies until a full re-OAuth.

Affected Component

Other

Messaging Platform (if gateway-related)

No response

Debug Report

Report       https://paste.rs/hIytr
  agent.log    https://paste.rs/bi5fy
  gateway.log  https://paste.rs/K4jV6
  gui.log      https://paste.rs/0FCUp

Operating System

Ubuntu 24.04.4 LTS

Python Version

3.11.15 (inside the container; host is 3.12.3)

Hermes Version

Hermes Agent v0.18.0 (2026.7.1)

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

HermesTokenStorage.set_tokens (tools/mcp_oauth.py, ~L310) does
tokens.model_dump(mode="json", exclude_none=True) then overwrites the file.
Per RFC 6749 §6 a refresh response MAY omit refresh_token (= "reuse the
existing one"), which Zoho does; exclude_none=True drops the None
refresh_token, so the write erases the stored one.

Proposed Fix (optional)

Fix — carry it forward:

payload = tokens.model_dump(mode="json", exclude_none=True)
if not payload.get("refresh_token"):
    prev = _read_json(self._tokens_path())
    if prev and prev.get("refresh_token"):
        payload["refresh_token"] = prev["refresh_token"]

Verified: two consecutive forced-expiry refreshes now preserve the token.
Env: hermes-agent 0.18.0, [mcp] extra (mcp SDK 1.26.0), Linux/Docker, Zoho
MCP over Streamable HTTP.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/authAuthentication, OAuth, credential poolsbugcomp/toolsTool registry, model_tools, toolsetsduplicateThis issue or pull request already existstool/mcpMCP client and OAuthtype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions