mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-31 15:48:51 +01:00
* Decouple mcp config from start up config * Chore: Work on AI Review and Copilot Comments - setRawConfig is not needed since the private raw config is not needed any more - !!serversLoading bug fixed - added unit tests for route /api/mcp/servers - copilot comments addressed * chore: remove comments * chore: rename data-provider dir for MCP * chore: reorganize mcp specific query hooks * fix: consolidate imports for MCP server manager * chore: add dev-staging branch to frontend review workflow triggers * feat: add GitHub Actions workflow for building and pushing Docker images to GitHub Container Registry and Docker Hub * fix: update label for tag input in BookmarkForm tests to improve clarity --------- Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com> Co-authored-by: Danny Avila <danny@librechat.ai>
143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
import type * as t from '~/mcp/types';
|
|
import {
|
|
ServerConfigsCacheFactory,
|
|
type ServerConfigsCache,
|
|
} from './cache/ServerConfigsCacheFactory';
|
|
import {
|
|
PrivateServerConfigsCache,
|
|
PrivateServerConfigsCacheFactory,
|
|
} from './cache/PrivateServerConfigs/PrivateServerConfigsCacheFactory';
|
|
|
|
/**
|
|
* Central registry for managing MCP server configurations across different scopes and users.
|
|
* Authoritative source of truth for all MCP servers provided by LibreChat.
|
|
*
|
|
* Maintains three-tier cache structure:
|
|
* - Shared App Servers: Auto-started servers available to all users (initialized at startup)
|
|
* - Shared User Servers: User-scope servers that require OAuth or on-demand startup
|
|
* - Private Servers: Per-user configurations dynamically added during runtime
|
|
*
|
|
* Provides a unified query interface with proper fallback hierarchy:
|
|
* checks shared app servers first, then shared user servers, then private user servers.
|
|
*/
|
|
class MCPServersRegistry {
|
|
public readonly sharedAppServers: ServerConfigsCache = ServerConfigsCacheFactory.create(
|
|
'App',
|
|
'Shared',
|
|
false,
|
|
);
|
|
|
|
public readonly sharedUserServers: ServerConfigsCache = ServerConfigsCacheFactory.create(
|
|
'User',
|
|
'Shared',
|
|
false,
|
|
);
|
|
|
|
public readonly privateServersCache: PrivateServerConfigsCache =
|
|
PrivateServerConfigsCacheFactory.create();
|
|
|
|
private rawConfigs: t.MCPServers = {};
|
|
|
|
public async getServerConfig(
|
|
serverName: string,
|
|
userId?: string,
|
|
): Promise<t.ParsedServerConfig | undefined> {
|
|
const sharedAppServer = await this.sharedAppServers.get(serverName);
|
|
if (sharedAppServer) return sharedAppServer;
|
|
|
|
if (userId) {
|
|
//we require user id to also access sharedServers to ensure that getServerConfig(serverName, undefined) returns only app level configs.
|
|
const sharedUserServer = await this.sharedUserServers.get(serverName);
|
|
if (sharedUserServer) return sharedUserServer;
|
|
|
|
const privateUserServer = await this.privateServersCache.get(userId, serverName);
|
|
if (privateUserServer) return privateUserServer;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
public async getAllServerConfigs(userId?: string): Promise<Record<string, t.ParsedServerConfig>> {
|
|
const privateConfigs = userId ? await this.privateServersCache.getAll(userId) : {};
|
|
const registryConfigs = {
|
|
...(await this.sharedAppServers.getAll()),
|
|
...(await this.sharedUserServers.getAll()),
|
|
...privateConfigs,
|
|
};
|
|
/** Include all raw configs, but registry configs take precedence (they have inspection data) */
|
|
const allConfigs: Record<string, t.ParsedServerConfig> = {};
|
|
for (const serverName in this.rawConfigs) {
|
|
allConfigs[serverName] = this.rawConfigs[serverName] as t.ParsedServerConfig;
|
|
}
|
|
|
|
/** Override with registry configs where available (they have richer data) */
|
|
for (const serverName in registryConfigs) {
|
|
allConfigs[serverName] = registryConfigs[serverName];
|
|
}
|
|
|
|
return allConfigs;
|
|
}
|
|
|
|
// TODO: This is currently used to determine if a server requires OAuth. However, this info can
|
|
// can be determined through config.requiresOAuth. Refactor usages and remove this method.
|
|
public async getOAuthServers(userId?: string): Promise<Set<string>> {
|
|
const allServers = await this.getAllServerConfigs(userId);
|
|
const oauthServers = Object.entries(allServers).filter(([, config]) => config.requiresOAuth);
|
|
return new Set(oauthServers.map(([name]) => name));
|
|
}
|
|
|
|
/**
|
|
* Add a shared server configuration.
|
|
* Automatically routes to appropriate cache (app vs user) based on config properties.
|
|
* - Servers requiring OAuth or with startup=false → sharedUserServers
|
|
* - All other servers → sharedAppServers
|
|
*
|
|
* @param serverName - Name of the MCP server
|
|
* @param config - Parsed server configuration
|
|
*/
|
|
public async addSharedServer(serverName: string, config: t.ParsedServerConfig): Promise<void> {
|
|
if (config.requiresOAuth || config.startup === false) {
|
|
await this.sharedUserServers.add(serverName, config);
|
|
} else {
|
|
await this.sharedAppServers.add(serverName, config);
|
|
}
|
|
}
|
|
|
|
public async reset(): Promise<void> {
|
|
await this.sharedAppServers.reset();
|
|
await this.sharedUserServers.reset();
|
|
await this.privateServersCache.resetAll();
|
|
}
|
|
|
|
public async removeServer(serverName: string, userId?: string): Promise<void> {
|
|
const appServer = await this.sharedAppServers.get(serverName);
|
|
if (appServer) {
|
|
await this.sharedAppServers.remove(serverName);
|
|
return;
|
|
}
|
|
|
|
const userServer = await this.sharedUserServers.get(serverName);
|
|
if (userServer) {
|
|
await this.sharedUserServers.remove(serverName);
|
|
return;
|
|
}
|
|
|
|
if (userId) {
|
|
const privateServer = await this.privateServersCache.get(userId, serverName);
|
|
if (privateServer) {
|
|
await this.privateServersCache.remove(userId, serverName);
|
|
return;
|
|
}
|
|
} else {
|
|
const affectedUsers = await this.privateServersCache.findUsersWithServer(serverName);
|
|
if (affectedUsers.length > 0) {
|
|
await this.privateServersCache.removeServerConfigIfCacheExists(affectedUsers, serverName);
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Server ${serverName} not found`);
|
|
}
|
|
}
|
|
|
|
export const mcpServersRegistry = new MCPServersRegistry();
|