Skip to content

🛂 fix: Respect requiresOAuth Config in User MCP Connections - #12511

Closed
danny-avila wants to merge 5 commits into
devfrom
claude/friendly-goldwasser
Closed

danny-avila wants to merge 5 commits into
devfrom
claude/friendly-goldwasser

Conversation

@danny-avila

@danny-avila danny-avila commented Apr 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Private MCP servers configured with requiresOAuth: false and OIDC token pass-through headers ({{LIBRECHAT_OPENID_ACCESS_TOKEN}}) were incorrectly triggering OAuth flows when the token expired (401 response). This happened because UserConnectionManager hardcoded useOAuth: true for all user connections, causing the OAuth machinery to intercept standard auth failures. The server would attempt OAuth client registration against a private endpoint with no OAuth support, resulting in a 404 and total connection failure.

  • Derive useOAuth from config.requiresOAuth and config.oauthMetadata in UserConnectionManager.createUserConnectionInternal(), aligning with the conditional logic already used in MCPManager.discoverServerTools().
  • Extract isOAuthServer() utility into utils.ts as a single source of truth for the OAuth check, replacing duplicated Boolean(config.requiresOAuth || config.oauthMetadata) in both MCPManager and UserConnectionManager.
  • Widen MCPConnectionFactory.create() signature to accept UserConnectionContext for non-OAuth user connections, since OAuth fields (flowManager, tokenMethods, etc.) are unnecessary when useOAuth is false.
  • Register a fallback oauthRequired handler on non-OAuth connections using connection.on (not once) so it survives all three retry attempts in connectTo(), immediately emitting oauthFailed to prevent a 120-second timeout hang.
  • Add UserMCPConnectionOptions type with optional OAuth-specific fields, so getUserConnection callers connecting to non-OAuth servers are not forced to supply flowManager/tokenMethods/signal.
  • Add explicit early throw when an OAuth server is missing flowManager, replacing a silent flowManager! non-null assertion that would crash deep in getOAuthTokens() with no actionable context.
  • Add test coverage verifying useOAuth derivation through getUserConnection, fallback handler behavior, listener cleanup, and the flowManager guard.

Change Type

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

Testing

  • Ran MCPConnectionFactory test suite (20 tests pass), including updated test for fallback handler using connection.on, cleanup verification, and removal of redundant assertion.
  • Ran MCPManager test suite (30 tests pass, up from 26), including new tests for:
    • Non-OAuth discoverServerTools path (requiresOAuth: false → no useOAuth: true)
    • getUserConnection with requiresOAuth: true → passes useOAuth: true to factory
    • getUserConnection with requiresOAuth: false → does NOT pass useOAuth: true
    • OAuth server missing flowManager → throws early with descriptive message
  • TypeScript compilation passes with no new errors.

Verification steps:

  1. Deploy updated LibreChat to dev.
  2. Connect as a user to a private MCP server (e.g., clickhouse-cloud with requiresOAuth: false) — should connect without OAuth prompts using OIDC token headers.
  3. Let the OIDC token expire — reconnection should propagate the auth error normally, NOT trigger OAuth client registration.
  4. Verify OAuth-based MCP servers (with requiresOAuth: true) still complete OAuth flows correctly.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

UserConnectionManager hardcoded useOAuth: true for all user connections,
causing non-OAuth servers (e.g., private MCP with OIDC token headers) to
incorrectly trigger OAuth flows on 401 responses.

- Derive useOAuth from config.requiresOAuth/config.oauthMetadata, matching
  the logic already used in MCPManager.discoverServerTools()
- Widen MCPConnectionFactory.create() to accept UserConnectionContext for
  non-OAuth user connections
- Register a fallback oauthRequired handler on non-OAuth connections that
  immediately emits oauthFailed, preventing a 120s timeout hang when the
  server returns 401 for expired tokens
Verify that MCPConnectionFactory registers a fallback handler on
non-OAuth connections that emits oauthFailed when oauthRequired fires.
Copilot AI review requested due to automatic review settings April 1, 2026 23:09

Copilot AI 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.

Pull request overview

This PR fixes user-scoped MCP connections incorrectly forcing OAuth behavior for servers configured as non-OAuth (requiresOAuth: false), which previously caused OAuth registration attempts and connection failures on standard 401 auth errors (e.g., expired pass-through OIDC tokens).

Changes:

  • Derives useOAuth for user connections from config.requiresOAuth || config.oauthMetadata in UserConnectionManager.
  • Broadens MCPConnectionFactory.create() to accept a lightweight UserConnectionContext when OAuth is not used.
  • Adds a non-OAuth oauthRequired fallback handler in MCPConnectionFactory and adds Jest coverage for that behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/api/src/mcp/UserConnectionManager.ts Stops hardcoding useOAuth: true; chooses OAuth vs non-OAuth options based on server config.
packages/api/src/mcp/MCPConnectionFactory.ts Accepts non-OAuth user context and adds a fallback oauthRequired listener for non-OAuth connections.
packages/api/src/mcp/tests/MCPConnectionFactory.test.ts Adds a unit test asserting the fallback handler is registered and emits oauthFailed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

);
connection.emit('oauthFailed', new Error('Server does not use OAuth'));
};
connection.once('oauthRequired', nonOAuthHandler);

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

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

The non-OAuth fallback handler is registered with once('oauthRequired', ...), but the factory’s connectTo() retries up to 3 times on failures when useOAuth is false. If the first attempt hits a 401/403, the once handler fires and is removed; a subsequent retry that triggers oauthRequired will have no listener and connectClient() will wait for oauthHandledPromise until timeout. Register this handler with on('oauthRequired', ...) (and clean it up afterwards), or alternatively ensure retries are skipped for 401/403 on non-OAuth connections so the handler isn’t needed more than once.

Suggested change
connection.once('oauthRequired', nonOAuthHandler);
connection.on('oauthRequired', nonOAuthHandler);

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +178
const oauthOptions: t.OAuthConnectionOptions | t.UserConnectionContext = useOAuth
? {
useOAuth: true as const,
user,
customUserVars,
flowManager: flowManager!,

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

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

flowManager is asserted with flowManager! when useOAuth is true. Because getConnection() accepts flowManager? (and some call sites are JS), it’s possible to reach this branch with flowManager undefined and get a runtime crash later (e.g., when loading tokens). Add an explicit validation when useOAuth is true (and ideally user.id), and throw a clear McpError/Error explaining that OAuth servers require a flow manager, instead of relying on a non-null assertion.

Suggested change
const oauthOptions: t.OAuthConnectionOptions | t.UserConnectionContext = useOAuth
? {
useOAuth: true as const,
user,
customUserVars,
flowManager: flowManager!,
if (useOAuth) {
if (!flowManager) {
throw new McpError(
ErrorCode.InvalidRequest,
'OAuth-enabled MCP servers require a flow manager to be provided.',
);
}
if (!user?.id) {
throw new McpError(
ErrorCode.InvalidRequest,
'OAuth-enabled MCP servers require a user with a valid id.',
);
}
}
const oauthOptions: t.OAuthConnectionOptions | t.UserConnectionContext = useOAuth
? {
useOAuth: true as const,
user,
customUserVars,
flowManager,

Copilot uses AI. Check for mistakes.
- Change connection.once to connection.on for the non-OAuth fallback
  handler so it survives retry attempts 2 and 3 in connectTo()
- Extract isOAuthServer() into utils.ts as a single source of truth
  for the requiresOAuth/oauthMetadata check, used by both MCPManager
  and UserConnectionManager
- Add explicit early throw when an OAuth server is missing flowManager,
  replacing the silent non-null assertion that would crash deep in
  getOAuthTokens with an undiagnosable error
- Add UserMCPConnectionOptions type with optional OAuth fields, so
  callers connecting to non-OAuth servers are not forced to supply
  flowManager/tokenMethods/signal they don't need
- Fix fallback handler test to use connection.on (not once), remove
  redundant assertion, and verify removeListener cleanup
- Add MCPManager test for non-OAuth discoverServerTools path
- Add getUserConnection tests verifying useOAuth is derived from
  config.requiresOAuth, not hardcoded
- Add test that OAuth server without flowManager throws early
@danny-avila
danny-avila marked this pull request as draft April 2, 2026 01:01
@danny-avila
danny-avila marked this pull request as draft April 2, 2026 01:01
@pnancarrow

Copy link
Copy Markdown
Contributor

@danny-avila you planning to merge this in? I am seeing what I think issue with an MCP server that takes a token as user input. The UI state is mixed “successfully initialized” then error.

I separately tried this locally with success:

In packages/api/src/mcp/UserConnectionManager.ts, changed hardcoded useOAuth: true to:

  • const useOAuth = Boolean(config.requiresOAuth || config.oauthMetadata);
  • pass useOAuth into MCPConnectionFactory.create(...)

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

ℹ️ 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

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #13254, which now folds in the policy-layer fix from this PR along with the proactive OAuth flow changes. The combined PR keeps requiresOAuth: false as the explicit opt-out, treats requiresOAuth: true and configured oauth as OAuth-enabled when unset, and has fresh passing CI plus a clean Codex review on the latest head.

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.

3 participants