From 5b1a31ef4d18cb01f45f791a24e99d15ab7432c4 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 21 Sep 2025 16:52:43 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20refactor:=20Optimize=20MCP=20Too?= =?UTF-8?q?l=20Initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔄 refactor: Optimize MCP Tool Initialization fix: update tool caching to use separated mcp logic refactor: Replace `req.user` with `userId` in MCP handling functions refactor: Replace `req` parameter with `userId` in file search tool functions fix: Update user connection parameter to use object format in reinitMCPServer refactor: Simplify MCP tool creation logic and improve handling of tool configurations to avoid capturing too much in closures refactor: ensure MCP available tools are fetched from cache only when needed --- api/app/clients/tools/util/fileSearch.js | 6 +- api/app/clients/tools/util/handleTools.js | 82 +++++++++++-------- api/models/Agent.js | 24 ++---- api/server/controllers/UserController.js | 5 +- api/server/controllers/agents/v1.js | 2 +- api/server/controllers/assistants/v1.js | 4 +- api/server/controllers/assistants/v2.js | 4 +- api/server/controllers/mcp.js | 7 +- api/server/routes/mcp.js | 10 +-- api/server/services/Config/getCachedTools.js | 11 --- api/server/services/Config/mcp.js | 37 --------- api/server/services/MCP.js | 43 ++++++---- api/server/services/ToolService.js | 2 +- api/server/services/Tools/mcp.js | 6 +- .../app/clients/tools/util/fileSearch.test.js | 2 +- 15 files changed, 111 insertions(+), 134 deletions(-) diff --git a/api/app/clients/tools/util/fileSearch.js b/api/app/clients/tools/util/fileSearch.js index f51004cd98..01e6384c94 100644 --- a/api/app/clients/tools/util/fileSearch.js +++ b/api/app/clients/tools/util/fileSearch.js @@ -68,19 +68,19 @@ const primeFiles = async (options) => { /** * * @param {Object} options - * @param {ServerRequest} options.req + * @param {string} options.userId * @param {Array<{ file_id: string; filename: string }>} options.files * @param {string} [options.entity_id] * @param {boolean} [options.fileCitations=false] - Whether to include citation instructions * @returns */ -const createFileSearchTool = async ({ req, files, entity_id, fileCitations = false }) => { +const createFileSearchTool = async ({ userId, files, entity_id, fileCitations = false }) => { return tool( async ({ query }) => { if (files.length === 0) { return 'No files to search. Instruct the user to add files for the search.'; } - const jwtToken = generateShortLivedToken(req.user.id); + const jwtToken = generateShortLivedToken(userId); if (!jwtToken) { return 'There was an error authenticating the file search request.'; } diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 999d7bfc79..8bd4a46cfe 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -33,7 +33,7 @@ const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSe const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { createMCPTool, createMCPTools } = require('~/server/services/MCP'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); -const { getCachedTools } = require('~/server/services/Config'); +const { getMCPServerTools } = require('~/server/services/Config'); const { getRoleByName } = require('~/models/Role'); /** @@ -250,7 +250,6 @@ const loadTools = async ({ /** @type {Record} */ const toolContextMap = {}; - const cachedTools = (await getCachedTools({ userId: user, includeGlobal: true })) ?? {}; const requestedMCPTools = {}; for (const tool of tools) { @@ -307,7 +306,7 @@ const loadTools = async ({ } return createFileSearchTool({ - req: options.req, + userId: user, files, entity_id: agent?.id, fileCitations, @@ -340,7 +339,7 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} }); }; continue; - } else if (tool && cachedTools && mcpToolPattern.test(tool)) { + } else if (tool && mcpToolPattern.test(tool)) { const [toolName, serverName] = tool.split(Constants.mcp_delimiter); if (toolName === Constants.mcp_server) { /** Placeholder used for UI purposes */ @@ -353,33 +352,21 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} continue; } if (toolName === Constants.mcp_all) { - const currentMCPGenerator = async (index) => - createMCPTools({ - req: options.req, - res: options.res, - index, + requestedMCPTools[serverName] = [ + { + type: 'all', serverName, - userMCPAuthMap, - model: agent?.model ?? model, - provider: agent?.provider ?? endpoint, - signal, - }); - requestedMCPTools[serverName] = [currentMCPGenerator]; + }, + ]; continue; } - const currentMCPGenerator = async (index) => - createMCPTool({ - index, - req: options.req, - res: options.res, - toolKey: tool, - userMCPAuthMap, - model: agent?.model ?? model, - provider: agent?.provider ?? endpoint, - signal, - }); + requestedMCPTools[serverName] = requestedMCPTools[serverName] || []; - requestedMCPTools[serverName].push(currentMCPGenerator); + requestedMCPTools[serverName].push({ + type: 'single', + toolKey: tool, + serverName, + }); continue; } @@ -422,20 +409,51 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} const mcpToolPromises = []; /** MCP server tools are initialized sequentially by server */ let index = -1; - for (const [serverName, generators] of Object.entries(requestedMCPTools)) { + for (const [serverName, toolConfigs] of Object.entries(requestedMCPTools)) { index++; - for (const generator of generators) { + /** @type {LCAvailableTools} */ + let availableTools; + for (const config of toolConfigs) { try { - if (generator && generators.length === 1) { + const mcpParams = { + res: options.res, + userId: user, + index, + serverName: config.serverName, + userMCPAuthMap, + model: agent?.model ?? model, + provider: agent?.provider ?? endpoint, + signal, + }; + + if (config.type === 'all' && toolConfigs.length === 1) { + /** Handle async loading for single 'all' tool config */ mcpToolPromises.push( - generator(index).catch((error) => { + createMCPTools(mcpParams).catch((error) => { logger.error(`Error loading ${serverName} tools:`, error); return null; }), ); continue; } - const mcpTool = await generator(index); + if (!availableTools) { + try { + availableTools = await getMCPServerTools(serverName); + } catch (error) { + logger.error(`Error fetching available tools for MCP server ${serverName}:`, error); + } + } + + /** Handle synchronous loading */ + const mcpTool = + config.type === 'all' + ? await createMCPTools(mcpParams) + : await createMCPTool({ + ...mcpParams, + availableTools, + toolKey: config.toolKey, + }); + if (Array.isArray(mcpTool)) { loadedTools.push(...mcpTool); } else if (mcpTool) { diff --git a/api/models/Agent.js b/api/models/Agent.js index b506c58f0a..5468293523 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -11,7 +11,7 @@ const { getProjectByName, } = require('./Project'); const { removeAllPermissions } = require('~/server/services/PermissionService'); -const { getCachedTools } = require('~/server/services/Config'); +const { getMCPServerTools } = require('~/server/services/Config'); const { getActions } = require('./Action'); const { Agent } = require('~/db/models'); @@ -69,8 +69,6 @@ const getAgents = async (searchParameter) => await Agent.find(searchParameter).l */ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => { const { model, ...model_parameters } = _m; - /** @type {Record} */ - const availableTools = await getCachedTools({ userId: req.user.id, includeGlobal: true }); /** @type {TEphemeralAgent | null} */ const ephemeralAgent = req.body.ephemeralAgent; const mcpServers = new Set(ephemeralAgent?.mcp); @@ -88,22 +86,18 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _ const addedServers = new Set(); if (mcpServers.size > 0) { - for (const toolName of Object.keys(availableTools)) { - if (!toolName.includes(mcp_delimiter)) { - continue; - } - const mcpServer = toolName.split(mcp_delimiter)?.[1]; - if (mcpServer && mcpServers.has(mcpServer)) { - addedServers.add(mcpServer); - tools.push(toolName); - } - } - for (const mcpServer of mcpServers) { if (addedServers.has(mcpServer)) { continue; } - tools.push(`${mcp_all}${mcp_delimiter}${mcpServer}`); + const serverTools = await getMCPServerTools(mcpServer); + if (!serverTools) { + tools.push(`${mcp_all}${mcp_delimiter}${mcpServer}`); + addedServers.add(mcpServer); + continue; + } + tools.push(...Object.keys(serverTools)); + addedServers.add(mcpServer); } } diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 5d8dd9a5d2..c7051f4608 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -22,11 +22,11 @@ const { const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService'); const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService'); -const { getAppConfig, clearMCPServerTools } = require('~/server/services/Config'); const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud'); const { processDeleteRequest } = require('~/server/services/Files/process'); const { Transaction, Balance, User, Token } = require('~/db/models'); const { getMCPManager, getFlowStateManager } = require('~/config'); +const { getAppConfig } = require('~/server/services/Config'); const { deleteToolCalls } = require('~/models/ToolCall'); const { getLogStores } = require('~/cache'); @@ -372,9 +372,6 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { const flowId = MCPOAuthHandler.generateFlowId(userId, serverName); await flowManager.deleteFlow(flowId, 'mcp_get_tokens'); await flowManager.deleteFlow(flowId, 'mcp_oauth'); - - // 6. clear the tools cache for the server - await clearMCPServerTools({ userId, serverName }); }; module.exports = { diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 0334d965db..d623603a28 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -71,7 +71,7 @@ const createAgentHandler = async (req, res) => { agentData.author = userId; agentData.tools = []; - const availableTools = await getCachedTools({ includeGlobal: true }); + const availableTools = await getCachedTools(); for (const tool of tools) { if (availableTools[tool]) { agentData.tools.push(tool); diff --git a/api/server/controllers/assistants/v1.js b/api/server/controllers/assistants/v1.js index 1fb872f716..e2fbbe5b34 100644 --- a/api/server/controllers/assistants/v1.js +++ b/api/server/controllers/assistants/v1.js @@ -31,7 +31,7 @@ const createAssistant = async (req, res) => { delete assistantData.conversation_starters; delete assistantData.append_current_datetime; - const toolDefinitions = await getCachedTools({ includeGlobal: true }); + const toolDefinitions = await getCachedTools(); assistantData.tools = tools .map((tool) => { @@ -136,7 +136,7 @@ const patchAssistant = async (req, res) => { ...updateData } = req.body; - const toolDefinitions = await getCachedTools({ includeGlobal: true }); + const toolDefinitions = await getCachedTools(); updateData.tools = (updateData.tools ?? []) .map((tool) => { diff --git a/api/server/controllers/assistants/v2.js b/api/server/controllers/assistants/v2.js index 824f58d268..278dd13021 100644 --- a/api/server/controllers/assistants/v2.js +++ b/api/server/controllers/assistants/v2.js @@ -28,7 +28,7 @@ const createAssistant = async (req, res) => { delete assistantData.conversation_starters; delete assistantData.append_current_datetime; - const toolDefinitions = await getCachedTools({ includeGlobal: true }); + const toolDefinitions = await getCachedTools(); assistantData.tools = tools .map((tool) => { @@ -125,7 +125,7 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { let hasFileSearch = false; for (const tool of updateData.tools ?? []) { - const toolDefinitions = await getCachedTools({ includeGlobal: true }); + const toolDefinitions = await getCachedTools(); let actualTool = typeof tool === 'string' ? toolDefinitions[tool] : tool; if (!actualTool && manifestToolMap[tool] && manifestToolMap[tool].toolkit === true) { diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js index 40607a5840..17c710dae3 100644 --- a/api/server/controllers/mcp.js +++ b/api/server/controllers/mcp.js @@ -5,7 +5,11 @@ const { logger } = require('@librechat/data-schemas'); const { Constants } = require('librechat-data-provider'); const { convertMCPToolToPlugin } = require('@librechat/api'); -const { getAppConfig, getMCPServerTools } = require('~/server/services/Config'); +const { + cacheMCPServerTools, + getMCPServerTools, + getAppConfig, +} = require('~/server/services/Config'); const { getMCPManager } = require('~/config'); /** @@ -49,7 +53,6 @@ const getMCPTools = async (req, res) => { // Cache server tools if found if (Object.keys(serverTools).length > 0) { - const { cacheMCPServerTools } = require('~/server/services/Config'); await cacheMCPServerTools({ serverName, serverTools }); } } diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index fa0166830b..d4aa225f23 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -300,9 +300,9 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => { router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { try { const { serverName } = req.params; - const user = req.user; + const userId = req.user?.id; - if (!user?.id) { + if (!userId) { return res.status(401).json({ error: 'User not authenticated' }); } @@ -316,7 +316,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { }); } - await mcpManager.disconnectUserConnection(user.id, serverName); + await mcpManager.disconnectUserConnection(userId, serverName); logger.info( `[MCP Reinitialize] Disconnected existing user connection for server: ${serverName}`, ); @@ -325,14 +325,14 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { let userMCPAuthMap; if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { userMCPAuthMap = await getUserMCPAuthMap({ - userId: user.id, + userId, servers: [serverName], findPluginAuthsByKeys, }); } const result = await reinitMCPServer({ - req, + userId, serverName, userMCPAuthMap, }); diff --git a/api/server/services/Config/getCachedTools.js b/api/server/services/Config/getCachedTools.js index e33b2046a7..59a0c8cc5d 100644 --- a/api/server/services/Config/getCachedTools.js +++ b/api/server/services/Config/getCachedTools.js @@ -95,21 +95,10 @@ async function getMCPServerTools(serverName) { return null; } -/** - * Middleware-friendly function to get tools for a request - * @function getToolsForRequest - * @param {Object} [req] - Express request object - * @returns {Promise} Available tools for the request - */ -async function getToolsForRequest(_req) { - return getCachedTools(); -} - module.exports = { ToolCacheKeys, getCachedTools, setCachedTools, getMCPServerTools, - getToolsForRequest, invalidateCachedTools, }; diff --git a/api/server/services/Config/mcp.js b/api/server/services/Config/mcp.js index aab2edf3c2..75824d1b30 100644 --- a/api/server/services/Config/mcp.js +++ b/api/server/services/Config/mcp.js @@ -84,45 +84,8 @@ async function cacheMCPServerTools({ serverName, serverTools }) { } } -/** - * Clears all MCP tools for a specific server - * @param {Object} params - Parameters for clearing MCP tools - * @param {string} params.serverName - MCP server name - * @returns {Promise} - */ -async function clearMCPServerTools({ serverName }) { - try { - const tools = await getCachedTools(); - - // Remove all tools for this server - const mcpDelimiter = Constants.mcp_delimiter; - let removedCount = 0; - for (const key of Object.keys(tools)) { - if (key.endsWith(`${mcpDelimiter}${serverName}`)) { - delete tools[key]; - removedCount++; - } - } - - if (removedCount > 0) { - await setCachedTools(tools); - - const cache = getLogStores(CacheKeys.CONFIG_STORE); - await cache.delete(CacheKeys.TOOLS); - // Also clear the server-specific cache - await cache.delete(`tools:mcp:${serverName}`); - - logger.debug(`[MCP Cache] Removed ${removedCount} tools for ${serverName} (global)`); - } - } catch (error) { - logger.error(`[MCP Cache] Failed to clear tools for ${serverName}:`, error); - throw error; - } -} - module.exports = { mergeAppTools, cacheMCPServerTools, - clearMCPServerTools, updateMCPServerTools, }; diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index bc32dabebf..36d61b2337 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -22,8 +22,8 @@ const { } = require('librechat-data-provider'); const { getMCPManager, getFlowStateManager, getOAuthReconnectionManager } = require('~/config'); const { findToken, createToken, updateToken } = require('~/models'); -const { getCachedTools, getAppConfig } = require('./Config'); const { reinitMCPServer } = require('./Tools/mcp'); +const { getAppConfig } = require('./Config'); const { getLogStores } = require('~/cache'); /** @@ -152,8 +152,8 @@ function createOAuthCallback({ runStepEmitter, runStepDeltaEmitter }) { /** * @param {Object} params - * @param {ServerRequest} params.req - The Express request object, containing user/request info. * @param {ServerResponse} params.res - The Express response object for sending events. + * @param {string} params.userId - The user ID from the request object. * @param {string} params.serverName * @param {AbortSignal} params.signal * @param {string} params.model @@ -161,9 +161,9 @@ function createOAuthCallback({ runStepEmitter, runStepDeltaEmitter }) { * @param {Record>} [params.userMCPAuthMap] * @returns { Promise unknown}>> } An object with `_call` method to execute the tool input. */ -async function reconnectServer({ req, res, index, signal, serverName, userMCPAuthMap }) { +async function reconnectServer({ res, userId, index, signal, serverName, userMCPAuthMap }) { const runId = Constants.USE_PRELIM_RESPONSE_MESSAGE_ID; - const flowId = `${req.user?.id}:${serverName}:${Date.now()}`; + const flowId = `${userId}:${serverName}:${Date.now()}`; const flowManager = getFlowStateManager(getLogStores(CacheKeys.FLOWS)); const stepId = 'step_oauth_login_' + serverName; const toolCall = { @@ -192,7 +192,7 @@ async function reconnectServer({ req, res, index, signal, serverName, userMCPAut flowManager, }); return await reinitMCPServer({ - req, + userId, signal, serverName, oauthStart, @@ -211,8 +211,8 @@ async function reconnectServer({ req, res, index, signal, serverName, userMCPAut * i.e. `availableTools`, and will reinitialize the MCP server to ensure all tools are generated. * * @param {Object} params - * @param {ServerRequest} params.req - The Express request object, containing user/request info. * @param {ServerResponse} params.res - The Express response object for sending events. + * @param {string} params.userId - The user ID from the request object. * @param {string} params.serverName * @param {string} params.model * @param {Providers | EModelEndpoint} params.provider - The provider for the tool. @@ -221,8 +221,16 @@ async function reconnectServer({ req, res, index, signal, serverName, userMCPAut * @param {Record>} [params.userMCPAuthMap] * @returns { Promise unknown}>> } An object with `_call` method to execute the tool input. */ -async function createMCPTools({ req, res, index, signal, serverName, provider, userMCPAuthMap }) { - const result = await reconnectServer({ req, res, index, signal, serverName, userMCPAuthMap }); +async function createMCPTools({ + res, + userId, + index, + signal, + serverName, + provider, + userMCPAuthMap, +}) { + const result = await reconnectServer({ res, userId, index, signal, serverName, userMCPAuthMap }); if (!result || !result.tools) { logger.warn(`[MCP][${serverName}] Failed to reinitialize MCP server.`); return; @@ -231,8 +239,8 @@ async function createMCPTools({ req, res, index, signal, serverName, provider, u const serverTools = []; for (const tool of result.tools) { const toolInstance = await createMCPTool({ - req, res, + userId, provider, userMCPAuthMap, availableTools: result.availableTools, @@ -249,8 +257,8 @@ async function createMCPTools({ req, res, index, signal, serverName, provider, u /** * Creates a single tool from the specified MCP Server via `toolKey`. * @param {Object} params - * @param {ServerRequest} params.req - The Express request object, containing user/request info. * @param {ServerResponse} params.res - The Express response object for sending events. + * @param {string} params.userId - The user ID from the request object. * @param {string} params.toolKey - The toolKey for the tool. * @param {string} params.model - The model for the tool. * @param {number} [params.index] @@ -261,26 +269,31 @@ async function createMCPTools({ req, res, index, signal, serverName, provider, u * @returns { Promise unknown}> } An object with `_call` method to execute the tool input. */ async function createMCPTool({ - req, res, + userId, index, signal, toolKey, provider, userMCPAuthMap, - availableTools: tools, + availableTools, }) { const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter); - const availableTools = - tools ?? (await getCachedTools({ userId: req.user?.id, includeGlobal: true })); /** @type {LCTool | undefined} */ let toolDefinition = availableTools?.[toolKey]?.function; if (!toolDefinition) { logger.warn( `[MCP][${serverName}][${toolName}] Requested tool not found in available tools, re-initializing MCP server.`, ); - const result = await reconnectServer({ req, res, index, signal, serverName, userMCPAuthMap }); + const result = await reconnectServer({ + res, + userId, + index, + signal, + serverName, + userMCPAuthMap, + }); toolDefinition = result?.availableTools?.[toolKey]?.function; } diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 174eae0788..5245b43221 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -74,7 +74,7 @@ async function processRequiredActions(client, requiredActions) { requiredActions, ); const appConfig = client.req.config; - const toolDefinitions = await getCachedTools({ userId: client.req.user.id, includeGlobal: true }); + const toolDefinitions = await getCachedTools(); const seenToolkits = new Set(); const tools = requiredActions .map((action) => { diff --git a/api/server/services/Tools/mcp.js b/api/server/services/Tools/mcp.js index c9c369868e..04c27eafe4 100644 --- a/api/server/services/Tools/mcp.js +++ b/api/server/services/Tools/mcp.js @@ -7,7 +7,7 @@ const { getLogStores } = require('~/cache'); /** * @param {Object} params - * @param {ServerRequest} params.req + * @param {string} params.userId * @param {string} params.serverName - The name of the MCP server * @param {boolean} params.returnOnOAuth - Whether to initiate OAuth and return, or wait for OAuth flow to finish * @param {AbortSignal} [params.signal] - The abort signal to handle cancellation. @@ -18,7 +18,7 @@ const { getLogStores } = require('~/cache'); * @param {Record>} [params.userMCPAuthMap] */ async function reinitMCPServer({ - req, + userId, signal, forceNew, serverName, @@ -51,7 +51,7 @@ async function reinitMCPServer({ try { userConnection = await mcpManager.getUserConnection({ - user: req.user, + user: { id: userId }, signal, forceNew, oauthStart, diff --git a/api/test/app/clients/tools/util/fileSearch.test.js b/api/test/app/clients/tools/util/fileSearch.test.js index ca19f50453..9a2ab112af 100644 --- a/api/test/app/clients/tools/util/fileSearch.test.js +++ b/api/test/app/clients/tools/util/fileSearch.test.js @@ -46,7 +46,7 @@ describe('fileSearch.js - test only new file_id and page additions', () => { queryVectors.mockResolvedValue(mockResults); const fileSearchTool = await createFileSearchTool({ - req: { user: { id: 'user1' } }, + userId: 'user1', files: mockFiles, entity_id: 'agent-123', });