🛂 fix: Preserve OpenID Re-Auth Errors Through MCP Tool Error Classification (#15010)

The MCP tool-call catch block classifies errors by message substring, so an OpenIDReauthRequiredError raised during header resolution was rewritten into an MCP OAuth configuration prompt and its class identity discarded. The typed error now passes through ahead of the heuristic, so the actionable re-authentication message reaches the caller intact.
This commit is contained in:
Dustin Healy 2026-08-19 11:21:39 -07:00 committed by GitHub
parent b91691937e
commit 7c71d6dc1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 0 deletions

View file

@ -35,6 +35,7 @@ const {
hasRuntimeUrlPlaceholders,
containsGraphTokenPlaceholder,
isOAuthServer,
OpenIDReauthRequiredError,
} = require('@librechat/api');
const {
Time,
@ -1125,6 +1126,11 @@ function createToolInstance({
error,
);
/** Carries the actionable re-auth message; the substring heuristic below would misreport it as an OAuth configuration problem */
if (error instanceof OpenIDReauthRequiredError) {
throw error;
}
/** OAuth error, provide a helpful message */
const isOAuthError =
error.message?.includes('401') ||

View file

@ -1642,6 +1642,54 @@ describe('User parameter passing tests', () => {
);
});
it('preserves OpenIDReauthRequiredError through the OAuth error classification', async () => {
const { OpenIDReauthRequiredError } = require('@librechat/api');
const mockUser = { id: 'reauth-user', role: 'USER' };
const mockRes = { write: jest.fn(), flush: jest.fn() };
const { getRoleByName } = require('~/models');
getRoleByName.mockResolvedValue({
permissions: {
[PermissionTypes.MCP_SERVERS]: {
[Permissions.USE]: true,
},
},
});
const reauthError = new OpenIDReauthRequiredError(
'OpenID token is expired or unavailable; re-authentication is required to resolve {{LIBRECHAT_OPENID_ACCESS_TOKEN}}.',
);
mockGetMCPManager.mockReturnValue({
callTool: jest.fn().mockRejectedValue(reauthError),
});
const mcpTool = await createMCPTool({
res: mockRes,
user: mockUser,
config: { requiresOAuth: false },
toolKey: `test-tool${D}test-server`,
provider: 'openai',
userMCPAuthMap: {},
availableTools: {
[`test-tool${D}test-server`]: {
function: {
description: 'Cached tool',
parameters: { type: 'object', properties: {} },
},
},
},
});
await expect(
mcpTool.invoke(
{},
{
configurable: { user: mockUser },
metadata: { provider: 'openai', thread_id: 'thread-1', run_id: 'run-1' },
toolCall: {},
},
),
).rejects.toBe(reauthError);
});
it('does not label OBO authentication failures as unconfigured MCP OAuth', async () => {
const mockUser = { id: 'obo-user', role: 'USER' };
const mockRes = { write: jest.fn(), flush: jest.fn() };