Skip to content

🧹 fix: Graceful MCP OAuth Revoke Cleanup When Tokens Are Missing - #12825

Merged
danny-avila merged 2 commits into
LibreChat-AI:devfrom
gaurav0107:fix/12754-mcp-oauth-revoke-cleanup
Apr 29, 2026
Merged

danny-avila merged 2 commits into
LibreChat-AI:devfrom
gaurav0107:fix/12754-mcp-oauth-revoke-cleanup

Conversation

@gaurav0107

@gaurav0107 gaurav0107 commented Apr 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #12754.

maybeUninstallOAuthMCP in api/server/controllers/UserController.js currently aborts before its two cleanup steps (DB token delete + OAuth flow-state cleanup) whenever MCPTokenStorage.getTokens throws ReauthenticationRequiredError. That error path fires precisely when a user clicks Revoke on an MCP server whose refresh token is gone — which is exactly the case where the cleanup needs to succeed. The result today is a red log line plus a leaked token row and leaked flow state.

This PR wraps the getTokens call in a try/catch, mirroring the best-effort pattern already used for the two revokeOAuthToken calls further down in the same function:

  • ReauthenticationRequiredError (matched by error?.name to avoid a cross-package import cycle): log info, skip revocation, continue to cleanup.
  • Any other error: log warn, skip revocation, continue to cleanup.

Because tokens?.access_token and tokens?.refresh_token already use optional chaining, a null tokens requires no other change — the revoke branches are simply skipped.

maybeUninstallOAuthMCP is also added to module.exports so it can be exercised directly from a unit test.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Added api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js (8 cases):

  1. non-MCP plugin key → early return, no cleanup
  2. MCP server is not OAuth-enabled → early return
  3. client info missing → early return
  4. happy path → both tokens revoked, then cleanup runs
  5. getTokens throws ReauthenticationRequiredError → revocation skipped, cleanup runs, info logged
  6. getTokens throws arbitrary error → revocation skipped, cleanup runs, warn logged
  7. only access token present → single revocation + cleanup
  8. both revoke calls fail → cleanup still runs
$ cd api && npx jest __tests__/maybeUninstallOAuthMCP.spec.js
Test Suites: 1 passed, 1 total
Tests:       8 passed, 8 total

Pre-existing unrelated failures on upstream/main (4 suites: strategies/appleStrategy, server/routes/__tests__/mcp, db/utils, server/index) were confirmed to be independent of this change by running them on a clean checkout of upstream/main without the patch applied.

Test Configuration

  • Node v25.9.0 (within the supported range documented in CONTRIBUTING.md)
  • npm install from repo root; npm run build:data-provider, build:data-schemas, build:api
  • Run: cd api && npx jest __tests__/maybeUninstallOAuthMCP.spec.js

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • I have made pertinent documentation changes — no user-facing behavior change
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective (8 new Jest cases)
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules
  • A pull request for updating the documentation has been submitted — N/A

…Chat-AI#12754)

`maybeUninstallOAuthMCP` in `api/server/controllers/UserController.js`
aborts before the DB-delete and flow-state cleanup steps whenever
`MCPTokenStorage.getTokens` throws `ReauthenticationRequiredError` —
which is exactly what happens when a user clicks "Revoke" on an MCP
server whose backend is already dead and whose refresh token is gone.
The resulting error is both surfaced to the log as a red line and, more
importantly, leaks the DB token row and OAuth flow state.

Wrap the token retrieval in try/catch following the same best-effort
pattern already used for the two `revokeOAuthToken` calls. On
`ReauthenticationRequiredError`, skip revocation silently (info log)
and continue to the cleanup steps. On any other unexpected error, log
a warning and continue — cleanup must always run.

Exported `maybeUninstallOAuthMCP` for direct unit testing and added
`api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js` with
8 cases: early-return guards (non-MCP key, non-OAuth server, missing
client info), happy path (both tokens revoked + cleanup), both
failure-to-retrieve paths (ReauthenticationRequiredError and arbitrary
error — cleanup still runs in both), single-token path, and
revocation-call failures (cleanup still runs).

Fixes LibreChat-AI#12754.
Follow-up to the previous commit on this branch. Two changes:

1. `UserController.maybeUninstallOAuthMCP` now checks
   `error instanceof ReauthenticationRequiredError` using the real
   class imported from `@librechat/api`, instead of comparing
   `error?.name === 'ReauthenticationRequiredError'`. The name-string
   check matched any unrelated error that happened to have the same
   `.name`; the `instanceof` check is a proper identity test.

2. The accompanying spec's jest mock for `@librechat/api` now
   exposes a `ReauthenticationRequiredError` class, and the test
   imports it from that mock so the `instanceof` comparison in the
   production code holds during the test. Without this, the two
   "skips revocation ... still runs cleanup" tests threw
   `TypeError: Right-hand side of 'instanceof' is not an object`
   because the mock left the class undefined.

All 8 tests in the spec pass.
@gaurav0107
gaurav0107 marked this pull request as ready for review April 25, 2026 14:59
@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila danny-avila changed the title fix: graceful MCP OAuth revoke cleanup when tokens are missing (#12754) 🧹 fix: Graceful MCP OAuth Revoke Cleanup When Tokens Are Missing Apr 29, 2026
@danny-avila
danny-avila changed the base branch from main to dev April 29, 2026 00:11
@danny-avila
danny-avila merged commit 8404343 into LibreChat-AI:dev Apr 29, 2026
11 checks passed
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request May 3, 2026
…reChat-AI#12825)

* fix: graceful MCP OAuth revoke cleanup when tokens are missing (LibreChat-AI#12754)

`maybeUninstallOAuthMCP` in `api/server/controllers/UserController.js`
aborts before the DB-delete and flow-state cleanup steps whenever
`MCPTokenStorage.getTokens` throws `ReauthenticationRequiredError` —
which is exactly what happens when a user clicks "Revoke" on an MCP
server whose backend is already dead and whose refresh token is gone.
The resulting error is both surfaced to the log as a red line and, more
importantly, leaks the DB token row and OAuth flow state.

Wrap the token retrieval in try/catch following the same best-effort
pattern already used for the two `revokeOAuthToken` calls. On
`ReauthenticationRequiredError`, skip revocation silently (info log)
and continue to the cleanup steps. On any other unexpected error, log
a warning and continue — cleanup must always run.

Exported `maybeUninstallOAuthMCP` for direct unit testing and added
`api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js` with
8 cases: early-return guards (non-MCP key, non-OAuth server, missing
client info), happy path (both tokens revoked + cleanup), both
failure-to-retrieve paths (ReauthenticationRequiredError and arbitrary
error — cleanup still runs in both), single-token path, and
revocation-call failures (cleanup still runs).

Fixes LibreChat-AI#12754.

* test: use instanceof check against real ReauthenticationRequiredError

Follow-up to the previous commit on this branch. Two changes:

1. `UserController.maybeUninstallOAuthMCP` now checks
   `error instanceof ReauthenticationRequiredError` using the real
   class imported from `@librechat/api`, instead of comparing
   `error?.name === 'ReauthenticationRequiredError'`. The name-string
   check matched any unrelated error that happened to have the same
   `.name`; the `instanceof` check is a proper identity test.

2. The accompanying spec's jest mock for `@librechat/api` now
   exposes a `ReauthenticationRequiredError` class, and the test
   imports it from that mock so the `instanceof` comparison in the
   production code holds during the test. Without this, the two
   "skips revocation ... still runs cleanup" tests threw
   `TypeError: Right-hand side of 'instanceof' is not an object`
   because the mock left the class undefined.

All 8 tests in the spec pass.
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
…reChat-AI#12825)

* fix: graceful MCP OAuth revoke cleanup when tokens are missing (LibreChat-AI#12754)

`maybeUninstallOAuthMCP` in `api/server/controllers/UserController.js`
aborts before the DB-delete and flow-state cleanup steps whenever
`MCPTokenStorage.getTokens` throws `ReauthenticationRequiredError` —
which is exactly what happens when a user clicks "Revoke" on an MCP
server whose backend is already dead and whose refresh token is gone.
The resulting error is both surfaced to the log as a red line and, more
importantly, leaks the DB token row and OAuth flow state.

Wrap the token retrieval in try/catch following the same best-effort
pattern already used for the two `revokeOAuthToken` calls. On
`ReauthenticationRequiredError`, skip revocation silently (info log)
and continue to the cleanup steps. On any other unexpected error, log
a warning and continue — cleanup must always run.

Exported `maybeUninstallOAuthMCP` for direct unit testing and added
`api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js` with
8 cases: early-return guards (non-MCP key, non-OAuth server, missing
client info), happy path (both tokens revoked + cleanup), both
failure-to-retrieve paths (ReauthenticationRequiredError and arbitrary
error — cleanup still runs in both), single-token path, and
revocation-call failures (cleanup still runs).

Fixes LibreChat-AI#12754.

* test: use instanceof check against real ReauthenticationRequiredError

Follow-up to the previous commit on this branch. Two changes:

1. `UserController.maybeUninstallOAuthMCP` now checks
   `error instanceof ReauthenticationRequiredError` using the real
   class imported from `@librechat/api`, instead of comparing
   `error?.name === 'ReauthenticationRequiredError'`. The name-string
   check matched any unrelated error that happened to have the same
   `.name`; the `instanceof` check is a proper identity test.

2. The accompanying spec's jest mock for `@librechat/api` now
   exposes a `ReauthenticationRequiredError` class, and the test
   imports it from that mock so the `instanceof` comparison in the
   production code holds during the test. Without this, the two
   "skips revocation ... still runs cleanup" tests threw
   `TypeError: Right-hand side of 'instanceof' is not an object`
   because the mock left the class undefined.

All 8 tests in the spec pass.
ThomasVuNguyen pushed a commit to ThomasVuNguyen/LibreChat that referenced this pull request Jul 15, 2026
…reChat-AI#12825)

* fix: graceful MCP OAuth revoke cleanup when tokens are missing (LibreChat-AI#12754)

`maybeUninstallOAuthMCP` in `api/server/controllers/UserController.js`
aborts before the DB-delete and flow-state cleanup steps whenever
`MCPTokenStorage.getTokens` throws `ReauthenticationRequiredError` —
which is exactly what happens when a user clicks "Revoke" on an MCP
server whose backend is already dead and whose refresh token is gone.
The resulting error is both surfaced to the log as a red line and, more
importantly, leaks the DB token row and OAuth flow state.

Wrap the token retrieval in try/catch following the same best-effort
pattern already used for the two `revokeOAuthToken` calls. On
`ReauthenticationRequiredError`, skip revocation silently (info log)
and continue to the cleanup steps. On any other unexpected error, log
a warning and continue — cleanup must always run.

Exported `maybeUninstallOAuthMCP` for direct unit testing and added
`api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js` with
8 cases: early-return guards (non-MCP key, non-OAuth server, missing
client info), happy path (both tokens revoked + cleanup), both
failure-to-retrieve paths (ReauthenticationRequiredError and arbitrary
error — cleanup still runs in both), single-token path, and
revocation-call failures (cleanup still runs).

Fixes LibreChat-AI#12754.

* test: use instanceof check against real ReauthenticationRequiredError

Follow-up to the previous commit on this branch. Two changes:

1. `UserController.maybeUninstallOAuthMCP` now checks
   `error instanceof ReauthenticationRequiredError` using the real
   class imported from `@librechat/api`, instead of comparing
   `error?.name === 'ReauthenticationRequiredError'`. The name-string
   check matched any unrelated error that happened to have the same
   `.name`; the `instanceof` check is a proper identity test.

2. The accompanying spec's jest mock for `@librechat/api` now
   exposes a `ReauthenticationRequiredError` class, and the test
   imports it from that mock so the `instanceof` comparison in the
   production code holds during the test. Without this, the two
   "skips revocation ... still runs cleanup" tests threw
   `TypeError: Right-hand side of 'instanceof' is not an object`
   because the mock left the class undefined.

All 8 tests in the spec pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bug in token cleanup logic for deleted MCP servers

2 participants