mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
♻️ refactor: MCPManager for Scalability, Fix App-Level Detection, Add Lazy Connections (#8930)
* feat: MCP Connection management overhaul - Making MCPManager manageable Refactor the monolithic MCPManager into focused, single-responsibility classes: • MCPServersRegistry: Server configuration discovery and metadata management • UserConnectionManager: Manages user-level connections • ConnectionsRepository: Low-level connection pool with lazy loading • MCPConnectionFactory: Handles MCP connection creation with OAuth support New Features: • Lazy loading of app-level connections for horizontal scaling • Automatic reconnection for app-level connections • Enhanced OAuth detection with explicit requiresOAuth flag • Centralized MCP configuration management Bug Fixes: • App-level connection detection in MCPManager.callTool • MCP Connection Reinitialization route behavior Optimizations: • MCPConnection.isConnected() caching to reduce overhead • Concurrent server metadata retrieval instead of sequential This refactoring addresses scalability bottlenecks and improves reliability while maintaining backward compatibility with existing configurations. * feat: Enabled import order in eslint. * # Moved tests to __tests__ folder # added tests for MCPServersRegistry.ts * # Add unit tests for ConnectionsRepository functionality * # Add unit tests for MCPConnectionFactory functionality * # Reorganize MCP connection tests and improve error handling * # reordering imports * # Update testPathIgnorePatterns in jest.config.mjs to exclude development TypeScript files * # removed mcp/manager.ts
This commit is contained in:
parent
9dbf153489
commit
8780a78165
32 changed files with 2571 additions and 1468 deletions
87
packages/api/src/mcp/ConnectionsRepository.ts
Normal file
87
packages/api/src/mcp/ConnectionsRepository.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import { MCPConnectionFactory, OAuthConnectionOptions } from '~/mcp/MCPConnectionFactory';
|
||||
import { MCPConnection } from './connection';
|
||||
import type * as t from './types';
|
||||
|
||||
/**
|
||||
* Manages MCP connections with lazy loading and reconnection.
|
||||
* Maintains a pool of connections and handles connection lifecycle management.
|
||||
*/
|
||||
export class ConnectionsRepository {
|
||||
protected readonly serverConfigs: Record<string, t.MCPOptions>;
|
||||
protected connections: Map<string, MCPConnection> = new Map();
|
||||
protected oauthOpts: OAuthConnectionOptions | undefined;
|
||||
|
||||
constructor(serverConfigs: t.MCPServers, oauthOpts?: OAuthConnectionOptions) {
|
||||
this.serverConfigs = serverConfigs;
|
||||
this.oauthOpts = oauthOpts;
|
||||
}
|
||||
|
||||
/** Checks whether this repository can connect to a specific server */
|
||||
has(serverName: string): boolean {
|
||||
return !!this.serverConfigs[serverName];
|
||||
}
|
||||
|
||||
/** Gets or creates a connection for the specified server with lazy loading */
|
||||
async get(serverName: string): Promise<MCPConnection> {
|
||||
const existingConnection = this.connections.get(serverName);
|
||||
if (existingConnection && (await existingConnection.isConnected())) return existingConnection;
|
||||
else await this.disconnect(serverName);
|
||||
|
||||
const connection = await MCPConnectionFactory.create(
|
||||
{
|
||||
serverName,
|
||||
serverConfig: this.getServerConfig(serverName),
|
||||
},
|
||||
this.oauthOpts,
|
||||
);
|
||||
|
||||
this.connections.set(serverName, connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/** Gets or creates connections for multiple servers concurrently */
|
||||
async getMany(serverNames: string[]): Promise<Map<string, MCPConnection>> {
|
||||
const connectionPromises = serverNames.map(async (name) => [name, await this.get(name)]);
|
||||
const connections = await Promise.all(connectionPromises);
|
||||
return new Map(connections as [string, MCPConnection][]);
|
||||
}
|
||||
|
||||
/** Returns all currently loaded connections without creating new ones */
|
||||
async getLoaded(): Promise<Map<string, MCPConnection>> {
|
||||
return this.getMany(Array.from(this.connections.keys()));
|
||||
}
|
||||
|
||||
/** Gets or creates connections for all configured servers */
|
||||
async getAll(): Promise<Map<string, MCPConnection>> {
|
||||
return this.getMany(Object.keys(this.serverConfigs));
|
||||
}
|
||||
|
||||
/** Disconnects and removes a specific server connection from the pool */
|
||||
disconnect(serverName: string): Promise<void> {
|
||||
const connection = this.connections.get(serverName);
|
||||
if (!connection) return Promise.resolve();
|
||||
this.connections.delete(serverName);
|
||||
return connection.disconnect().catch((err) => {
|
||||
logger.error(`${this.prefix(serverName)} Error disconnecting`, err);
|
||||
});
|
||||
}
|
||||
|
||||
/** Disconnects all active connections and returns array of disconnect promises */
|
||||
disconnectAll(): Promise<void>[] {
|
||||
const serverNames = Array.from(this.connections.keys());
|
||||
return serverNames.map((serverName) => this.disconnect(serverName));
|
||||
}
|
||||
|
||||
// Retrieves server configuration by name or throws if not found
|
||||
protected getServerConfig(serverName: string): t.MCPOptions {
|
||||
const serverConfig = this.serverConfigs[serverName];
|
||||
if (serverConfig) return serverConfig;
|
||||
throw new Error(`${this.prefix(serverName)} Server not found in configuration`);
|
||||
}
|
||||
|
||||
// Returns formatted log prefix for server messages
|
||||
protected prefix(serverName: string): string {
|
||||
return `[MCP][${serverName}]`;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue