Skip to content

fix(core): import V1 MCP OAuth credentials on upgrade - #55989

Open
neriousy wants to merge 2 commits into
devfrom
mcp-auth-migration
Open

neriousy wants to merge 2 commits into
devfrom
mcp-auth-migration

Conversation

@neriousy

Copy link
Copy Markdown
Member

Issue for this PR

Closes #54879
Closes #55964

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

V1 stored remote MCP OAuth tokens in mcp-auth.json, one entry per server name. V2 looks up an MCP credential in the credential table under mcp_<sha1(name + "\0" + url)>, and nothing ever copies the V1 file there. The V1 provider keys in auth.json are imported by 20260805200742_import_legacy_credentials, but there is no equivalent for mcp-auth.json. So connectProvider finds no row, falls back to an empty store, and every V1 OAuth server shows needs authentication after the upgrade.

This adds a migration, 20261016120000_import_legacy_mcp_credentials, that reads mcp-auth.json and writes one OAuth credential for each usable entry:

  • The integration id comes from a new McpIdentity.integrationID(name, url), which the MCP layer now uses too, so the two can't drift. The derivation itself is unchanged, so existing V2 credentials keep their ids.
  • Tokens are converted to V2's credential shape. V1's expiresAt is in seconds (and often fractional); V2 stores whole milliseconds, with 0 meaning no expiry.
  • V1's registered client (clientId, secret, issued/expiry times) is kept in metadata.client. Refresh tokens are bound to the client that obtained them, so refreshing against a new registration would fail.
  • Entries without serverUrl are skipped, because V1 only used an entry when its serverUrl matched the configured URL. Entries without tokens are skipped because they are unfinished logins.
  • If the server already has a credential (for example, the user signed in again on V2), that credential is kept.
  • A missing, corrupt, or non-object file is skipped instead of failing. A failed migration keeps the database from opening, and V1 itself treated an unreadable file as empty.
  • The source file is not modified.

Relation to #55197: that PR takes the same approach, and I checked its diff while working on this. I traced the problem and the fix separately against dev. The differences here are a namespaced identity module that follows the core self-export pattern, keeping V1's client issued/expiry metadata, and an end-to-end check against a mock server. Either PR fixes the bug, so whichever one lands, the other can be closed.

How did you verify your code works?

  • New tests in packages/core/test/database-migration.test.ts:
    • Integration ids are pinned to literal hashes computed with the original inline formula, so changing the derivation breaks the test.
    • Expiry conversion from fractional seconds to milliseconds.
    • Client metadata is preserved.
    • An existing V2 credential takes priority over the V1 entry.
    • Entries without tokens or without a URL, and invalid entries, are dropped.
    • Running the import twice adds nothing.
    • The source file is unchanged.
    • Missing, corrupt, and non-object files are skipped.
    • The imported row is decoded through McpOAuth.toTokens and McpOAuth.clientFromCredential, the same calls the connect-time provider makes.
  • End-to-end, from source, in an isolated home and service: I bootstrapped a database, removed this migration's row to simulate an older database, added a V1 mcp-auth.json, and ran opencode mcp list against a mock OAuth MCP server that only accepts the V1 bearer token.
    • On origin/dev: ⚠ work needs authentication, no credential row, and no request carried the token.
    • On this branch: ✓ work connected, and the mock received the V1 bearer token.
  • bun test for database-migration, mcp, mcp-oauth, and mcp-import-boundary in packages/core: 110 pass.
  • bun run script/migration.ts --check passes, and bun run check passes.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good. I ran the #54879 reproduction from source in both checkouts: a real V1 data dir with a V1 mcp-auth.json, and a mock OAuth MCP server that only accepts the V1 bearer token. On the base the server shows needs authentication, no MCP credential is imported, and no request carries the token. With this PR an OAuth credential row is created under the same integration id the MCP layer looks up, mcp list shows connected, and every request carries the V1 access token. The migration tests and core typecheck pass. The one failing MCP test (stdio descendant termination) also fails on the base.

Public API unchanged: this adds an internal McpIdentity module and a database migration, and only reads the V1 mcp-auth.json.

No blocking concerns. The import mirrors the existing auth.json migration, and the id derivation is unchanged, so existing V2 credentials keep their ids.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant