mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-20 09:24:10 +01:00
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* refactor: Restructure MCP registry system with caching - Split MCPServersRegistry into modular components: - MCPServerInspector: handles server inspection and health checks - MCPServersInitializer: manages server initialization logic - MCPServersRegistry: simplified registry coordination - Add distributed caching layer: - ServerConfigsCacheRedis: Redis-backed configuration cache - ServerConfigsCacheInMemory: in-memory fallback cache - RegistryStatusCache: distributed leader election state - Add promise utilities (withTimeout) replacing Promise.race patterns - Add comprehensive cache integration tests for all cache implementations - Remove unused MCPManager.getAllToolFunctions method * fix: Update OAuth flow to include user-specific headers * chore: Update Jest configuration to ignore additional test files - Added patterns to ignore files ending with .helper.ts and .helper.d.ts in testPathIgnorePatterns for cleaner test runs. * fix: oauth headers in callback * chore: Update Jest testPathIgnorePatterns to exclude helper files - Modified testPathIgnorePatterns in package.json to ignore files ending with .helper.ts and .helper.d.ts for cleaner test execution. * ci: update test mocks --------- Co-authored-by: Danny Avila <danny@librechat.ai>
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
import type * as t from '~/mcp/types';
|
|
import {
|
|
ServerConfigsCacheFactory,
|
|
type ServerConfigsCache,
|
|
} from './cache/ServerConfigsCacheFactory';
|
|
|
|
/**
|
|
* Central registry for managing MCP server configurations across different scopes and users.
|
|
* Maintains three categories of server configurations:
|
|
* - 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 User Servers: Per-user configurations dynamically added during runtime
|
|
*
|
|
* Provides a unified interface for retrieving server configs with proper fallback hierarchy:
|
|
* checks shared app servers first, then shared user servers, then private user servers.
|
|
* Handles server lifecycle operations including adding, removing, and querying configurations.
|
|
*/
|
|
class MCPServersRegistry {
|
|
public readonly sharedAppServers = ServerConfigsCacheFactory.create('App', true);
|
|
public readonly sharedUserServers = ServerConfigsCacheFactory.create('User', true);
|
|
private readonly privateUserServers: Map<string | undefined, ServerConfigsCache> = new Map();
|
|
|
|
public async addPrivateUserServer(
|
|
userId: string,
|
|
serverName: string,
|
|
config: t.ParsedServerConfig,
|
|
): Promise<void> {
|
|
if (!this.privateUserServers.has(userId)) {
|
|
const cache = ServerConfigsCacheFactory.create(`User(${userId})`, false);
|
|
this.privateUserServers.set(userId, cache);
|
|
}
|
|
await this.privateUserServers.get(userId)!.add(serverName, config);
|
|
}
|
|
|
|
public async updatePrivateUserServer(
|
|
userId: string,
|
|
serverName: string,
|
|
config: t.ParsedServerConfig,
|
|
): Promise<void> {
|
|
const userCache = this.privateUserServers.get(userId);
|
|
if (!userCache) throw new Error(`No private servers found for user "${userId}".`);
|
|
await userCache.update(serverName, config);
|
|
}
|
|
|
|
public async removePrivateUserServer(userId: string, serverName: string): Promise<void> {
|
|
await this.privateUserServers.get(userId)?.remove(serverName);
|
|
}
|
|
|
|
public async getServerConfig(
|
|
serverName: string,
|
|
userId?: string,
|
|
): Promise<t.ParsedServerConfig | undefined> {
|
|
const sharedAppServer = await this.sharedAppServers.get(serverName);
|
|
if (sharedAppServer) return sharedAppServer;
|
|
|
|
const sharedUserServer = await this.sharedUserServers.get(serverName);
|
|
if (sharedUserServer) return sharedUserServer;
|
|
|
|
const privateUserServer = await this.privateUserServers.get(userId)?.get(serverName);
|
|
if (privateUserServer) return privateUserServer;
|
|
|
|
return undefined;
|
|
}
|
|
|
|
public async getAllServerConfigs(userId?: string): Promise<Record<string, t.ParsedServerConfig>> {
|
|
return {
|
|
...(await this.sharedAppServers.getAll()),
|
|
...(await this.sharedUserServers.getAll()),
|
|
...((await this.privateUserServers.get(userId)?.getAll()) ?? {}),
|
|
};
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
public async reset(): Promise<void> {
|
|
await this.sharedAppServers.reset();
|
|
await this.sharedUserServers.reset();
|
|
for (const cache of this.privateUserServers.values()) {
|
|
await cache.reset();
|
|
}
|
|
this.privateUserServers.clear();
|
|
}
|
|
}
|
|
|
|
export const mcpServersRegistry = new MCPServersRegistry();
|