🔐 fix: Handle Multiple Concurrent MCP OAuth Login Prompts (#13200)

* fix: handle multiple MCP OAuth prompts

* fix: address MCP OAuth review feedback

* fix: address MCP OAuth prompt lifecycle review

* fix: narrow OAuth prompt slot cleanup

* fix: format OAuth prompt test

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
janluedemann-esome 2026-06-05 23:18:24 +02:00 committed by GitHub
parent aeb5adff34
commit 2ed59ac98a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 436 additions and 32 deletions

View file

@ -596,11 +596,15 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
const flowManager = getFlowStateManager(flowsCache);
const configServers = await resolveConfigServers(req);
const pendingOAuthServers = new Set();
const oauthToolCallIds = new Map();
const oauthStepIndexes = new Map();
const createOAuthEmitter = (serverName) => {
const createOAuthEmitter = (serverName, index) => {
return async (authURL) => {
const flowId = `${req.user.id}:${serverName}:${Date.now()}`;
const stepId = 'step_oauth_login_' + serverName;
oauthToolCallIds.set(serverName, flowId);
oauthStepIndexes.set(serverName, index);
const toolCall = {
id: flowId,
name: buildOAuthToolCallName(serverName),
@ -611,7 +615,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
runId: Constants.USE_PRELIM_RESPONSE_MESSAGE_ID,
id: stepId,
type: StepTypes.TOOL_CALLS,
index: 0,
index,
stepDetails: {
type: StepTypes.TOOL_CALLS,
tool_calls: [toolCall],
@ -645,6 +649,40 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
};
};
const createOAuthEndEmitter = (serverName) => {
return async () => {
const stepId = 'step_oauth_login_' + serverName;
const toolCall = {
id: oauthToolCallIds.get(serverName),
name: buildOAuthToolCallName(serverName),
args: '',
output: 'OAuth authentication completed',
type: 'tool_call',
};
const runStepCompletedEvent = {
event: GraphEvents.ON_RUN_STEP_COMPLETED,
data: {
result: {
id: stepId,
index: oauthStepIndexes.get(serverName) ?? 0,
tool_call: toolCall,
},
},
};
if (streamId) {
await GenerationJobManager.emitChunk(streamId, runStepCompletedEvent);
} else if (res && !res.writableEnded) {
sendEvent(res, runStepCompletedEvent);
} else {
logger.warn(
`[Tool Definitions] Cannot emit OAuth completion for ${serverName}: no streamId and res not available`,
);
}
};
};
const getOrFetchMCPServerTools = async (userId, serverName) => {
let serverConfig;
try {
@ -781,7 +819,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
`[Tool Definitions] OAuth required for ${serverNames.length} server(s): ${serverNames.join(', ')}. Emitting events and waiting.`,
);
const oauthWaitPromises = serverNames.map(async (serverName) => {
const oauthWaitPromises = serverNames.map(async (serverName, index) => {
try {
const result = await reinitMCPServer({
user: req.user,
@ -790,7 +828,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
userMCPAuthMap,
flowManager,
returnOnOAuth: false,
oauthStart: createOAuthEmitter(serverName),
oauthStart: createOAuthEmitter(serverName, index),
oauthEnd: createOAuthEndEmitter(serverName),
connectionTimeout: Time.TWO_MINUTES,
});