LibreChat/packages/api/src/mcp/registry/cache/ServerConfigsCacheInMemory.ts
Atef Bellaaj ac68e629e6
📡 refactor: MCP Runtime Config Sync with Redis Distributed Locking (#10352)
* 🔄 Refactoring: MCP Runtime Configuration Reload
 - PrivateServerConfigs own cache classes (inMemory and Redis).
 - Connections staleness detection by comparing (connection.createdAt and config.LastUpdatedAt)
 - ConnectionsRepo access Registry instead of in memory config dict and renew stale connections
 - MCPManager: adjusted init of ConnectionsRepo (app level)
 - UserConnectionManager: renew stale connections
 - skipped test, to test "should only clear keys in its own namespace"
 - MCPPrivateServerLoader: new component to manage logic of loading / editing private servers on runtime
 - PrivateServersLoadStatusCache to track private server cache status
 - New unit and integration tests.
Misc:
 - add es lint rule to enforce line between class methods

* Fix cluster mode batch update and delete workarround. Fixed unit tests for cluster mode.

* Fix Keyv redis clear cache namespace  awareness issue + Integration tests fixes

* chore: address copilot comments

* Fixing rebase issue: removed the mcp config fallback in single getServerConfig method:
- to not to interfere with the logic of the right Tier (APP/USER/Private)
- If userId is null, the getServerConfig should not return configs that are a SharedUser tier and not APP tier

* chore: add dev-staging branch to workflow triggers for backend, cache integration, and ESLint checks

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
2025-12-11 16:36:15 -05:00

62 lines
2.3 KiB
TypeScript

import { ParsedServerConfig } from '~/mcp/types';
/**
* In-memory implementation of MCP server configurations cache for single-instance deployments.
* Uses a native JavaScript Map for fast, local storage without Redis dependencies.
* Suitable for development environments or single-server production deployments.
* Does not require leader checks or distributed coordination since data is instance-local.
* Data is lost on server restart and not shared across multiple server instances.
*/
export class ServerConfigsCacheInMemory {
private readonly cache: Map<string, ParsedServerConfig> = new Map();
public async add(serverName: string, config: ParsedServerConfig): Promise<void> {
if (this.cache.has(serverName))
throw new Error(
`Server "${serverName}" already exists in cache. Use update() to modify existing configs.`,
);
this.cache.set(serverName, { ...config, lastUpdatedAt: Date.now() });
}
public async update(serverName: string, config: ParsedServerConfig): Promise<void> {
if (!this.cache.has(serverName))
throw new Error(
`Server "${serverName}" does not exist in cache. Use add() to create new configs.`,
);
this.cache.set(serverName, { ...config, lastUpdatedAt: Date.now() });
}
/**
* Sets a server config without checking if it exists (upsert operation).
* Use this for bulk operations where you want to add or update without error handling.
*/
public async set(serverName: string, config: ParsedServerConfig): Promise<void> {
this.cache.set(serverName, { ...config, lastUpdatedAt: Date.now() });
}
public async remove(serverName: string): Promise<void> {
if (!this.cache.delete(serverName)) {
throw new Error(`Failed to remove server "${serverName}" in cache.`);
}
}
public async get(serverName: string): Promise<ParsedServerConfig | undefined> {
return this.cache.get(serverName);
}
public async getAll(): Promise<Record<string, ParsedServerConfig>> {
return Object.fromEntries(this.cache);
}
public async reset(): Promise<void> {
this.cache.clear();
}
/**
* Returns a placeholder namespace for consistency with Redis implementation.
* In-memory cache doesn't use namespaces, so this always returns empty string.
*/
public getNamespace(): string {
return '';
}
}