2025-08-13 09:45:06 -06:00
import { logger } from '@librechat/data-schemas' ;
2025-08-16 20:45:55 -04:00
import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory' ;
2025-08-13 09:45:06 -06:00
import { MCPConnection } from './connection' ;
2025-11-26 15:11:36 +01:00
import { mcpServersRegistry as registry } from '~/mcp/registry/MCPServersRegistry' ;
2025-08-13 09:45:06 -06:00
import type * as t from './types' ;
/ * *
* Manages MCP connections with lazy loading and reconnection .
* Maintains a pool of connections and handles connection lifecycle management .
2025-11-26 15:11:36 +01:00
* 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
2025-08-13 09:45:06 -06:00
* /
export class ConnectionsRepository {
protected connections : Map < string , MCPConnection > = new Map ( ) ;
2025-08-16 20:45:55 -04:00
protected oauthOpts : t.OAuthConnectionOptions | undefined ;
2025-11-26 15:11:36 +01:00
private readonly ownerId : string | undefined ;
2025-08-13 09:45:06 -06:00
2025-11-26 15:11:36 +01:00
constructor ( ownerId? : string , oauthOpts? : t.OAuthConnectionOptions ) {
this . ownerId = ownerId ;
2025-08-13 09:45:06 -06:00
this . oauthOpts = oauthOpts ;
}
/** Checks whether this repository can connect to a specific server */
2025-11-26 15:11:36 +01:00
async has ( serverName : string ) : Promise < boolean > {
const config = await registry . getServerConfig ( serverName , this . ownerId ) ;
2025-11-28 16:07:09 +01:00
const canConnect = ! ! config && this . isAllowedToConnectToServer ( config ) ;
if ( ! canConnect ) {
//if connection is no longer possible we attempt to disconnect any leftover connections
2025-11-26 15:11:36 +01:00
await this . disconnect ( serverName ) ;
}
2025-11-28 16:07:09 +01:00
return canConnect ;
2025-08-13 09:45:06 -06:00
}
/** Gets or creates a connection for the specified server with lazy loading */
2025-11-26 15:11:36 +01:00
async get ( serverName : string ) : Promise < MCPConnection | null > {
const serverConfig = await registry . getServerConfig ( serverName , this . ownerId ) ;
2025-11-28 16:07:09 +01:00
2025-08-13 09:45:06 -06:00
const existingConnection = this . connections . get ( serverName ) ;
2025-11-28 16:07:09 +01:00
if ( ! serverConfig || ! this . isAllowedToConnectToServer ( serverConfig ) ) {
2025-11-26 15:11:36 +01:00
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 ) ;
}
}
2025-08-13 09:45:06 -06:00
const connection = await MCPConnectionFactory . create (
{
serverName ,
2025-11-26 15:11:36 +01:00
serverConfig ,
2025-08-13 09:45:06 -06:00
} ,
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 ) ;
2025-11-26 15:11:36 +01:00
return new Map ( ( connections as [ string , MCPConnection ] [ ] ) . filter ( ( v ) = > ! ! v [ 1 ] ) ) ;
2025-08-13 09:45:06 -06:00
}
/** Returns all currently loaded connections without creating new ones */
async getLoaded ( ) : Promise < Map < string , MCPConnection > > {
return this . getMany ( Array . from ( this . connections . keys ( ) ) ) ;
}
2025-11-26 15:11:36 +01:00
/** Gets or creates connections for all configured servers in this repository's scope */
2025-08-13 09:45:06 -06:00
async getAll ( ) : Promise < Map < string , MCPConnection > > {
2025-11-26 15:11:36 +01:00
//TODO in the future we should use a scoped config getter (APPLevel, UserLevel, Private)
2025-11-28 16:07:09 +01:00
//for now the absent config will not throw error
2025-11-26 15:11:36 +01:00
const allConfigs = await registry . getAllServerConfigs ( this . ownerId ) ;
return this . getMany ( Object . keys ( allConfigs ) ) ;
2025-08-13 09:45:06 -06:00
}
/** Disconnects and removes a specific server connection from the pool */
2025-11-28 16:07:09 +01:00
async disconnect ( serverName : string ) : Promise < void > {
2025-08-13 09:45:06 -06:00
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 ) ) ;
}
// Returns formatted log prefix for server messages
protected prefix ( serverName : string ) : string {
return ` [MCP][ ${ serverName } ] ` ;
}
2025-11-28 16:07:09 +01:00
private isAllowedToConnectToServer ( config : t.ParsedServerConfig ) {
//the repository is not allowed to be connected in case the Connection repository is shared (ownerId is undefined/null) and the server requires Auth or startup false.
if ( this . ownerId === undefined && ( config . startup === false || config . requiresOAuth ) ) {
return false ;
}
return true ;
}
2025-08-13 09:45:06 -06:00
}