diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index d00b4053d3..0c351996fa 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -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') || diff --git a/api/server/services/MCP.spec.js b/api/server/services/MCP.spec.js index c03d0e8902..976e6f81e8 100644 --- a/api/server/services/MCP.spec.js +++ b/api/server/services/MCP.spec.js @@ -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() };