Skip to content

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

Draft
danny-avila wants to merge 5 commits intodevfrom
claude/friendly-goldwasser
Draft

🛂 fix: Respect requiresOAuth Config in User MCP Connections#12511
danny-avila wants to merge 5 commits intodevfrom
claude/friendly-goldwasser

Conversation

@danny-avila
Copy link
Copy Markdown
Owner

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

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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);
Copy link

Copilot AI Apr 1, 2026

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!,
Copy link

Copilot AI Apr 1, 2026

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

2 participants