Skip to content

[Bug] agy CLI clears mcp_oauth_tokens.json on every launch in WSL #348

@Xendomn

Description

@Xendomn

Description

Every time agy CLI is restarted in WSL, the token storage file ~/.gemini/antigravity-cli/mcp_oauth_tokens.json is cleared to {}. This forces the user to re-authenticate with Notion and other MCP Servers on every single launch.

Root Cause

When the MCP manager attempts to connect using the stored token and receives a 400 Bad Request from the Notion server during the notifications/roots/list_changed message phase, connector.go treats ANY error as token invalidation and calls tokenStore.removeToken(serverURL).

Complete Analysis Report

Please see the pasted bug analysis below for the detailed call stack, timeline logs, and recommended fix.

Bug Analysis Report: agy Clears MCP OAuth Token File on Every Restart

Caution

This bug has been confirmed across 8 separate session logs. Every time agy CLI is launched (with a Notion MCP server configured), the local OAuth credentials file is completely cleared.

Issue Overview

Whenever agy CLI restarts in WSL (Windows Subsystem for Linux), the token storage file ~/.gemini/antigravity-cli/mcp_oauth_tokens.json is cleared to {}. This results in the loss of the OAuth token for the Notion MCP Server, forcing the user to re-authenticate on every launch.


Key Evidence

1. Token File Status

File Size Modification Time Content
mcp_oauth_tokens.json 2 bytes 21:00:09 (After new session startup) {} (Cleared)
mcp_oauth_tokens.json.back 457 bytes 20:59:55 (At the end of previous session) Contains valid Notion OAuth Token

2. Reproduction History (All Confirmed Sessions)

Session Time Error Message Result
2026-06-08 21:58 Connection with stored token failed for notion: ... Bad Request Token cleared
2026-06-08 22:10 Connection with stored token failed for notion: ... Bad Request Token cleared
2026-06-09 19:39 Connection with stored token failed for notion: ... Bad Request Token cleared
2026-06-09 20:30 Connection with stored token failed for notion: ... Bad Request Token cleared
2026-06-09 20:40 Connection with stored token failed for notion: ... Bad Request Token cleared
2026-06-09 20:50 Connection with stored token failed for notion: ... Bad Request Token cleared
2026-06-09 20:53 Connection with stored token failed for notion: ... Bad Request Token cleared
2026-06-09 21:00 Connection with stored token failed for notion: ... Bad Request Token cleared

100% Reproduction Rate: All agy sessions configured with the Notion MCP server trigger this bug on startup.

3. Precise Timeline (Extracted from Logs)

21:00:07.978891  agy starts, initializing servers
21:00:07.979817  token_storage.go:55] Using file-based token storage because WSL environment detected
21:00:08.600409  auth.go:114] ChainedAuth: authenticated via keyring (effective: keyring)
21:00:08.600449  server_oauth.go] OAuth: authenticated successfully as xendomn@gmail.com

--- Token file modified (cleared) at 21:00:09.206907 ---

21:00:09.207472  connector.go:190] Connection with stored token failed for notion:
                                   calling "initialize": sending "notifications/roots/list_changed": Bad Request

Important

The modification timestamp of the token file (21:00:09.206) is almost identical to the timestamp of the warning log in connector.go:190 (21:00:09.207). This 1ms interval demonstrates that the connection failure immediately triggers the token deletion logic in connector.go.


Two Distinct MCP Failure Modes

Analyzing the logs reveals two completely different MCP failure patterns:

Pattern HTTP Status Trigger Phase Behavior
Startup Connection 400 Bad Request Sending notifications/roots/list_changed Clears local Token (BUG)
Active Session 401 Unauthorized Calling tools/call or initialize Triggers Re-authentication (Normal)

While 401 Unauthorized is the correct indicator of token expiration, 400 Bad Request (which is typically a protocol or server-side error) is being incorrectly treated as a token invalidation trigger.


Root Cause: Treating "Bad Request" as Token Invalidation

Code Call Hierarchy

Reconstructing the Go package symbols from the binary PCLN tables reveals the following call stack:

mcp.(*McpManager).startSession()
  └── connector.go: Attempt to connect using the stored token
        └── Send MCP initialization handshake ("notifications/roots/list_changed")
              └── Notion Server responds: 400 Bad Request
                    └── connector.go:190: Warning log "Connection with stored token failed for %s: %v"
                          └── Call mcp.(*tokenStore).removeToken()  <- THE BUG!
                                └── mcp.(*tokenStore).writeCredentials()
                                      └── Writes empty map {} to mcp_oauth_tokens.json

Core Functions (Extracted from Binary Symbol Tables)

// mcp_auth.go: Token Storage Management
type tokenStore struct { ... }
func (ts *tokenStore) findCredentials(serverURL string) *credentials
func (ts *tokenStore) saveCredentials(serverURL string, creds *credentials)
func (ts *tokenStore) removeToken(serverURL string)    // <- Bug occurs here
func (ts *tokenStore) loadCredentials() map[string]*credentials
func (ts *tokenStore) writeCredentials(store map[string]*credentials) // <- Writes empty map {} to file

// connector.go: MCP Connector
// StreamableHTTPConnector connects to HTTP MCP Servers (used by Notion)
func (c *StreamableHTTPConnector) Connect(ctx, serverURL, token) error

Bug Details

Inside connector.go, the connection failure error-handling logic behaves as follows:

// connector.go (Reconstructed logic)
func (c *StreamableHTTPConnector) Connect(ctx, ...) error {
    if token := tokenStore.findCredentials(serverURL); token != nil {
        // Attempt connection with saved token
        err := c.tryConnectWithToken(ctx, serverURL, token)
        if err != nil {
            // BUG: Treating any error (including Bad Request) as token invalidation
            log.Warningf("connector.go:190] Connection with stored token failed for %s: %v", serverName, err)
            tokenStore.removeToken(serverURL)  // Erroneously clears token!
            // Fall back to new OAuth authentication flow
            return c.initiateOAuthFlow(ctx, serverURL)
        }
    }
}

Why is this a bug?

The Notion MCP Server returns 400 Bad Request when receiving the notifications/roots/list_changed notification. This error is not a token invalidation issue; instead, it is caused by:

  1. MCP Protocol Mismatch: The Notion server might not support or expect this specific protocol notification.
  2. Transient Server-Side Issue: A temporary issue on Notion's servers.
  3. Workspace Roots Change: The agy CLI was started from a different directory (e.g., /home/siubing instead of /home/siubing/code/python/mcp_test), resulting in a different workspace layout that the server rejected.

The error handler in connector.go fails to differentiate between 401 Unauthorized and other HTTP errors, resulting in removeToken() being called on any initialization warning.


WSL Environment Factors

token_storage.go:55] Using file-based token storage because WSL environment detected

In a WSL environment, agy falls back to file-based storage (mcp_oauth_tokens.json) instead of the system keyring. As a result, the removeToken() operation directly rewrites the local configuration file with an empty map ({}), making the token loss immediately visible on disk.


Complete Trigger Chain

[User starts agy CLI]
      |
      v
[WSL environment detected -> agy falls back to file-based token storage]
      |
      v
[User authenticates via ChainedAuth (xendomn@gmail.com)]
      |
      v
[MCP Manager loads Notion MCP configuration]
      |
      v
[McpManager.startSession() for "notion"]
      |
      v
[StreamableHTTPConnector.Connect()]
      |
      +-- tokenStore.findCredentials("https://mcp.notion.com/mcp") -> Finds saved token
      |
      v
[Initiate connection with stored token]
      +-- Send MCP "initialize" request (Succeeds)
      +-- Send "notifications/roots/list_changed" notification
      |     └── Notion Server returns: 400 Bad Request <- Failure point
      |
      v
[connector.go:190 logs warning: "Connection with stored token failed for notion"]
      |
      v
[BUG: tokenStore.removeToken("https://mcp.notion.com/mcp")]
      |
      v
[tokenStore.writeCredentials({}) -> Writes empty map to mcp_oauth_tokens.json]
      |
      v
[Token file is wiped -> contains only "{}"]
      |
      v
[Connector triggers new OAuth login flow, forcing user to re-authenticate]

Recommended Fixes

Option 1: Differentiate Connection Errors (Recommended)

In connector.go, only trigger removeToken() if the error is explicitly an authentication/authorization failure (e.g., 401 Unauthorized):

// Proposed Fix
if isUnauthorized(err) {
    // Clear token only if it has actually expired or is invalid
    tokenStore.removeToken(serverURL)
    return c.initiateOAuthFlow(ctx, serverURL)
} else {
    // For other transport/protocol errors (e.g., Bad Request), preserve the token
    log.Warningf("MCP connection failed (token preserved): %v", err)
    return err
}

Option 2: Ignore Errors on Non-Critical Notifications

Failure to deliver the notifications/roots/list_changed notification should not invalidate the entire session credentials. These secondary notifications should be logged as warnings without discarding the token.

Option 3: Implement Token Backup

Create a backup of the token before discarding it:

os.Copy(tokenPath, tokenPath+".back")
tokenStore.removeToken(serverURL)

Summary

Metric Details
Bug Location third_party/jetski/cortex/utils/mcp/connector.go:190
Trigger Event MCP Server responds with 400 Bad Request during notifications/roots/list_changed
Buggy Logic Any connection/initialization error triggers removeToken()
Impact mcp_oauth_tokens.json is overwritten with {} on every CLI restart
Root Cause Missing validation of error type (401 Unauthorized vs other status codes)
Workaround Restore the token file using cp mcp_oauth_tokens.json.back mcp_oauth_tokens.json

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions