2025-08-13 09:45:06 -06:00
|
|
|
import { logger } from '@librechat/data-schemas';
|
2026-02-01 19:37:04 -05:00
|
|
|
import type { TokenMethods, IUser } from '@librechat/data-schemas';
|
2025-08-13 09:45:06 -06:00
|
|
|
import type { FlowStateManager } from '~/flow/manager';
|
|
|
|
|
import type { MCPOAuthTokens } from '~/mcp/oauth';
|
2025-08-16 20:45:55 -04:00
|
|
|
import type * as t from '~/mcp/types';
|
|
|
|
|
import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory';
|
|
|
|
|
import { MCPConnection } from '~/mcp/connection';
|
2025-08-13 09:45:06 -06:00
|
|
|
import { MCPOAuthHandler } from '~/mcp/oauth';
|
|
|
|
|
import { processMCPEnv } from '~/utils';
|
|
|
|
|
|
2025-08-16 20:45:55 -04:00
|
|
|
jest.mock('~/mcp/connection');
|
2025-08-13 09:45:06 -06:00
|
|
|
jest.mock('~/mcp/oauth');
|
|
|
|
|
jest.mock('~/utils');
|
|
|
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
|
|
|
logger: {
|
|
|
|
|
info: jest.fn(),
|
|
|
|
|
warn: jest.fn(),
|
|
|
|
|
error: jest.fn(),
|
|
|
|
|
debug: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const mockLogger = logger as jest.Mocked<typeof logger>;
|
|
|
|
|
const mockProcessMCPEnv = processMCPEnv as jest.MockedFunction<typeof processMCPEnv>;
|
|
|
|
|
const mockMCPConnection = MCPConnection as jest.MockedClass<typeof MCPConnection>;
|
|
|
|
|
const mockMCPOAuthHandler = MCPOAuthHandler as jest.Mocked<typeof MCPOAuthHandler>;
|
|
|
|
|
|
|
|
|
|
describe('MCPConnectionFactory', () => {
|
2026-02-01 19:37:04 -05:00
|
|
|
let mockUser: IUser | undefined;
|
2025-08-13 09:45:06 -06:00
|
|
|
let mockServerConfig: t.MCPOptions;
|
|
|
|
|
let mockFlowManager: jest.Mocked<FlowStateManager<MCPOAuthTokens | null>>;
|
|
|
|
|
let mockConnectionInstance: jest.Mocked<MCPConnection>;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
jest.clearAllMocks();
|
|
|
|
|
mockUser = {
|
|
|
|
|
id: 'user123',
|
|
|
|
|
email: 'test@example.com',
|
2026-02-01 19:37:04 -05:00
|
|
|
} as IUser;
|
2025-08-13 09:45:06 -06:00
|
|
|
|
|
|
|
|
mockServerConfig = {
|
|
|
|
|
command: 'node',
|
|
|
|
|
args: ['server.js'],
|
|
|
|
|
initTimeout: 5000,
|
|
|
|
|
} as t.MCPOptions;
|
|
|
|
|
|
|
|
|
|
mockFlowManager = {
|
2026-03-03 00:27:36 +00:00
|
|
|
initFlow: jest.fn().mockResolvedValue(undefined),
|
2025-08-13 09:45:06 -06:00
|
|
|
createFlow: jest.fn(),
|
|
|
|
|
createFlowWithHandler: jest.fn(),
|
|
|
|
|
getFlowState: jest.fn(),
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
deleteFlow: jest.fn().mockResolvedValue(true),
|
2025-08-13 09:45:06 -06:00
|
|
|
} as unknown as jest.Mocked<FlowStateManager<MCPOAuthTokens | null>>;
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance = {
|
|
|
|
|
connect: jest.fn(),
|
|
|
|
|
isConnected: jest.fn(),
|
|
|
|
|
setOAuthTokens: jest.fn(),
|
|
|
|
|
on: jest.fn().mockReturnValue(mockConnectionInstance),
|
2025-09-11 18:54:43 -04:00
|
|
|
once: jest.fn().mockReturnValue(mockConnectionInstance),
|
|
|
|
|
off: jest.fn().mockReturnValue(mockConnectionInstance),
|
|
|
|
|
removeListener: jest.fn().mockReturnValue(mockConnectionInstance),
|
2025-08-13 09:45:06 -06:00
|
|
|
emit: jest.fn(),
|
|
|
|
|
} as unknown as jest.Mocked<MCPConnection>;
|
|
|
|
|
|
|
|
|
|
mockMCPConnection.mockImplementation(() => mockConnectionInstance);
|
|
|
|
|
mockProcessMCPEnv.mockReturnValue(mockServerConfig);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('static create method', () => {
|
|
|
|
|
it('should create a basic connection without OAuth', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
🗝️ feat: Credential Variables for DB-Sourced MCP Servers (#12044)
* feat: Allow Credential Variables in Headers for DB-sourced MCP Servers
- Removed the hasCustomUserVars check from ToolService.js, directly retrieving userMCPAuthMap.
- Updated MCPConnectionFactory and related classes to include a dbSourced flag for better handling of database-sourced configurations.
- Added integration tests to ensure proper behavior of dbSourced servers, verifying that sensitive placeholders are not resolved while allowing customUserVars.
- Adjusted various MCP-related files to accommodate the new dbSourced logic, ensuring consistent handling across the codebase.
* chore: MCPConnectionFactory Tests with Additional Flow Metadata for typing
- Updated MCPConnectionFactory tests to include new fields in flowMetadata: serverUrl and state.
- Enhanced mockFlowData in multiple test cases to reflect the updated structure, ensuring comprehensive coverage of the OAuth flow scenarios.
- Added authorization_endpoint to metadata in the test setup for improved validation of the OAuth process.
* refactor: Simplify MCPManager Configuration Handling
- Removed unnecessary type assertions and streamlined the retrieval of server configuration in MCPManager.
- Enhanced the handling of OAuth and database-sourced flags for improved clarity and efficiency.
- Updated tests to reflect changes in user object structure and ensure proper processing of MCP environment variables.
* refactor: Optimize User MCP Auth Map Retrieval in ToolService
- Introduced conditional loading of userMCPAuthMap based on the presence of MCP-delimited tools, improving efficiency by avoiding unnecessary calls.
- Updated the loadToolDefinitionsWrapper and loadAgentTools functions to reflect this change, enhancing overall performance and clarity.
* test: Add userMCPAuthMap gating tests in ToolService
- Introduced new tests to validate the logic for determining if MCP tools are present in the agent's tool list.
- Implemented various scenarios to ensure accurate detection of MCP tools, including edge cases for empty, undefined, and null tool lists.
- Enhanced clarity and coverage of the ToolService capability checking logic.
* refactor: Enhance MCP Environment Variable Processing
- Simplified the handling of the dbSourced parameter in the processMCPEnv function.
- Introduced a failsafe mechanism to derive dbSourced from options if not explicitly provided, improving robustness and clarity in MCP environment variable processing.
* refactor: Update Regex Patterns for Credential Placeholders in ServerConfigsDB
- Modified regex patterns to include additional credential/env placeholders that should not be allowed in user-provided configurations.
- Clarified comments to emphasize the security risks associated with credential exfiltration when MCP servers are shared between users.
* chore: field order
* refactor: Clean Up dbSourced Parameter Handling in processMCPEnv
- Reintroduced the failsafe mechanism for deriving the dbSourced parameter from options, ensuring clarity and robustness in MCP environment variable processing.
- Enhanced code readability by maintaining consistent comment structure.
* refactor: Update MCPOptions Type to Include Optional dbId
- Modified the processMCPEnv function to extend the MCPOptions type, allowing for an optional dbId property.
- Simplified the logic for deriving the dbSourced parameter by directly checking the dbId property, enhancing code clarity and maintainability.
2026-03-03 18:02:37 -05:00
|
|
|
expect(mockProcessMCPEnv).toHaveBeenCalledWith({
|
|
|
|
|
options: mockServerConfig,
|
|
|
|
|
dbSourced: undefined,
|
|
|
|
|
});
|
2025-08-13 09:45:06 -06:00
|
|
|
expect(mockMCPConnection).toHaveBeenCalledWith({
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
userId: undefined,
|
|
|
|
|
oauthTokens: null,
|
2026-02-11 22:09:58 -05:00
|
|
|
useSSRFProtection: false,
|
2025-08-13 09:45:06 -06:00
|
|
|
});
|
|
|
|
|
expect(mockConnectionInstance.connect).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a connection with OAuth', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockTokens: MCPOAuthTokens = {
|
|
|
|
|
access_token: 'access123',
|
|
|
|
|
refresh_token: 'refresh123',
|
|
|
|
|
token_type: 'Bearer',
|
|
|
|
|
obtained_at: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockFlowManager.createFlowWithHandler.mockResolvedValue(mockTokens);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
🗝️ feat: Credential Variables for DB-Sourced MCP Servers (#12044)
* feat: Allow Credential Variables in Headers for DB-sourced MCP Servers
- Removed the hasCustomUserVars check from ToolService.js, directly retrieving userMCPAuthMap.
- Updated MCPConnectionFactory and related classes to include a dbSourced flag for better handling of database-sourced configurations.
- Added integration tests to ensure proper behavior of dbSourced servers, verifying that sensitive placeholders are not resolved while allowing customUserVars.
- Adjusted various MCP-related files to accommodate the new dbSourced logic, ensuring consistent handling across the codebase.
* chore: MCPConnectionFactory Tests with Additional Flow Metadata for typing
- Updated MCPConnectionFactory tests to include new fields in flowMetadata: serverUrl and state.
- Enhanced mockFlowData in multiple test cases to reflect the updated structure, ensuring comprehensive coverage of the OAuth flow scenarios.
- Added authorization_endpoint to metadata in the test setup for improved validation of the OAuth process.
* refactor: Simplify MCPManager Configuration Handling
- Removed unnecessary type assertions and streamlined the retrieval of server configuration in MCPManager.
- Enhanced the handling of OAuth and database-sourced flags for improved clarity and efficiency.
- Updated tests to reflect changes in user object structure and ensure proper processing of MCP environment variables.
* refactor: Optimize User MCP Auth Map Retrieval in ToolService
- Introduced conditional loading of userMCPAuthMap based on the presence of MCP-delimited tools, improving efficiency by avoiding unnecessary calls.
- Updated the loadToolDefinitionsWrapper and loadAgentTools functions to reflect this change, enhancing overall performance and clarity.
* test: Add userMCPAuthMap gating tests in ToolService
- Introduced new tests to validate the logic for determining if MCP tools are present in the agent's tool list.
- Implemented various scenarios to ensure accurate detection of MCP tools, including edge cases for empty, undefined, and null tool lists.
- Enhanced clarity and coverage of the ToolService capability checking logic.
* refactor: Enhance MCP Environment Variable Processing
- Simplified the handling of the dbSourced parameter in the processMCPEnv function.
- Introduced a failsafe mechanism to derive dbSourced from options if not explicitly provided, improving robustness and clarity in MCP environment variable processing.
* refactor: Update Regex Patterns for Credential Placeholders in ServerConfigsDB
- Modified regex patterns to include additional credential/env placeholders that should not be allowed in user-provided configurations.
- Clarified comments to emphasize the security risks associated with credential exfiltration when MCP servers are shared between users.
* chore: field order
* refactor: Clean Up dbSourced Parameter Handling in processMCPEnv
- Reintroduced the failsafe mechanism for deriving the dbSourced parameter from options, ensuring clarity and robustness in MCP environment variable processing.
- Enhanced code readability by maintaining consistent comment structure.
* refactor: Update MCPOptions Type to Include Optional dbId
- Modified the processMCPEnv function to extend the MCPOptions type, allowing for an optional dbId property.
- Simplified the logic for deriving the dbSourced parameter by directly checking the dbId property, enhancing code clarity and maintainability.
2026-03-03 18:02:37 -05:00
|
|
|
expect(mockProcessMCPEnv).toHaveBeenCalledWith({
|
|
|
|
|
options: mockServerConfig,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
dbSourced: undefined,
|
|
|
|
|
});
|
2025-08-13 09:45:06 -06:00
|
|
|
expect(mockMCPConnection).toHaveBeenCalledWith({
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
oauthTokens: mockTokens,
|
2026-02-11 22:09:58 -05:00
|
|
|
useSSRFProtection: false,
|
2025-08-13 09:45:06 -06:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('OAuth token handling', () => {
|
|
|
|
|
it('should return null when no findToken method is provided', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-16 20:45:55 -04:00
|
|
|
const oauthOptions: t.OAuthConnectionOptions = {
|
2025-08-13 09:45:06 -06:00
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
2025-08-16 20:45:55 -04:00
|
|
|
findToken: undefined as unknown as TokenMethods['findToken'],
|
2025-08-13 09:45:06 -06:00
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
|
|
|
|
|
expect(mockFlowManager.createFlowWithHandler).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle token retrieval errors gracefully', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockFlowManager.createFlowWithHandler.mockRejectedValue(new Error('Token fetch failed'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
|
|
|
|
expect(mockMCPConnection).toHaveBeenCalledWith({
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
oauthTokens: null,
|
2026-02-11 22:09:58 -05:00
|
|
|
useSSRFProtection: false,
|
2025-08-13 09:45:06 -06:00
|
|
|
});
|
|
|
|
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('No existing tokens found or error loading tokens'),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('OAuth event handling', () => {
|
|
|
|
|
it('should handle oauthRequired event for returnOnOAuth scenario', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: {
|
|
|
|
|
...mockServerConfig,
|
|
|
|
|
url: 'https://api.example.com',
|
|
|
|
|
type: 'sse' as const,
|
|
|
|
|
} as t.SSEOptions,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
returnOnOAuth: true,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockFlowData = {
|
|
|
|
|
authorizationUrl: 'https://auth.example.com',
|
|
|
|
|
flowId: 'flow123',
|
|
|
|
|
flowMetadata: {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
serverUrl: 'https://api.example.com',
|
|
|
|
|
state: 'random-state',
|
|
|
|
|
clientInfo: { client_id: 'client123' },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData);
|
2026-03-03 00:27:36 +00:00
|
|
|
// createFlow runs as a background monitor — simulate it staying pending
|
|
|
|
|
mockFlowManager.createFlow.mockReturnValue(new Promise(() => {}));
|
2025-08-13 09:45:06 -06:00
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
|
|
|
|
// Expected to fail due to connection not established
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(oauthRequiredHandler!).toBeDefined();
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
expect(mockMCPOAuthHandler.initiateOAuthFlow).toHaveBeenCalledWith(
|
|
|
|
|
'test-server',
|
|
|
|
|
'https://api.example.com',
|
|
|
|
|
'user123',
|
2025-10-11 17:17:12 +02:00
|
|
|
{},
|
2025-08-13 09:45:06 -06:00
|
|
|
undefined,
|
|
|
|
|
);
|
2026-03-03 00:27:36 +00:00
|
|
|
|
|
|
|
|
// initFlow must be awaited BEFORE the redirect to guarantee state is stored
|
|
|
|
|
expect(mockFlowManager.initFlow).toHaveBeenCalledWith(
|
|
|
|
|
'flow123',
|
|
|
|
|
'mcp_oauth',
|
🛂 fix: MCP OAuth Race Conditions, CSRF Fallback, and Token Expiry Handling (#12171)
* fix: Implement race conditions in MCP OAuth flow
- Added connection mutex to coalesce concurrent `getUserConnection` calls, preventing multiple simultaneous attempts.
- Enhanced flow state management to retry once when a flow state is missing, improving resilience against race conditions.
- Introduced `ReauthenticationRequiredError` for better error handling when access tokens are expired or missing.
- Updated tests to cover new race condition scenarios and ensure proper handling of OAuth flows.
* fix: Stale PENDING flow detection and OAuth URL re-issuance
PENDING flows in handleOAuthRequired now check createdAt age — flows
older than 2 minutes are treated as stale and replaced instead of
joined. Fixes the case where a leftover PENDING flow from a previous
session blocks new OAuth initiation.
authorizationUrl is now stored in MCPOAuthFlowMetadata so that when a
second caller joins an active PENDING flow (e.g., the SSE-emitting path
in ToolService), it can re-issue the URL to the user via oauthStart.
* fix: CSRF fallback via active PENDING flow in OAuth callback
When the OAuth callback arrives without CSRF or session cookies (common
in the chat/SSE flow where cookies can't be set on streaming responses),
fall back to validating that a PENDING flow exists for the flowId. This
is safe because the flow was created server-side after JWT authentication
and the authorization code is PKCE-protected.
* test: Extract shared OAuth test server helpers
Move MockKeyv, getFreePort, trackSockets, and createOAuthMCPServer into
a shared helpers/oauthTestServer module. Enhance the test server with
refresh token support, token rotation, metadata discovery, and dynamic
client registration endpoints. Add InMemoryTokenStore for token storage
tests.
Refactor MCPOAuthRaceCondition.test.ts to import from shared helpers.
* test: Add comprehensive MCP OAuth test modules
MCPOAuthTokenStorage — 21 tests for storeTokens/getTokens with
InMemoryTokenStore: encrypt/decrypt round-trips, expiry calculation,
refresh callback wiring, ReauthenticationRequiredError paths.
MCPOAuthFlow — 10 tests against real HTTP server: token refresh with
stored client info, refresh token rotation, metadata discovery, dynamic
client registration, full store/retrieve/expire/refresh lifecycle.
MCPOAuthConnectionEvents — 5 tests for MCPConnection OAuth event cycle
with real OAuth-gated MCP server: oauthRequired emission on 401,
oauthHandled reconnection, oauthFailed rejection, token expiry detection.
MCPOAuthTokenExpiry — 12 tests for the token expiry edge case: refresh
success/failure paths, ReauthenticationRequiredError, PENDING flow CSRF
fallback, authorizationUrl metadata storage, full re-auth cycle after
refresh failure, concurrent expired token coalescing, stale PENDING
flow detection.
* test: Enhance MCP OAuth connection tests with cooldown reset
Added a `beforeEach` hook to clear the cooldown for `MCPConnection` before each test, ensuring a clean state. Updated the race condition handling in the tests to properly clear the timeout, improving reliability in the event data retrieval process.
* refactor: PENDING flow management and state recovery in MCP OAuth
- Introduced a constant `PENDING_STALE_MS` to define the age threshold for PENDING flows, improving the handling of stale flows.
- Updated the logic in `MCPConnectionFactory` and `FlowStateManager` to check the age of PENDING flows before joining or reusing them.
- Modified the `completeFlow` method to return false when the flow state is deleted, ensuring graceful handling of race conditions.
- Enhanced tests to validate the new behavior and ensure robustness against state recovery issues.
* refactor: MCP OAuth flow management and testing
- Updated the `completeFlow` method to log warnings when a tool flow state is not found during completion, improving error handling.
- Introduced a new `normalizeExpiresAt` function to standardize expiration timestamp handling across the application.
- Refactored token expiration checks in `MCPConnectionFactory` to utilize the new normalization function, ensuring consistent behavior.
- Added a comprehensive test suite for OAuth callback CSRF fallback logic, validating the handling of PENDING flows and their staleness.
- Enhanced existing tests to cover new expiration normalization logic and ensure robust flow state management.
* test: Add CSRF fallback tests for active PENDING flows in MCP OAuth
- Introduced new tests to validate CSRF fallback behavior when a fresh PENDING flow exists without cookies, ensuring successful OAuth callback handling.
- Added scenarios to reject requests when no PENDING flow exists, when only a COMPLETED flow is present, and when a PENDING flow is stale, enhancing the robustness of flow state management.
- Improved overall test coverage for OAuth callback logic, reinforcing the handling of CSRF validation failures.
* chore: imports order
* refactor: Update UserConnectionManager to conditionally manage pending connections
- Modified the logic in `UserConnectionManager` to only set pending connections if `forceNew` is false, preventing unnecessary overwrites.
- Adjusted the cleanup process to ensure pending connections are only deleted when not forced, enhancing connection management efficiency.
* refactor: MCP OAuth flow state management
- Introduced a new method `storeStateMapping` in `MCPOAuthHandler` to securely map the OAuth state parameter to the flow ID, improving callback resolution and security against forgery.
- Updated the OAuth initiation and callback handling in `mcp.js` to utilize the new state mapping functionality, ensuring robust flow management.
- Refactored `MCPConnectionFactory` to store state mappings during flow initialization, enhancing the integrity of the OAuth process.
- Adjusted comments to clarify the purpose of state parameters in authorization URLs, reinforcing code readability.
* refactor: MCPConnection with OAuth recovery handling
- Added `oauthRecovery` flag to manage OAuth recovery state during connection attempts.
- Introduced `decrementCycleCount` method to reduce the circuit breaker's cycle count upon successful reconnection after OAuth recovery.
- Updated connection logic to reset the `oauthRecovery` flag after handling OAuth, improving state management and connection reliability.
* chore: Add debug logging for OAuth recovery cycle count decrement
- Introduced a debug log statement in the `MCPConnection` class to track the decrement of the cycle count after a successful reconnection during OAuth recovery.
- This enhancement improves observability and aids in troubleshooting connection issues related to OAuth recovery.
* test: Add OAuth recovery cycle management tests
- Introduced new tests for the OAuth recovery cycle in `MCPConnection`, validating the decrement of cycle counts after successful reconnections.
- Added scenarios to ensure that the cycle count is not decremented on OAuth failures, enhancing the robustness of connection management.
- Improved test coverage for OAuth reconnect scenarios, ensuring reliable behavior under various conditions.
* feat: Implement circuit breaker configuration in MCP
- Added circuit breaker settings to `.env.example` for max cycles, cycle window, and cooldown duration.
- Refactored `MCPConnection` to utilize the new configuration values from `mcpConfig`, enhancing circuit breaker management.
- Improved code maintainability by centralizing circuit breaker parameters in the configuration file.
* refactor: Update decrementCycleCount method for circuit breaker management
- Changed the visibility of the `decrementCycleCount` method in `MCPConnection` from private to public static, allowing it to be called with a server name parameter.
- Updated calls to `decrementCycleCount` in `MCPConnectionFactory` to use the new static method, improving clarity and consistency in circuit breaker management during connection failures and OAuth recovery.
- Enhanced the handling of circuit breaker state by ensuring the method checks for the existence of the circuit breaker before decrementing the cycle count.
* refactor: cycle count decrement on tool listing failure
- Added a call to `MCPConnection.decrementCycleCount` in the `MCPConnectionFactory` to handle cases where unauthenticated tool listing fails, improving circuit breaker management.
- This change ensures that the cycle count is decremented appropriately, maintaining the integrity of the connection recovery process.
* refactor: Update circuit breaker configuration and logic
- Enhanced circuit breaker settings in `.env.example` to include new parameters for failed rounds and backoff strategies.
- Refactored `MCPConnection` to utilize the updated configuration values from `mcpConfig`, improving circuit breaker management.
- Updated tests to reflect changes in circuit breaker logic, ensuring accurate validation of connection behavior under rapid reconnect scenarios.
* feat: Implement state mapping deletion in MCP flow management
- Added a new method `deleteStateMapping` in `MCPOAuthHandler` to remove orphaned state mappings when a flow is replaced, preventing old authorization URLs from resolving after a flow restart.
- Updated `MCPConnectionFactory` to call `deleteStateMapping` during flow cleanup, ensuring proper management of OAuth states.
- Enhanced test coverage for state mapping functionality to validate the new deletion logic.
2026-03-10 21:15:01 -04:00
|
|
|
expect.objectContaining(mockFlowData.flowMetadata),
|
2026-03-03 00:27:36 +00:00
|
|
|
);
|
|
|
|
|
const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0];
|
|
|
|
|
const oauthStartCallOrder = (oauthOptions.oauthStart as jest.Mock).mock
|
|
|
|
|
.invocationCallOrder[0];
|
|
|
|
|
expect(initCallOrder).toBeLessThan(oauthStartCallOrder);
|
|
|
|
|
|
2025-08-13 09:45:06 -06:00
|
|
|
expect(oauthOptions.oauthStart).toHaveBeenCalledWith('https://auth.example.com');
|
|
|
|
|
expect(mockConnectionInstance.emit).toHaveBeenCalledWith(
|
|
|
|
|
'oauthFailed',
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
message: 'OAuth flow initiated - return early',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
|
2026-02-12 14:22:05 -05:00
|
|
|
it('should skip new OAuth flow initiation when a PENDING flow already exists (returnOnOAuth)', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions: t.OAuthConnectionOptions = {
|
|
|
|
|
user: mockUser,
|
|
|
|
|
useOAuth: true,
|
|
|
|
|
returnOnOAuth: true,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockFlowManager.getFlowState.mockResolvedValue({
|
|
|
|
|
status: 'PENDING',
|
|
|
|
|
type: 'mcp_oauth',
|
|
|
|
|
metadata: { codeVerifier: 'existing-verifier' },
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
|
|
|
|
// Expected to fail
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
expect(mockMCPOAuthHandler.initiateOAuthFlow).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockFlowManager.deleteFlow).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockConnectionInstance.emit).toHaveBeenCalledWith(
|
|
|
|
|
'oauthFailed',
|
|
|
|
|
expect.objectContaining({ message: 'OAuth flow initiated - return early' }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 00:27:36 +00:00
|
|
|
it('should emit oauthFailed when initFlow fails to store flow state (returnOnOAuth)', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: {
|
|
|
|
|
...mockServerConfig,
|
|
|
|
|
url: 'https://api.example.com',
|
|
|
|
|
type: 'sse' as const,
|
|
|
|
|
} as t.SSEOptions,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
returnOnOAuth: true,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockFlowData = {
|
|
|
|
|
authorizationUrl: 'https://auth.example.com',
|
|
|
|
|
flowId: 'flow123',
|
🗝️ feat: Credential Variables for DB-Sourced MCP Servers (#12044)
* feat: Allow Credential Variables in Headers for DB-sourced MCP Servers
- Removed the hasCustomUserVars check from ToolService.js, directly retrieving userMCPAuthMap.
- Updated MCPConnectionFactory and related classes to include a dbSourced flag for better handling of database-sourced configurations.
- Added integration tests to ensure proper behavior of dbSourced servers, verifying that sensitive placeholders are not resolved while allowing customUserVars.
- Adjusted various MCP-related files to accommodate the new dbSourced logic, ensuring consistent handling across the codebase.
* chore: MCPConnectionFactory Tests with Additional Flow Metadata for typing
- Updated MCPConnectionFactory tests to include new fields in flowMetadata: serverUrl and state.
- Enhanced mockFlowData in multiple test cases to reflect the updated structure, ensuring comprehensive coverage of the OAuth flow scenarios.
- Added authorization_endpoint to metadata in the test setup for improved validation of the OAuth process.
* refactor: Simplify MCPManager Configuration Handling
- Removed unnecessary type assertions and streamlined the retrieval of server configuration in MCPManager.
- Enhanced the handling of OAuth and database-sourced flags for improved clarity and efficiency.
- Updated tests to reflect changes in user object structure and ensure proper processing of MCP environment variables.
* refactor: Optimize User MCP Auth Map Retrieval in ToolService
- Introduced conditional loading of userMCPAuthMap based on the presence of MCP-delimited tools, improving efficiency by avoiding unnecessary calls.
- Updated the loadToolDefinitionsWrapper and loadAgentTools functions to reflect this change, enhancing overall performance and clarity.
* test: Add userMCPAuthMap gating tests in ToolService
- Introduced new tests to validate the logic for determining if MCP tools are present in the agent's tool list.
- Implemented various scenarios to ensure accurate detection of MCP tools, including edge cases for empty, undefined, and null tool lists.
- Enhanced clarity and coverage of the ToolService capability checking logic.
* refactor: Enhance MCP Environment Variable Processing
- Simplified the handling of the dbSourced parameter in the processMCPEnv function.
- Introduced a failsafe mechanism to derive dbSourced from options if not explicitly provided, improving robustness and clarity in MCP environment variable processing.
* refactor: Update Regex Patterns for Credential Placeholders in ServerConfigsDB
- Modified regex patterns to include additional credential/env placeholders that should not be allowed in user-provided configurations.
- Clarified comments to emphasize the security risks associated with credential exfiltration when MCP servers are shared between users.
* chore: field order
* refactor: Clean Up dbSourced Parameter Handling in processMCPEnv
- Reintroduced the failsafe mechanism for deriving the dbSourced parameter from options, ensuring clarity and robustness in MCP environment variable processing.
- Enhanced code readability by maintaining consistent comment structure.
* refactor: Update MCPOptions Type to Include Optional dbId
- Modified the processMCPEnv function to extend the MCPOptions type, allowing for an optional dbId property.
- Simplified the logic for deriving the dbSourced parameter by directly checking the dbId property, enhancing code clarity and maintainability.
2026-03-03 18:02:37 -05:00
|
|
|
flowMetadata: {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
serverUrl: 'https://api.example.com',
|
|
|
|
|
state: 'state123',
|
|
|
|
|
},
|
2026-03-03 00:27:36 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData);
|
|
|
|
|
mockFlowManager.initFlow.mockRejectedValue(new Error('Store write failed'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
|
|
|
|
// Expected to fail
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
// initFlow failed, so oauthStart should NOT have been called (redirect never happens)
|
|
|
|
|
expect(oauthOptions.oauthStart).not.toHaveBeenCalled();
|
|
|
|
|
// createFlow should NOT have been called since initFlow failed first
|
|
|
|
|
expect(mockFlowManager.createFlow).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockConnectionInstance.emit).toHaveBeenCalledWith(
|
|
|
|
|
'oauthFailed',
|
|
|
|
|
expect.objectContaining({ message: 'OAuth initiation failed' }),
|
|
|
|
|
);
|
|
|
|
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should log warnings when background createFlow monitor rejects (returnOnOAuth)', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: {
|
|
|
|
|
...mockServerConfig,
|
|
|
|
|
url: 'https://api.example.com',
|
|
|
|
|
type: 'sse' as const,
|
|
|
|
|
} as t.SSEOptions,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
returnOnOAuth: true,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockFlowData = {
|
|
|
|
|
authorizationUrl: 'https://auth.example.com',
|
|
|
|
|
flowId: 'flow123',
|
🗝️ feat: Credential Variables for DB-Sourced MCP Servers (#12044)
* feat: Allow Credential Variables in Headers for DB-sourced MCP Servers
- Removed the hasCustomUserVars check from ToolService.js, directly retrieving userMCPAuthMap.
- Updated MCPConnectionFactory and related classes to include a dbSourced flag for better handling of database-sourced configurations.
- Added integration tests to ensure proper behavior of dbSourced servers, verifying that sensitive placeholders are not resolved while allowing customUserVars.
- Adjusted various MCP-related files to accommodate the new dbSourced logic, ensuring consistent handling across the codebase.
* chore: MCPConnectionFactory Tests with Additional Flow Metadata for typing
- Updated MCPConnectionFactory tests to include new fields in flowMetadata: serverUrl and state.
- Enhanced mockFlowData in multiple test cases to reflect the updated structure, ensuring comprehensive coverage of the OAuth flow scenarios.
- Added authorization_endpoint to metadata in the test setup for improved validation of the OAuth process.
* refactor: Simplify MCPManager Configuration Handling
- Removed unnecessary type assertions and streamlined the retrieval of server configuration in MCPManager.
- Enhanced the handling of OAuth and database-sourced flags for improved clarity and efficiency.
- Updated tests to reflect changes in user object structure and ensure proper processing of MCP environment variables.
* refactor: Optimize User MCP Auth Map Retrieval in ToolService
- Introduced conditional loading of userMCPAuthMap based on the presence of MCP-delimited tools, improving efficiency by avoiding unnecessary calls.
- Updated the loadToolDefinitionsWrapper and loadAgentTools functions to reflect this change, enhancing overall performance and clarity.
* test: Add userMCPAuthMap gating tests in ToolService
- Introduced new tests to validate the logic for determining if MCP tools are present in the agent's tool list.
- Implemented various scenarios to ensure accurate detection of MCP tools, including edge cases for empty, undefined, and null tool lists.
- Enhanced clarity and coverage of the ToolService capability checking logic.
* refactor: Enhance MCP Environment Variable Processing
- Simplified the handling of the dbSourced parameter in the processMCPEnv function.
- Introduced a failsafe mechanism to derive dbSourced from options if not explicitly provided, improving robustness and clarity in MCP environment variable processing.
* refactor: Update Regex Patterns for Credential Placeholders in ServerConfigsDB
- Modified regex patterns to include additional credential/env placeholders that should not be allowed in user-provided configurations.
- Clarified comments to emphasize the security risks associated with credential exfiltration when MCP servers are shared between users.
* chore: field order
* refactor: Clean Up dbSourced Parameter Handling in processMCPEnv
- Reintroduced the failsafe mechanism for deriving the dbSourced parameter from options, ensuring clarity and robustness in MCP environment variable processing.
- Enhanced code readability by maintaining consistent comment structure.
* refactor: Update MCPOptions Type to Include Optional dbId
- Modified the processMCPEnv function to extend the MCPOptions type, allowing for an optional dbId property.
- Simplified the logic for deriving the dbSourced parameter by directly checking the dbId property, enhancing code clarity and maintainability.
2026-03-03 18:02:37 -05:00
|
|
|
flowMetadata: {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
serverUrl: 'https://api.example.com',
|
|
|
|
|
state: 'state123',
|
|
|
|
|
},
|
2026-03-03 00:27:36 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData);
|
|
|
|
|
// Simulate the background monitor timing out
|
|
|
|
|
mockFlowManager.createFlow.mockRejectedValue(new Error('mcp_oauth flow timed out'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
|
|
|
|
// Expected to fail
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
// Allow the .catch handler on createFlow to execute
|
|
|
|
|
await Promise.resolve();
|
|
|
|
|
|
|
|
|
|
// initFlow should have succeeded and redirect should have happened
|
|
|
|
|
expect(mockFlowManager.initFlow).toHaveBeenCalled();
|
|
|
|
|
expect(oauthOptions.oauthStart).toHaveBeenCalledWith('https://auth.example.com');
|
|
|
|
|
// The background monitor error should be logged, not silently swallowed
|
|
|
|
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('OAuth flow monitor ended'),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call initFlow before createFlow in blocking OAuth path (non-returnOnOAuth)', async () => {
|
|
|
|
|
const sseConfig = {
|
|
|
|
|
...mockServerConfig,
|
|
|
|
|
url: 'https://api.example.com',
|
|
|
|
|
type: 'sse' as const,
|
|
|
|
|
} as t.SSEOptions;
|
|
|
|
|
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: sseConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
oauthEnd: jest.fn(),
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockFlowData = {
|
|
|
|
|
authorizationUrl: 'https://auth.example.com',
|
|
|
|
|
flowId: 'flow123',
|
|
|
|
|
flowMetadata: {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
serverUrl: 'https://api.example.com',
|
|
|
|
|
state: 'random-state',
|
|
|
|
|
clientInfo: { client_id: 'client123' },
|
🗝️ feat: Credential Variables for DB-Sourced MCP Servers (#12044)
* feat: Allow Credential Variables in Headers for DB-sourced MCP Servers
- Removed the hasCustomUserVars check from ToolService.js, directly retrieving userMCPAuthMap.
- Updated MCPConnectionFactory and related classes to include a dbSourced flag for better handling of database-sourced configurations.
- Added integration tests to ensure proper behavior of dbSourced servers, verifying that sensitive placeholders are not resolved while allowing customUserVars.
- Adjusted various MCP-related files to accommodate the new dbSourced logic, ensuring consistent handling across the codebase.
* chore: MCPConnectionFactory Tests with Additional Flow Metadata for typing
- Updated MCPConnectionFactory tests to include new fields in flowMetadata: serverUrl and state.
- Enhanced mockFlowData in multiple test cases to reflect the updated structure, ensuring comprehensive coverage of the OAuth flow scenarios.
- Added authorization_endpoint to metadata in the test setup for improved validation of the OAuth process.
* refactor: Simplify MCPManager Configuration Handling
- Removed unnecessary type assertions and streamlined the retrieval of server configuration in MCPManager.
- Enhanced the handling of OAuth and database-sourced flags for improved clarity and efficiency.
- Updated tests to reflect changes in user object structure and ensure proper processing of MCP environment variables.
* refactor: Optimize User MCP Auth Map Retrieval in ToolService
- Introduced conditional loading of userMCPAuthMap based on the presence of MCP-delimited tools, improving efficiency by avoiding unnecessary calls.
- Updated the loadToolDefinitionsWrapper and loadAgentTools functions to reflect this change, enhancing overall performance and clarity.
* test: Add userMCPAuthMap gating tests in ToolService
- Introduced new tests to validate the logic for determining if MCP tools are present in the agent's tool list.
- Implemented various scenarios to ensure accurate detection of MCP tools, including edge cases for empty, undefined, and null tool lists.
- Enhanced clarity and coverage of the ToolService capability checking logic.
* refactor: Enhance MCP Environment Variable Processing
- Simplified the handling of the dbSourced parameter in the processMCPEnv function.
- Introduced a failsafe mechanism to derive dbSourced from options if not explicitly provided, improving robustness and clarity in MCP environment variable processing.
* refactor: Update Regex Patterns for Credential Placeholders in ServerConfigsDB
- Modified regex patterns to include additional credential/env placeholders that should not be allowed in user-provided configurations.
- Clarified comments to emphasize the security risks associated with credential exfiltration when MCP servers are shared between users.
* chore: field order
* refactor: Clean Up dbSourced Parameter Handling in processMCPEnv
- Reintroduced the failsafe mechanism for deriving the dbSourced parameter from options, ensuring clarity and robustness in MCP environment variable processing.
- Enhanced code readability by maintaining consistent comment structure.
* refactor: Update MCPOptions Type to Include Optional dbId
- Modified the processMCPEnv function to extend the MCPOptions type, allowing for an optional dbId property.
- Simplified the logic for deriving the dbSourced parameter by directly checking the dbId property, enhancing code clarity and maintainability.
2026-03-03 18:02:37 -05:00
|
|
|
metadata: {
|
|
|
|
|
token_endpoint: 'https://auth.example.com/token',
|
|
|
|
|
authorization_endpoint: 'https://auth.example.com/authorize',
|
|
|
|
|
},
|
2026-03-03 00:27:36 +00:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockTokens: MCPOAuthTokens = {
|
|
|
|
|
access_token: 'access123',
|
|
|
|
|
refresh_token: 'refresh123',
|
|
|
|
|
token_type: 'Bearer',
|
|
|
|
|
obtained_at: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// processMCPEnv must return config with url so handleOAuthRequired proceeds
|
|
|
|
|
mockProcessMCPEnv.mockReturnValue(sseConfig);
|
|
|
|
|
mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData);
|
|
|
|
|
mockMCPOAuthHandler.generateFlowId.mockReturnValue('flow123');
|
|
|
|
|
mockFlowManager.getFlowState.mockResolvedValue(null);
|
|
|
|
|
mockFlowManager.createFlow.mockResolvedValue(mockTokens);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
|
|
|
|
// Expected to fail
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
// initFlow must be called BEFORE oauthStart and createFlow
|
|
|
|
|
expect(mockFlowManager.initFlow).toHaveBeenCalledWith(
|
|
|
|
|
'flow123',
|
|
|
|
|
'mcp_oauth',
|
🛂 fix: MCP OAuth Race Conditions, CSRF Fallback, and Token Expiry Handling (#12171)
* fix: Implement race conditions in MCP OAuth flow
- Added connection mutex to coalesce concurrent `getUserConnection` calls, preventing multiple simultaneous attempts.
- Enhanced flow state management to retry once when a flow state is missing, improving resilience against race conditions.
- Introduced `ReauthenticationRequiredError` for better error handling when access tokens are expired or missing.
- Updated tests to cover new race condition scenarios and ensure proper handling of OAuth flows.
* fix: Stale PENDING flow detection and OAuth URL re-issuance
PENDING flows in handleOAuthRequired now check createdAt age — flows
older than 2 minutes are treated as stale and replaced instead of
joined. Fixes the case where a leftover PENDING flow from a previous
session blocks new OAuth initiation.
authorizationUrl is now stored in MCPOAuthFlowMetadata so that when a
second caller joins an active PENDING flow (e.g., the SSE-emitting path
in ToolService), it can re-issue the URL to the user via oauthStart.
* fix: CSRF fallback via active PENDING flow in OAuth callback
When the OAuth callback arrives without CSRF or session cookies (common
in the chat/SSE flow where cookies can't be set on streaming responses),
fall back to validating that a PENDING flow exists for the flowId. This
is safe because the flow was created server-side after JWT authentication
and the authorization code is PKCE-protected.
* test: Extract shared OAuth test server helpers
Move MockKeyv, getFreePort, trackSockets, and createOAuthMCPServer into
a shared helpers/oauthTestServer module. Enhance the test server with
refresh token support, token rotation, metadata discovery, and dynamic
client registration endpoints. Add InMemoryTokenStore for token storage
tests.
Refactor MCPOAuthRaceCondition.test.ts to import from shared helpers.
* test: Add comprehensive MCP OAuth test modules
MCPOAuthTokenStorage — 21 tests for storeTokens/getTokens with
InMemoryTokenStore: encrypt/decrypt round-trips, expiry calculation,
refresh callback wiring, ReauthenticationRequiredError paths.
MCPOAuthFlow — 10 tests against real HTTP server: token refresh with
stored client info, refresh token rotation, metadata discovery, dynamic
client registration, full store/retrieve/expire/refresh lifecycle.
MCPOAuthConnectionEvents — 5 tests for MCPConnection OAuth event cycle
with real OAuth-gated MCP server: oauthRequired emission on 401,
oauthHandled reconnection, oauthFailed rejection, token expiry detection.
MCPOAuthTokenExpiry — 12 tests for the token expiry edge case: refresh
success/failure paths, ReauthenticationRequiredError, PENDING flow CSRF
fallback, authorizationUrl metadata storage, full re-auth cycle after
refresh failure, concurrent expired token coalescing, stale PENDING
flow detection.
* test: Enhance MCP OAuth connection tests with cooldown reset
Added a `beforeEach` hook to clear the cooldown for `MCPConnection` before each test, ensuring a clean state. Updated the race condition handling in the tests to properly clear the timeout, improving reliability in the event data retrieval process.
* refactor: PENDING flow management and state recovery in MCP OAuth
- Introduced a constant `PENDING_STALE_MS` to define the age threshold for PENDING flows, improving the handling of stale flows.
- Updated the logic in `MCPConnectionFactory` and `FlowStateManager` to check the age of PENDING flows before joining or reusing them.
- Modified the `completeFlow` method to return false when the flow state is deleted, ensuring graceful handling of race conditions.
- Enhanced tests to validate the new behavior and ensure robustness against state recovery issues.
* refactor: MCP OAuth flow management and testing
- Updated the `completeFlow` method to log warnings when a tool flow state is not found during completion, improving error handling.
- Introduced a new `normalizeExpiresAt` function to standardize expiration timestamp handling across the application.
- Refactored token expiration checks in `MCPConnectionFactory` to utilize the new normalization function, ensuring consistent behavior.
- Added a comprehensive test suite for OAuth callback CSRF fallback logic, validating the handling of PENDING flows and their staleness.
- Enhanced existing tests to cover new expiration normalization logic and ensure robust flow state management.
* test: Add CSRF fallback tests for active PENDING flows in MCP OAuth
- Introduced new tests to validate CSRF fallback behavior when a fresh PENDING flow exists without cookies, ensuring successful OAuth callback handling.
- Added scenarios to reject requests when no PENDING flow exists, when only a COMPLETED flow is present, and when a PENDING flow is stale, enhancing the robustness of flow state management.
- Improved overall test coverage for OAuth callback logic, reinforcing the handling of CSRF validation failures.
* chore: imports order
* refactor: Update UserConnectionManager to conditionally manage pending connections
- Modified the logic in `UserConnectionManager` to only set pending connections if `forceNew` is false, preventing unnecessary overwrites.
- Adjusted the cleanup process to ensure pending connections are only deleted when not forced, enhancing connection management efficiency.
* refactor: MCP OAuth flow state management
- Introduced a new method `storeStateMapping` in `MCPOAuthHandler` to securely map the OAuth state parameter to the flow ID, improving callback resolution and security against forgery.
- Updated the OAuth initiation and callback handling in `mcp.js` to utilize the new state mapping functionality, ensuring robust flow management.
- Refactored `MCPConnectionFactory` to store state mappings during flow initialization, enhancing the integrity of the OAuth process.
- Adjusted comments to clarify the purpose of state parameters in authorization URLs, reinforcing code readability.
* refactor: MCPConnection with OAuth recovery handling
- Added `oauthRecovery` flag to manage OAuth recovery state during connection attempts.
- Introduced `decrementCycleCount` method to reduce the circuit breaker's cycle count upon successful reconnection after OAuth recovery.
- Updated connection logic to reset the `oauthRecovery` flag after handling OAuth, improving state management and connection reliability.
* chore: Add debug logging for OAuth recovery cycle count decrement
- Introduced a debug log statement in the `MCPConnection` class to track the decrement of the cycle count after a successful reconnection during OAuth recovery.
- This enhancement improves observability and aids in troubleshooting connection issues related to OAuth recovery.
* test: Add OAuth recovery cycle management tests
- Introduced new tests for the OAuth recovery cycle in `MCPConnection`, validating the decrement of cycle counts after successful reconnections.
- Added scenarios to ensure that the cycle count is not decremented on OAuth failures, enhancing the robustness of connection management.
- Improved test coverage for OAuth reconnect scenarios, ensuring reliable behavior under various conditions.
* feat: Implement circuit breaker configuration in MCP
- Added circuit breaker settings to `.env.example` for max cycles, cycle window, and cooldown duration.
- Refactored `MCPConnection` to utilize the new configuration values from `mcpConfig`, enhancing circuit breaker management.
- Improved code maintainability by centralizing circuit breaker parameters in the configuration file.
* refactor: Update decrementCycleCount method for circuit breaker management
- Changed the visibility of the `decrementCycleCount` method in `MCPConnection` from private to public static, allowing it to be called with a server name parameter.
- Updated calls to `decrementCycleCount` in `MCPConnectionFactory` to use the new static method, improving clarity and consistency in circuit breaker management during connection failures and OAuth recovery.
- Enhanced the handling of circuit breaker state by ensuring the method checks for the existence of the circuit breaker before decrementing the cycle count.
* refactor: cycle count decrement on tool listing failure
- Added a call to `MCPConnection.decrementCycleCount` in the `MCPConnectionFactory` to handle cases where unauthenticated tool listing fails, improving circuit breaker management.
- This change ensures that the cycle count is decremented appropriately, maintaining the integrity of the connection recovery process.
* refactor: Update circuit breaker configuration and logic
- Enhanced circuit breaker settings in `.env.example` to include new parameters for failed rounds and backoff strategies.
- Refactored `MCPConnection` to utilize the updated configuration values from `mcpConfig`, improving circuit breaker management.
- Updated tests to reflect changes in circuit breaker logic, ensuring accurate validation of connection behavior under rapid reconnect scenarios.
* feat: Implement state mapping deletion in MCP flow management
- Added a new method `deleteStateMapping` in `MCPOAuthHandler` to remove orphaned state mappings when a flow is replaced, preventing old authorization URLs from resolving after a flow restart.
- Updated `MCPConnectionFactory` to call `deleteStateMapping` during flow cleanup, ensuring proper management of OAuth states.
- Enhanced test coverage for state mapping functionality to validate the new deletion logic.
2026-03-10 21:15:01 -04:00
|
|
|
expect.objectContaining(mockFlowData.flowMetadata),
|
2026-03-03 00:27:36 +00:00
|
|
|
);
|
|
|
|
|
const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0];
|
|
|
|
|
const oauthStartCallOrder = (oauthOptions.oauthStart as jest.Mock).mock
|
|
|
|
|
.invocationCallOrder[0];
|
|
|
|
|
const createCallOrder = mockFlowManager.createFlow.mock.invocationCallOrder[0];
|
|
|
|
|
expect(initCallOrder).toBeLessThan(oauthStartCallOrder);
|
|
|
|
|
expect(initCallOrder).toBeLessThan(createCallOrder);
|
|
|
|
|
|
|
|
|
|
// createFlow should receive {} since initFlow already persisted metadata
|
|
|
|
|
expect(mockFlowManager.createFlow).toHaveBeenCalledWith(
|
|
|
|
|
'flow123',
|
|
|
|
|
'mcp_oauth',
|
|
|
|
|
{},
|
|
|
|
|
undefined,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-12 14:22:05 -05:00
|
|
|
it('should delete stale flow and create new OAuth flow when existing flow is COMPLETED', async () => {
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-01 19:37:04 -05:00
|
|
|
const oauthOptions: t.OAuthConnectionOptions = {
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
user: mockUser,
|
|
|
|
|
useOAuth: true,
|
|
|
|
|
returnOnOAuth: true,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockFlowData = {
|
|
|
|
|
authorizationUrl: 'https://auth.example.com',
|
|
|
|
|
flowId: 'user123:test-server',
|
|
|
|
|
flowMetadata: {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
serverUrl: 'https://api.example.com',
|
|
|
|
|
state: 'test-state',
|
|
|
|
|
codeVerifier: 'new-code-verifier-xyz',
|
|
|
|
|
clientInfo: { client_id: 'test-client' },
|
|
|
|
|
metadata: {
|
|
|
|
|
authorization_endpoint: 'https://auth.example.com/authorize',
|
|
|
|
|
token_endpoint: 'https://auth.example.com/token',
|
|
|
|
|
issuer: 'https://api.example.com',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-12 14:22:05 -05:00
|
|
|
mockFlowManager.getFlowState.mockResolvedValue({
|
|
|
|
|
status: 'COMPLETED',
|
|
|
|
|
type: 'mcp_oauth',
|
|
|
|
|
metadata: { codeVerifier: 'old-verifier' },
|
|
|
|
|
createdAt: Date.now() - 60000,
|
|
|
|
|
});
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData);
|
|
|
|
|
mockFlowManager.deleteFlow.mockResolvedValue(true);
|
2026-03-03 00:27:36 +00:00
|
|
|
// createFlow runs as a background monitor — simulate it staying pending
|
|
|
|
|
mockFlowManager.createFlow.mockReturnValue(new Promise(() => {}));
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
2026-02-12 14:22:05 -05:00
|
|
|
// Expected to fail
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith('user123:test-server', 'mcp_oauth');
|
|
|
|
|
|
2026-03-03 00:27:36 +00:00
|
|
|
// initFlow must be called after deleteFlow and before createFlow
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
const deleteCallOrder = mockFlowManager.deleteFlow.mock.invocationCallOrder[0];
|
2026-03-03 00:27:36 +00:00
|
|
|
const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0];
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
const createCallOrder = mockFlowManager.createFlow.mock.invocationCallOrder[0];
|
2026-03-03 00:27:36 +00:00
|
|
|
expect(deleteCallOrder).toBeLessThan(initCallOrder);
|
|
|
|
|
expect(initCallOrder).toBeLessThan(createCallOrder);
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
|
2026-03-03 00:27:36 +00:00
|
|
|
expect(mockFlowManager.initFlow).toHaveBeenCalledWith(
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
'user123:test-server',
|
|
|
|
|
'mcp_oauth',
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
codeVerifier: 'new-code-verifier-xyz',
|
|
|
|
|
}),
|
2026-03-03 00:27:36 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// createFlow finds the existing PENDING state written by initFlow,
|
|
|
|
|
// so metadata arg is unused (passed as {})
|
|
|
|
|
expect(mockFlowManager.createFlow).toHaveBeenCalledWith(
|
|
|
|
|
'user123:test-server',
|
|
|
|
|
'mcp_oauth',
|
|
|
|
|
{},
|
🌊 feat: Resumable LLM Streams with Horizontal Scaling (#10926)
* ✨ feat: Implement Resumable Generation Jobs with SSE Support
- Introduced GenerationJobManager to handle resumable LLM generation jobs independently of HTTP connections.
- Added support for subscribing to ongoing generation jobs via SSE, allowing clients to reconnect and receive updates without losing progress.
- Enhanced existing agent controllers and routes to integrate resumable functionality, including job creation, completion, and error handling.
- Updated client-side hooks to manage adaptive SSE streams, switching between standard and resumable modes based on user settings.
- Added UI components and settings for enabling/disabling resumable streams, improving user experience during unstable connections.
* WIP: resuming
* WIP: resumable stream
* feat: Enhance Stream Management with Abort Functionality
- Updated the abort endpoint to support aborting ongoing generation streams using either streamId or conversationId.
- Introduced a new mutation hook `useAbortStreamMutation` for client-side integration.
- Added `useStreamStatus` query to monitor stream status and facilitate resuming conversations.
- Enhanced `useChatHelpers` to incorporate abort functionality when stopping generation.
- Improved `useResumableSSE` to handle stream errors and token refresh seamlessly.
- Updated `useResumeOnLoad` to check for active streams and resume conversations appropriately.
* fix: Update query parameter handling in useChatHelpers
- Refactored the logic for determining the query parameter used in fetching messages to prioritize paramId from the URL, falling back to conversationId only if paramId is not available. This change ensures consistency with the ChatView component's expectations.
* fix: improve syncing when switching conversations
* fix: Prevent memory leaks in useResumableSSE by clearing handler maps on stream completion and cleanup
* fix: Improve content type mismatch handling in useStepHandler
- Enhanced the condition for detecting content type mismatches to include additional checks, ensuring more robust validation of content types before processing updates.
* fix: Allow dynamic content creation in useChatFunctions
- Updated the initial response handling to avoid pre-initializing content types, enabling dynamic creation of content parts based on incoming delta events. This change supports various content types such as think and text.
* fix: Refine response message handling in useStepHandler
- Updated logic to determine the appropriate response message based on the last message's origin, ensuring correct message replacement or appending based on user interaction. This change enhances the accuracy of message updates in the chat flow.
* refactor: Enhance GenerationJobManager with In-Memory Implementations
- Introduced InMemoryJobStore, InMemoryEventTransport, and InMemoryContentState for improved job management and event handling.
- Updated GenerationJobManager to utilize these new implementations, allowing for better separation of concerns and easier maintenance.
- Enhanced job metadata handling to support user messages and response IDs for resumable functionality.
- Improved cleanup and state management processes to prevent memory leaks and ensure efficient resource usage.
* refactor: Enhance GenerationJobManager with improved subscriber handling
- Updated RuntimeJobState to include allSubscribersLeftHandlers for managing client disconnections without affecting subscriber count.
- Refined createJob and subscribe methods to ensure generation starts only when the first real client connects.
- Added detailed documentation for methods and properties to clarify the synchronization of job generation with client readiness.
- Improved logging for subscriber checks and event handling to facilitate debugging and monitoring.
* chore: Adjust timeout for subscriber readiness in ResumableAgentController
- Reduced the timeout duration from 5000ms to 2500ms in the startGeneration function to improve responsiveness when waiting for subscriber readiness. This change aims to enhance the efficiency of the agent's background generation process.
* refactor: Update GenerationJobManager documentation and structure
- Enhanced the documentation for GenerationJobManager to clarify the architecture and pluggable service design.
- Updated comments to reflect the potential for Redis integration and the need for async refactoring.
- Improved the structure of the GenerationJob facade to emphasize the unified API while allowing for implementation swapping without affecting consumer code.
* refactor: Convert GenerationJobManager methods to async for improved performance
- Updated methods in GenerationJobManager and InMemoryJobStore to be asynchronous, enhancing the handling of job creation, retrieval, and management.
- Adjusted the ResumableAgentController and related routes to await job operations, ensuring proper flow and error handling.
- Increased timeout duration in ResumableAgentController's startGeneration function to 3500ms for better subscriber readiness management.
* refactor: Simplify initial response handling in useChatFunctions
- Removed unnecessary pre-initialization of content types in the initial response, allowing for dynamic content creation based on incoming delta events. This change enhances flexibility in handling various content types in the chat flow.
* refactor: Clarify content handling logic in useStepHandler
- Updated comments to better explain the handling of initialContent and existingContent in edit and resume scenarios.
- Simplified the logic for merging content, ensuring that initialContent is used directly when available, improving clarity and maintainability.
* refactor: Improve message handling logic in useStepHandler
- Enhanced the logic for managing messages in multi-tab scenarios, ensuring that the most up-to-date message history is utilized.
- Removed existing response placeholders and ensured user messages are included, improving the accuracy of message updates in the chat flow.
* fix: remove unnecessary content length logging in the chat stream response, simplifying the debug message while retaining essential information about run steps. This change enhances clarity in logging without losing critical context.
* refactor: Integrate streamId handling for improved resumable functionality for attachments
- Added streamId parameter to various functions to support resumable mode in tool loading and memory processing.
- Updated related methods to ensure proper handling of attachments and responses based on the presence of streamId, enhancing the overall streaming experience.
- Improved logging and attachment management to accommodate both standard and resumable modes.
* refactor: Streamline abort handling and integrate GenerationJobManager for improved job management
- Removed the abortControllers middleware and integrated abort handling directly into GenerationJobManager.
- Updated abortMessage function to utilize GenerationJobManager for aborting jobs by conversation ID, enhancing clarity and efficiency.
- Simplified cleanup processes and improved error handling during abort operations.
- Enhanced metadata management for jobs, including endpoint and model information, to facilitate better tracking and resource management.
* refactor: Unify streamId and conversationId handling for improved job management
- Updated ResumableAgentController and AgentController to generate conversationId upfront, ensuring it matches streamId for consistency.
- Simplified job creation and metadata management by removing redundant conversationId updates from callbacks.
- Refactored abortMiddleware and related methods to utilize the unified streamId/conversationId approach, enhancing clarity in job handling.
- Removed deprecated methods from GenerationJobManager and InMemoryJobStore, streamlining the codebase and improving maintainability.
* refactor: Enhance resumable SSE handling with improved UI state management and error recovery
- Added UI state restoration on successful SSE connection to indicate ongoing submission.
- Implemented detailed error handling for network failures, including retry logic with exponential backoff.
- Introduced abort event handling to reset UI state on intentional stream closure.
- Enhanced debugging capabilities for testing reconnection and clean close scenarios.
- Updated generation function to retry on network errors, improving resilience during submission processes.
* refactor: Consolidate content state management into IJobStore for improved job handling
- Removed InMemoryContentState and integrated its functionality into InMemoryJobStore, streamlining content state management.
- Updated GenerationJobManager to utilize jobStore for content state operations, enhancing clarity and reducing redundancy.
- Introduced RedisJobStore for horizontal scaling, allowing for efficient job management and content reconstruction from chunks.
- Updated IJobStore interface to reflect changes in content state handling, ensuring consistency across implementations.
* feat: Introduce Redis-backed stream services for enhanced job management
- Added createStreamServices function to configure job store and event transport, supporting both Redis and in-memory options.
- Updated GenerationJobManager to allow configuration with custom job stores and event transports, improving flexibility for different deployment scenarios.
- Refactored IJobStore interface to support asynchronous content retrieval, ensuring compatibility with Redis implementations.
- Implemented RedisEventTransport for real-time event delivery across instances, enhancing scalability and responsiveness.
- Updated InMemoryJobStore to align with new async patterns for content and run step retrieval, ensuring consistent behavior across storage options.
* refactor: Remove redundant debug logging in GenerationJobManager and RedisEventTransport
- Eliminated unnecessary debug statements in GenerationJobManager related to subscriber actions and job updates, enhancing log clarity.
- Removed debug logging in RedisEventTransport for subscription and subscriber disconnection events, streamlining the logging output.
- Cleaned up debug messages in RedisJobStore to focus on essential information, improving overall logging efficiency.
* refactor: Enhance job state management and TTL configuration in RedisJobStore
- Updated the RedisJobStore to allow customizable TTL values for job states, improving flexibility in job management.
- Refactored the handling of job expiration and cleanup processes to align with new TTL configurations.
- Simplified the response structure in the chat status endpoint by consolidating state retrieval, enhancing clarity and performance.
- Improved comments and documentation for better understanding of the changes made.
* refactor: cleanupOnComplete option to GenerationJobManager for flexible resource management
- Introduced a new configuration option, cleanupOnComplete, allowing immediate cleanup of event transport and job resources upon job completion.
- Updated completeJob and abortJob methods to respect the cleanupOnComplete setting, enhancing memory management.
- Improved cleanup logic in the cleanup method to handle orphaned resources effectively.
- Enhanced documentation and comments for better clarity on the new functionality.
* refactor: Update TTL configuration for completed jobs in InMemoryJobStore
- Changed the TTL for completed jobs from 5 minutes to 0, allowing for immediate cleanup.
- Enhanced cleanup logic to respect the new TTL setting, improving resource management.
- Updated comments for clarity on the behavior of the TTL configuration.
* refactor: Enhance RedisJobStore with local graph caching for improved performance
- Introduced a local cache for graph references using WeakRef to optimize reconnects for the same instance.
- Updated job deletion and cleanup methods to manage the local cache effectively, ensuring stale entries are removed.
- Enhanced content retrieval methods to prioritize local cache access, reducing Redis round-trips for same-instance reconnects.
- Improved documentation and comments for clarity on the caching mechanism and its benefits.
* feat: Add integration tests for GenerationJobManager, RedisEventTransport, and RedisJobStore, add Redis Cluster support
- Introduced comprehensive integration tests for GenerationJobManager, covering both in-memory and Redis modes to ensure consistent job management and event handling.
- Added tests for RedisEventTransport to validate pub/sub functionality, including cross-instance event delivery and error handling.
- Implemented integration tests for RedisJobStore, focusing on multi-instance job access, content reconstruction from chunks, and consumer group behavior.
- Enhanced test setup and teardown processes to ensure a clean environment for each test run, improving reliability and maintainability.
* fix: Improve error handling in GenerationJobManager for allSubscribersLeft handlers
- Enhanced the error handling logic when retrieving content parts for allSubscribersLeft handlers, ensuring that any failures are logged appropriately.
- Updated the promise chain to catch errors from getContentParts, improving robustness and clarity in error reporting.
* ci: Improve Redis client disconnection handling in integration tests
- Updated the afterAll cleanup logic in integration tests for GenerationJobManager, RedisEventTransport, and RedisJobStore to use `quit()` for graceful disconnection of the Redis client.
- Added fallback to `disconnect()` if `quit()` fails, enhancing robustness in resource management during test teardown.
- Improved comments for clarity on the disconnection process and error handling.
* refactor: Enhance GenerationJobManager and event transports for improved resource management
- Updated GenerationJobManager to prevent immediate cleanup of eventTransport upon job completion, allowing final events to transmit fully before cleanup.
- Added orphaned stream cleanup logic in GenerationJobManager to handle streams without corresponding jobs.
- Introduced getTrackedStreamIds method in both InMemoryEventTransport and RedisEventTransport for better management of orphaned streams.
- Improved comments for clarity on resource management and cleanup processes.
* refactor: Update GenerationJobManager and ResumableAgentController for improved event handling
- Modified GenerationJobManager to resolve readyPromise immediately, eliminating startup latency and allowing early event buffering for late subscribers.
- Enhanced event handling logic to replay buffered events when the first subscriber connects, ensuring no events are lost due to race conditions.
- Updated comments for clarity on the new event synchronization mechanism and its benefits in both Redis and in-memory modes.
* fix: Update cache integration test command for stream to ensure proper execution
- Modified the test command for cache integration related to streams by adding the --forceExit flag to prevent hanging tests.
- This change enhances the reliability of the test suite by ensuring all tests complete as expected.
* feat: Add active job management for user and show progress in conversation list
- Implemented a new endpoint to retrieve active generation job IDs for the current user, enhancing user experience by allowing visibility of ongoing tasks.
- Integrated active job tracking in the Conversations component, displaying generation indicators based on active jobs.
- Optimized job management in the GenerationJobManager and InMemoryJobStore to support user-specific job queries, ensuring efficient resource handling and cleanup.
- Updated relevant components and hooks to utilize the new active jobs feature, improving overall application responsiveness and user feedback.
* feat: Implement active job tracking by user in RedisJobStore
- Added functionality to retrieve active job IDs for a specific user, enhancing user experience by allowing visibility of ongoing tasks.
- Implemented self-healing cleanup for stale job entries, ensuring accurate tracking of active jobs.
- Updated job creation, update, and deletion methods to manage user-specific job sets effectively.
- Enhanced integration tests to validate the new user-specific job management features.
* refactor: Simplify job deletion logic by removing user job cleanup from InMemoryJobStore and RedisJobStore
* WIP: Add backend inspect script for easier debugging in production
* refactor: title generation logic
- Changed the title generation endpoint from POST to GET, allowing for more efficient retrieval of titles based on conversation ID.
- Implemented exponential backoff for title fetching retries, improving responsiveness and reducing server load.
- Introduced a queuing mechanism for title generation, ensuring titles are generated only after job completion.
- Updated relevant components and hooks to utilize the new title generation logic, enhancing user experience and application performance.
* feat: Enhance updateConvoInAllQueries to support moving conversations to the top
* chore: temp. remove added multi convo
* refactor: Update active jobs query integration for optimistic updates on abort
- Introduced a new interface for active jobs response to standardize data handling.
- Updated query keys for active jobs to ensure consistency across components.
- Enhanced job management logic in hooks to properly reflect active job states, improving overall application responsiveness.
* refactor: useResumableStreamToggle hook to manage resumable streams for legacy/assistants endpoints
- Introduced a new hook, useResumableStreamToggle, to automatically toggle resumable streams off for assistants endpoints and restore the previous value when switching away.
- Updated ChatView component to utilize the new hook, enhancing the handling of streaming behavior based on endpoint type.
- Refactored imports in ChatView for better organization.
* refactor: streamline conversation title generation handling
- Removed unused type definition for TGenTitleMutation in mutations.ts to clean up the codebase.
- Integrated queueTitleGeneration call in useEventHandlers to trigger title generation for new conversations, enhancing the responsiveness of the application.
* feat: Add USE_REDIS_STREAMS configuration for stream job storage
- Introduced USE_REDIS_STREAMS to control Redis usage for resumable stream job storage, defaulting to true if USE_REDIS is enabled but not explicitly set.
- Updated cacheConfig to include USE_REDIS_STREAMS and modified createStreamServices to utilize this new configuration.
- Enhanced unit tests to validate the behavior of USE_REDIS_STREAMS under various environment settings, ensuring correct defaults and overrides.
* fix: title generation queue management for assistants
- Introduced a queueListeners mechanism to notify changes in the title generation queue, improving responsiveness for non-resumable streams.
- Updated the useTitleGeneration hook to track queue changes with a queueVersion state, ensuring accurate updates when jobs complete.
- Refactored the queueTitleGeneration function to trigger listeners upon adding new conversation IDs, enhancing the overall title generation flow.
* refactor: streamline agent controller and remove legacy resumable handling
- Updated the AgentController to route all requests to ResumableAgentController, simplifying the logic.
- Deprecated the legacy non-resumable path, providing a clear migration path for future use.
- Adjusted setHeaders middleware to remove unnecessary checks for resumable mode.
- Cleaned up the useResumableSSE hook to eliminate redundant query parameters, enhancing clarity and performance.
* feat: Add USE_REDIS_STREAMS configuration to .env.example
- Updated .env.example to include USE_REDIS_STREAMS setting, allowing control over Redis usage for resumable LLM streams.
- Provided additional context on the behavior of USE_REDIS_STREAMS when not explicitly set, enhancing clarity for configuration management.
* refactor: remove unused setHeaders middleware from chat route
- Eliminated the setHeaders middleware from the chat route, streamlining the request handling process.
- This change contributes to cleaner code and improved performance by reducing unnecessary middleware checks.
* fix: Add streamId parameter for resumable stream handling across services (actions, mcp oauth)
* fix(flow): add immediate abort handling and fix intervalId initialization
- Add immediate abort handler that responds instantly to abort signal
- Declare intervalId before cleanup function to prevent 'Cannot access before initialization' error
- Consolidate cleanup logic into single function to avoid duplicate cleanup
- Properly remove abort event listener on cleanup
* fix(mcp): clean up OAuth flows on abort and simplify flow handling
- Add abort handler in reconnectServer to clean up mcp_oauth and mcp_get_tokens flows
- Update createAbortHandler to clean up both flow types on tool call abort
- Pass abort signal to createFlow in returnOnOAuth path
- Simplify handleOAuthRequired to always cancel existing flows and start fresh
- This ensures user always gets a new OAuth URL instead of waiting for stale flows
* fix(agents): handle 'new' conversationId and improve abort reliability
- Treat 'new' as placeholder that needs UUID in request controller
- Send JSON response immediately before tool loading for faster SSE connection
- Use job's abort controller instead of prelimAbortController
- Emit errors to stream if headers already sent
- Skip 'new' as valid ID in abort endpoint
- Add fallback to find active jobs by userId when conversationId is 'new'
* fix(stream): detect early abort and prevent navigation to non-existent conversation
- Abort controller on job completion to signal pending operations
- Detect early abort (no content, no responseMessageId) in abortJob
- Set conversation and responseMessage to null for early aborts
- Add earlyAbort flag to final event for frontend detection
- Remove unused text field from AbortResult interface
- Frontend handles earlyAbort by staying on/navigating to new chat
* test(mcp): update test to expect signal parameter in createFlow
fix(agents): include 'new' conversationId in newConvo check for title generation
When frontend sends 'new' as conversationId, it should still trigger
title generation since it's a new conversation. Rename boolean variable for clarity
fix(agents): check abort state before completeJob for title generation
completeJob now triggers abort signal for cleanup, so we need to
capture the abort state beforehand to correctly determine if title
generation should run.
2025-12-19 10:12:39 -05:00
|
|
|
undefined,
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
);
|
|
|
|
|
});
|
2025-08-13 09:45:06 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('connection retry logic', () => {
|
|
|
|
|
it('should establish connection successfully', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig, // Use default 5000ms timeout
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockResolvedValue(undefined);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
|
|
|
|
expect(mockConnectionInstance.connect).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle OAuth errors during connection attempts', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthError = new Error('Non-200 status code (401)');
|
|
|
|
|
(oauthError as unknown as Record<string, unknown>).isOAuthError = true;
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockRejectedValue(oauthError);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
await expect(MCPConnectionFactory.create(basicOptions, oauthOptions)).rejects.toThrow(
|
|
|
|
|
'Non-200 status code (401)',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('OAuth required, stopping connection attempts'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('isOAuthError method', () => {
|
|
|
|
|
it('should identify OAuth errors by message content', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const error401 = new Error('401 Unauthorized');
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockRejectedValue(error401);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
await expect(MCPConnectionFactory.create(basicOptions, oauthOptions)).rejects.toThrow('401');
|
|
|
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('OAuth required, stopping connection attempts'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-02-01 19:37:04 -05:00
|
|
|
|
|
|
|
|
describe('discoverTools static method', () => {
|
|
|
|
|
const mockTools = [
|
|
|
|
|
{ name: 'tool1', description: 'First tool', inputSchema: { type: 'object' } },
|
|
|
|
|
{ name: 'tool2', description: 'Second tool', inputSchema: { type: 'object' } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
it('should discover tools from a successfully connected server', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockResolvedValue(undefined);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
mockConnectionInstance.fetchTools = jest.fn().mockResolvedValue(mockTools);
|
|
|
|
|
|
|
|
|
|
const result = await MCPConnectionFactory.discoverTools(basicOptions);
|
|
|
|
|
|
|
|
|
|
expect(result.tools).toEqual(mockTools);
|
|
|
|
|
expect(result.oauthRequired).toBe(false);
|
|
|
|
|
expect(result.oauthUrl).toBeNull();
|
|
|
|
|
expect(result.connection).toBe(mockConnectionInstance);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should detect OAuth required without generating URL in discovery mode', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: {
|
|
|
|
|
...mockServerConfig,
|
|
|
|
|
url: 'https://api.example.com',
|
|
|
|
|
type: 'sse' as const,
|
|
|
|
|
} as t.SSEOptions,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockOAuthStart = jest.fn().mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser as unknown as IUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
oauthStart: mockOAuthStart,
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
mockConnectionInstance.disconnect = jest.fn().mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
let oauthHandler: (() => Promise<void>) | undefined;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthHandler = handler as () => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockImplementation(async () => {
|
|
|
|
|
if (oauthHandler) {
|
|
|
|
|
await oauthHandler();
|
|
|
|
|
}
|
|
|
|
|
throw new Error('OAuth required');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await MCPConnectionFactory.discoverTools(basicOptions, oauthOptions);
|
|
|
|
|
|
|
|
|
|
expect(result.connection).toBeNull();
|
|
|
|
|
expect(result.tools).toBeNull();
|
|
|
|
|
expect(result.oauthRequired).toBe(true);
|
|
|
|
|
expect(result.oauthUrl).toBeNull();
|
|
|
|
|
expect(mockOAuthStart).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null tools when discovery fails completely', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockRejectedValue(new Error('Connection failed'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
mockConnectionInstance.disconnect = jest.fn().mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
const result = await MCPConnectionFactory.discoverTools(basicOptions);
|
|
|
|
|
|
|
|
|
|
expect(result.tools).toBeNull();
|
|
|
|
|
expect(result.connection).toBeNull();
|
|
|
|
|
expect(result.oauthRequired).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle disconnect errors gracefully during cleanup', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockRejectedValue(new Error('Connection failed'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
mockConnectionInstance.disconnect = jest
|
|
|
|
|
.fn()
|
|
|
|
|
.mockRejectedValue(new Error('Disconnect failed'));
|
|
|
|
|
|
|
|
|
|
const result = await MCPConnectionFactory.discoverTools(basicOptions);
|
|
|
|
|
|
|
|
|
|
expect(result.tools).toBeNull();
|
|
|
|
|
expect(mockLogger.debug).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-08-13 09:45:06 -06:00
|
|
|
});
|