mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-20 20:20:42 +00:00
* 🗄️ fix: Gate Request-Scoped MCP Servers Out of Persistent Tool Cache PR #13626 established that request-scoped MCP servers (runtime OPENID/GRAPH/BODY placeholders) must not use the persistent 12h tool cache, but only gated three of five touchpoints. The panel endpoint still back-filled the cache and the OAuth callback still wrote to it, while agent loading read those entries ungated — pinning ephemeral model-spec/agent toolsets to stale definitions for up to 12h. Centralize the invariant in createMCPToolCacheService: a getServerConfig resolver dep gates both writers and a new service-owned getMCPServerTools read, so every current and future caller is covered. Callers that already hold the parsed config pass it to skip resolution; the per-call skipCache flag and duplicated call-site gates are removed in favor of the single config-based mechanism. Resolution failures fail open to preserve prior behavior. * 🩹 fix: Address Codex Review on Cache Gating - Repair getCachedTools.spec.js, which destructured the relocated getMCPServerTools directly from the module; its coverage now lives in the service-level tools.spec.ts. - Resolve the merged (Config-tier-aware) server config in the OAuth callback before writing tool definitions, so the cache gate detects request-scoped servers supplied via admin Config overlays that the base registry lookup cannot see. - Discover tools actively for request-scoped servers in the panel endpoint via ephemeral reinitialization: such servers have no stored app/user connections, so the previous getServerToolFunctions fallback returned an empty toolset once the cache read was gated. * 🧵 fix: Address Second Codex Review on Cache Gating - Resolve the merged server config before the OAuth callback reconnects, so the connection itself uses Config-tier overlays rather than only the subsequent cache write. - Pass Config-tier candidates into the panel's request-scoped discovery, matching the reinitialize route: reinitMCPServer forwards configServers (not the provided serverConfig) to its OAuth discovery fallback. - Document the accepted read-path trade-off: the gate resolver sees base configs only, all writers pass merged configs, so a pre-gating or overlay-divergent entry survives at most one cache TTL. * 🚏 chore: Rework Cache Gating for BODY-Only Request Scoping After #13673 narrowed requiresEphemeralUserConnection to BODY placeholders, the central gate follows the predicate unchanged, but the panel's active discovery no longer serves a purpose: the only remaining request-scoped class cannot connect outside a chat turn, so the reinitialization attempt would always fail at the missing-body check. Remove that path; OpenID/Graph servers are persistent user-scoped again and flow through the stored-connection and cache lookups as before. Flip test fixtures that used OPENID placeholders to denote request-scoped configs over to BODY placeholders. * 🪟 fix: Check Config Overlays in Agent-Loading Cache Reads The cache service's registry resolver sees only base YAML/DB configs, so a BODY placeholder introduced by a request-tier Config overlay was invisible to the gate on the agent-loading read path: model-spec and ephemeral-agent expansion could read a leftover persistent entry and pin stale concrete tool names instead of the mcp_all fresh-discovery path. Check the raw overlay candidate inline in loadEphemeralAgent and loadAddedAgent — a pure placeholder scan with no extra IO — and skip the cache read when the overlay makes the server request-scoped. Widen UserScopedConnectionConfig so raw (pre-inspection) configs qualify for the scoping predicates, which only check key presence. * 🧪 test: Guard Run-Scoped MCP Definition Handoff Boundaries The original ClickHouse breaker storm regressed precisely at field pass-through boundaries that unit tests of each end could not see: initializeAgent dropping mcpAvailableTools from its destructure, and the agent tool context losing it on the way into ON_TOOL_EXECUTE. Add direct guards on both hops: the loadTools result must surface on the initialized agent, and the captured toolExecuteOptions closure must forward it to loadToolsForExecution.
89 lines
3 KiB
JavaScript
89 lines
3 KiB
JavaScript
const { CacheKeys, Time } = require('librechat-data-provider');
|
|
const getLogStores = require('~/cache/getLogStores');
|
|
|
|
/**
|
|
* Cache key generators for different tool access patterns
|
|
*/
|
|
const ToolCacheKeys = {
|
|
/** Global tools available to all users */
|
|
GLOBAL: 'tools:global',
|
|
/** MCP tools cached by user ID and server name */
|
|
MCP_SERVER: (userId, serverName) => `tools:mcp:${userId}:${serverName}`,
|
|
};
|
|
|
|
/**
|
|
* Retrieves available tools from cache
|
|
* @function getCachedTools
|
|
* @param {Object} options - Options for retrieving tools
|
|
* @param {string} [options.userId] - User ID for user-specific MCP tools
|
|
* @param {string} [options.serverName] - MCP server name to get cached tools for
|
|
* @returns {Promise<LCAvailableTools|null>} The available tools object or null if not cached
|
|
*/
|
|
async function getCachedTools(options = {}) {
|
|
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
|
const { userId, serverName } = options;
|
|
|
|
// Return MCP server-specific tools if requested
|
|
if (serverName && userId) {
|
|
return await cache.get(ToolCacheKeys.MCP_SERVER(userId, serverName));
|
|
}
|
|
|
|
// Default to global tools
|
|
return await cache.get(ToolCacheKeys.GLOBAL);
|
|
}
|
|
|
|
/**
|
|
* Sets available tools in cache
|
|
* @function setCachedTools
|
|
* @param {Object} tools - The tools object to cache
|
|
* @param {Object} options - Options for caching tools
|
|
* @param {string} [options.userId] - User ID for user-specific MCP tools
|
|
* @param {string} [options.serverName] - MCP server name for server-specific tools
|
|
* @param {number} [options.ttl] - Time to live in milliseconds (default: 12 hours)
|
|
* @returns {Promise<boolean>} Whether the operation was successful
|
|
*/
|
|
async function setCachedTools(tools, options = {}) {
|
|
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
|
const { userId, serverName, ttl = Time.TWELVE_HOURS } = options;
|
|
|
|
// Cache by MCP server if specified (requires userId)
|
|
if (serverName && userId) {
|
|
return await cache.set(ToolCacheKeys.MCP_SERVER(userId, serverName), tools, ttl);
|
|
}
|
|
|
|
// Default to global cache
|
|
return await cache.set(ToolCacheKeys.GLOBAL, tools, ttl);
|
|
}
|
|
|
|
/**
|
|
* Invalidates cached tools
|
|
* @function invalidateCachedTools
|
|
* @param {Object} options - Options for invalidating tools
|
|
* @param {string} [options.userId] - User ID for user-specific MCP tools
|
|
* @param {string} [options.serverName] - MCP server name to invalidate
|
|
* @param {boolean} [options.invalidateGlobal=false] - Whether to invalidate global tools
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function invalidateCachedTools(options = {}) {
|
|
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
|
const { userId, serverName, invalidateGlobal = false } = options;
|
|
|
|
const keysToDelete = [];
|
|
|
|
if (invalidateGlobal) {
|
|
keysToDelete.push(ToolCacheKeys.GLOBAL);
|
|
}
|
|
|
|
if (serverName && userId) {
|
|
keysToDelete.push(ToolCacheKeys.MCP_SERVER(userId, serverName));
|
|
}
|
|
|
|
await Promise.all(keysToDelete.map((key) => cache.delete(key)));
|
|
}
|
|
|
|
module.exports = {
|
|
ToolCacheKeys,
|
|
getCachedTools,
|
|
setCachedTools,
|
|
invalidateCachedTools,
|
|
};
|