import { logger } from '@librechat/data-schemas'; import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; import { MCPConnection } from './connection'; import { mcpServersRegistry as registry } from '~/mcp/registry/MCPServersRegistry'; import type * as t from './types'; /** * Manages MCP connections with lazy loading and reconnection. * Maintains a pool of connections and handles connection lifecycle management. * Queries server configurations dynamically from the MCPServersRegistry (single source of truth). * * Scope-aware: Each repository is tied to a specific owner scope: * - ownerId = undefined → manages app-level servers only * - ownerId = userId → manages user-level and private servers for that user */ export class ConnectionsRepository { protected connections: Map = new Map(); protected oauthOpts: t.OAuthConnectionOptions | undefined; private readonly ownerId: string | undefined; constructor(ownerId?: string, oauthOpts?: t.OAuthConnectionOptions) { this.ownerId = ownerId; this.oauthOpts = oauthOpts; } /** Checks whether this repository can connect to a specific server */ async has(serverName: string): Promise { const config = await registry.getServerConfig(serverName, this.ownerId); if (!config) { //if the config does not exist, clean up any potential orphaned connections (caused by server tier migration) await this.disconnect(serverName); } return !!config; } /** Gets or creates a connection for the specified server with lazy loading */ async get(serverName: string): Promise { const serverConfig = await registry.getServerConfig(serverName, this.ownerId); const existingConnection = this.connections.get(serverName); if (!serverConfig) { if (existingConnection) { await existingConnection.disconnect(); } return null; } if (existingConnection) { // Check if config was cached/updated since connection was created if (serverConfig.lastUpdatedAt && existingConnection.isStale(serverConfig.lastUpdatedAt)) { logger.info( `${this.prefix(serverName)} Existing connection for ${serverName} is outdated. Recreating a new connection.`, { connectionCreated: new Date(existingConnection.createdAt).toISOString(), configCachedAt: new Date(serverConfig.lastUpdatedAt).toISOString(), }, ); // Disconnect stale connection await existingConnection.disconnect(); this.connections.delete(serverName); // Fall through to create new connection } else if (await existingConnection.isConnected()) { return existingConnection; } else { await this.disconnect(serverName); } } const connection = await MCPConnectionFactory.create( { serverName, serverConfig, }, this.oauthOpts, ); this.connections.set(serverName, connection); return connection; } /** Gets or creates connections for multiple servers concurrently */ async getMany(serverNames: string[]): Promise> { const connectionPromises = serverNames.map(async (name) => [name, await this.get(name)]); const connections = await Promise.all(connectionPromises); return new Map((connections as [string, MCPConnection][]).filter((v) => !!v[1])); } /** Returns all currently loaded connections without creating new ones */ async getLoaded(): Promise> { return this.getMany(Array.from(this.connections.keys())); } /** Gets or creates connections for all configured servers in this repository's scope */ async getAll(): Promise> { //TODO in the future we should use a scoped config getter (APPLevel, UserLevel, Private) //for now the unexisting config will not throw error const allConfigs = await registry.getAllServerConfigs(this.ownerId); return this.getMany(Object.keys(allConfigs)); } /** Disconnects and removes a specific server connection from the pool */ disconnect(serverName: string): Promise { 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[] { const serverNames = Array.from(this.connections.keys()); return serverNames.map((serverName) => this.disconnect(serverName)); } // Returns formatted log prefix for server messages protected prefix(serverName: string): string { return `[MCP][${serverName}]`; } }