Skip to content

fix(mcp): persist SDK-resolved client identity so cold-start refresh works - #1970

Merged
Aaronontheweb merged 3 commits into
netclaw-dev:devfrom
nixie-ai:fix/mcp-oauth-persist-client-identity
Aug 17, 2026
Merged

fix(mcp): persist SDK-resolved client identity so cold-start refresh works#1970
Aaronontheweb merged 3 commits into
netclaw-dev:devfrom
nixie-ai:fix/mcp-oauth-persist-client-identity

Conversation

@nixie-ai

@nixie-ai nixie-ai commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Problem

The persisted McpOAuthTokens record could end up with a null ClientId even when the SDK had resolved one internally (own dynamic registration or a client-metadata document), because CreateReplacement only ever wrote identity.ClientId — never tokens.ClientId. On restart CreateTokenCache then rebuilt an empty identity (DCR=false records were skipped entirely), so SDK 2.0's refresh gate (CachedTokensMatchClientCredentials) never matched and every token expiry fell through to interactive auth.

This produced the recurring AuthorizationCallbackHandler returned a null authorization result loop that blocked the Immovlan nightly Jira PR review for a week (2026-08-08 → 08-15). Live diagnosis via read-only probe (private tool, not in this PR):

clientId (prefix) = <null>
dynamicClientRegistration = False
authorizationServer = https://mcp.atlassian.com
tokenEndpointAuthMethod = client_secret_post
expiresAt = 2026-08-13T15:02:48  (auth failed 15:03:25 — exact match)

Change

  • CreateReplacement falls back to the SDK-reported TokenContainer client id/secret when Netclaw's own identity is absent, and marks the record dynamic when the SDK supplied the id.
  • CreateTokenCache rebuilds the provider identity from any persisted ClientId, not only records flagged DynamicClientRegistration.

Companion diagnostics PR: #1969 (log OAuth refresh failure diagnostics on auth-loss).

Verification

Diagnostic probe confirmed the missing identity; the fix aligns write and read paths so the persisted record satisfies the SDK 2.0 refresh gate across daemon restarts. (Full daemon build of this snapshot is blocked by unrelated pre-existing ShellSyntaxTree restore errors in Netclaw.Security on this workspace; the change is isolated to McpOAuthCredentialStore.)

…works

The persisted McpOAuthTokens record could end up with a null ClientId even
when the SDK had resolved one internally (own dynamic registration or a
client-metadata document), because CreateReplacement only ever wrote
identity.ClientId — never tokens.ClientId. On restart CreateTokenCache then
rebuilt an empty identity (DCR=false records were skipped entirely), so
SDK 2.0's refresh gate (CachedTokensMatchClientCredentials) never matched
and every token expiry fell through to interactive auth: the recurring
'AuthorizationCallbackHandler returned a null authorization result' loop
that blocked the Immovlan nightly Jira PR review for a week.

Two-part fix:
- CreateReplacement falls back to the SDK-reported TokenContainer client
  id/secret when Netclaw's own identity is absent, and marks the record
  dynamic when the SDK supplied the id.
- CreateTokenCache rebuilds the provider identity from any persisted
  ClientId, not only records flagged DynamicClientRegistration.

Repro: McpOAuthTokens record for atlassian showed clientId=null,
dynamicClientRegistration=false, authorizationServer=...mcp.atlassian.com,
tokenEndpointAuthMethod=client_secret_post, refresh token present, expiry
matching the observed 15:03 auth loss exactly.
Aaronontheweb added a commit to Aaronontheweb/netclaw that referenced this pull request Aug 17, 2026
PR netclaw-dev#1970 changes only in-memory identity fallback logic and adds no
logging, so it does not introduce a leak on its own. But an audit for
the same issue found McpOAuthCredentialStore has the same shape of
gap: it also logs a raw exception in its credentials-file load path.
The secrets file is decrypted before that callback runs, so a
malformed-JSON or decryption exception could in principle echo a
fragment of plaintext credential content into the log.

Move RedactForLogging out of McpClientManager and into
SecretOutputRedactor, so both McpClientManager and
McpOAuthCredentialStore route their exception logging through the
same shared helper.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

// own dynamic registration persist the SDK-resolved client id without the DCR
// marker; discarding it on restart made SDK 2.0 skip the refresh path
// entirely ("null authorization result" on every later expiry).
identity = new McpOAuthClientIdentity(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM and nice catch lol

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) August 17, 2026 21:55
@Aaronontheweb Aaronontheweb added mcp Model context protocol server / client issues. bug Something isn't working labels Aug 17, 2026
@Aaronontheweb
Aaronontheweb disabled auto-merge August 17, 2026 23:42
@Aaronontheweb
Aaronontheweb merged commit 5ac1047 into netclaw-dev:dev Aug 17, 2026
17 checks passed
@Aaronontheweb Aaronontheweb mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mcp Model context protocol server / client issues.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants