mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-03 04:42:11 +00:00
* 🧠 feat: Memory Agent Capability with Inline Tools and Ephemeral Badge
Add `AgentCapabilities.memory`, which expands into the inline set_memory/delete_memory tool pair (mirroring the execute_code expansion via registerMemoryTools) when a run-level memoryAvailable gate holds: capability enabled, memory configured, MEMORIES.USE permission, and personalization not opted out. Surfaces the memory artifact as an attachment in the agents tool-end callback.
Adds the ephemeral path (TEphemeralAgent.memory, load/added agent tool injection), a fully-gated memory badge plus tools-dropdown entry, the agent-builder Memory toggle with form round-trip, and a mock e2e test asserting the badge reaches the request payload. Additive to and independent of the existing post-turn memory extraction agent.
* 🩹 fix: Address Codex review on memory capability (gating, validKeys, usage guard)
- Strip the memory capability from the served agents capabilities when memory is not configured/enabled, so the badge, tools dropdown, agent-builder toggle, and backend capability gate stay consistent instead of exposing an inert toggle on default installs (where MEMORIES.USE defaults true).
- Surface configured memory.validKeys in the inline tool definitions so the model is told the allowed keys up front, matching the runtime createMemoryTool schema.
- Append a strict explicit-request usage guard to the agent instructions when inline memory tools are registered, preserving the memory-agent's privacy behavior.
- Add AppService tests covering memory-capability stripping.
* ✅ test: Update AppService capability snapshots for memory strip
AppService now strips the memory capability from the served agents defaults when no memory block is configured; update the spec's expected capability lists to defaultAgentCapabilitiesWithoutMemory for the no-memory-config cases.
* 🛡️ fix: Address Codex re-review on memory capability (round 2)
- Strip the memory capability from the FINAL served agents config, not just defaults; loadEndpoints reparses any endpoints.agents block, so memory was still exposed in that common shape (packages/data-schemas/src/app/service.ts) + regression test.
- Re-check the full memory gate (config, opt-out, MEMORIES.USE) inside handleTools before constructing set_memory/delete_memory, so an unsolicited tool call from a model/custom endpoint can't bypass the runtime gates (api/app/clients/tools/util/handleTools.js).
- Restore the persisted memory toggle for model-spec conversations via applyModelSpecEphemeralAgent (client/src/utils/endpoints.ts).
- Clear LAST_MEMORY_TOGGLE_ on logout and clear-all-chats so a stale memory preference can't leak across users on a shared browser (client/src/utils/localStorage.ts).
* 🧠 fix: Address Codex re-review on memory capability (round 3)
- Serialize set_memory writes and advance a running token total inside createMemoryTool, so parallel batched calls in one event-driven turn can't each pass the limit check against a stale total and collectively exceed memory.tokenLimit (packages/api/src/agents/memory.ts) + tests.
- Inject the keyed memory context (withKeys) instead of withoutKeys when the running agent has the inline memory capability, so delete_memory has a visible key to target (api/server/controllers/agents/client.js).
* 🔐 fix: Address Codex re-review on memory capability (round 4)
- Detect inline memory by tool NAME (set_memory/delete_memory) across an initialized agent's tools + toolDefinitions, since the 'memory' marker is expanded at init and the prior string check never matched; inject the keyed memory context for any primary OR sub-agent that carries the inline memory tools (api/server/controllers/agents/client.js).
- Enforce memory WRITE permissions in the inline tool gate: set_memory requires CREATE+UPDATE and delete_memory requires UPDATE (matching the REST memory routes), so a USE-only role can't mutate/delete memories via agent tool calls (api/app/clients/tools/util/handleTools.js).
* 🔒 fix: Address Codex re-review on memory capability (round 5)
- Gate inline memory registration (memoryAvailable) on the memory WRITE permissions (USE+CREATE+UPDATE), so a read-only-memory role no longer has set_memory/delete_memory shown to the model only for the runtime loader to refuse them (api/server/services/Endpoints/agents/initialize.js).
- Enforce the per-agent memory opt-in at execution: handleTools now refuses to construct set_memory/delete_memory unless the agent actually declared them (toolDefinitions/tools), blocking hallucinated/undeclared memory tool calls from mutating memory.
- Fail closed when getFormattedMemories errors with a configured tokenLimit, instead of writing as if storage were empty and bypassing the cap (api/app/clients/tools/util/handleTools.js).
* 🩹 fix: Address Codex re-review on memory capability (round 6)
- Fix a P1 regression from the prior round: the execution-context agent keeps the raw 'memory' capability marker (not the expanded set_memory/delete_memory names), so the opt-in check now matches the marker. This restores memory writes/deletes AND avoids hijacking an MCP tool that merely shares the set_memory/delete_memory name (api/app/clients/tools/util/handleTools.js).
- Count repeated set_memory writes to the same key as replacements, not additions, against tokenLimit — set_memory upserts, so a same-key rewrite swaps its prior token contribution instead of double-counting (packages/api/src/agents/memory.ts) + test.
- Gate the memory badge, tools dropdown, and agent-builder toggle on the full memory write permissions (USE+CREATE+UPDATE) via a shared useHasMemoryAccess hook, so a read-only-memory role no longer sees an enabled Memory control the backend would refuse to wire up.
* 🧷 fix: Address Codex re-review on memory capability (round 7)
- Recognize inline memory across both execution-context agent shapes: initializeAgent now sets a LibreChat-only memoryToolsRegistered flag on the InitializedAgent, and the opt-in/detection checks accept that flag OR the raw 'memory' marker. Fixes memory failing for processAddedConvo agents (which store the initialized config, marker already expanded) while staying MCP-name-collision-safe (api/app/clients/tools/util/handleTools.js, packages/api/src/agents/initialize.ts, api/server/controllers/agents/client.js).
- Scope keyed memory context to memory-enabled agents only: useMemory now returns both keyed and unkeyed contexts, and buildMessages injects the keyed one (memory keys + token metadata) only to agents that can call delete_memory, while the primary/post-turn path keeps the unkeyed values — so a primary without memory tools no longer sees memory keys it doesn't need.
* 🔏 fix: Address Codex re-review on memory capability (round 8)
- Enforce memory size limits on inline writes: createMemoryTool now rejects keys over 1000 chars and values over memory.charLimit, matching the REST memory routes, so an inline-memory agent can't persist blobs the memory UI/API would reject (packages/api/src/agents/memory.ts, api/app/clients/tools/util/handleTools.js) + test.
- Recheck the agents 'memory' endpoint capability at execution time, so a stale/hallucinated set_memory/delete_memory call can't mutate memory after an admin removes the capability while the agent document still carries the marker (api/app/clients/tools/util/handleTools.js).
* ♻️ refactor: Move inline-memory backend logic into packages/api + share memory load
Workspace boundary: the inline-memory gating/detection logic that had crept into /api now lives in packages/api/src/agents/memory.ts (TS), with /api kept as thin wrappers.
- Add agentHasInlineMemoryTools, isMemoryToolAllowed, and buildInlineMemoryTool to packages/api; handleTools.js now calls buildInlineMemoryTool instead of constructing/gating the tools inline, and client.js imports agentHasInlineMemoryTools instead of redefining it.
- Optimize repeated memory loads: getRequestMemories memoizes getFormattedMemories per request (WeakMap keyed by req), so the run's memory-context load and every memory-enabled agent's set_memory token-usage load share a single DB fetch instead of one per agent.
* 🧠 fix: Invalidate request memory cache after inline writes
Inline set_memory/delete_memory now invalidate the request-scoped
getFormattedMemories cache on a successful write, so a later tool round
in the same response is seeded with the post-write usage total instead
of the stale pre-write one (multi-round writes no longer collectively
exceed tokenLimit, and a set after a delete is not over-counted). The
within-round sharing across multiple memory-enabled agents is preserved.
* 🧠 fix: Persist memory capability on saved agents; honor registration flag
- Add Tools.memory to the v1 systemTools allowlist so filterAuthorizedTools
no longer silently drops the memory marker when an agent with the Memory
capability is created/updated/duplicated through the builder (previously
the capability only worked for ephemeral chats, not persisted agents).
- agentHasInlineMemoryTools now honors an explicit memoryToolsRegistered
boolean before falling back to the raw `memory` marker, so an initialized
config whose registration was denied (memoryAvailable false) is not given
keyed memory context just because the marker survives in tools.
* 🧩 fix: Bring memory tool to parity with other ephemeral tools
- Add `memory` to the model-spec schema/type and honor `modelSpec.memory`
in both ephemeral paths (load.ts, added.ts) and the frontend spec
application, so admins can pre-enable Memory from a model spec exactly
like webSearch/fileSearch/executeCode.
- Add LAST_MEMORY_TOGGLE_ to the timestamped-storage cleanup list so stale
per-conversation memory toggles are purged on startup like the others.
- Hide the agent-builder Memory toggle for users who disabled memory in
personalization (memories === false), mirroring the chat badge's opt-out
gate, so the setting isn't shown as inert/misleading.
* ✅ test: Cover memory in applyModelSpecEphemeralAgent spec defaults
Update the exact-object assertions to include the new `memory` field and
add positive coverage that `modelSpec.memory` maps to the ephemeral
agent's `memory` flag. Fixes the shard 2/4 failure from 672a03b05.
552 lines
18 KiB
JavaScript
552 lines
18 KiB
JavaScript
const { logger } = require('@librechat/data-schemas');
|
|
const { Calculator, createSearchTool, createCodeExecutionTool } = require('@librechat/agents');
|
|
const {
|
|
checkAccess,
|
|
toolkitParent,
|
|
createSafeUser,
|
|
mcpToolPattern,
|
|
loadWebSearchAuth,
|
|
buildInlineMemoryTool,
|
|
getCodeApiAuthHeaders,
|
|
buildImageToolContext,
|
|
SET_MEMORY_TOOL_NAME,
|
|
buildWebSearchContext,
|
|
DELETE_MEMORY_TOOL_NAME,
|
|
buildWebSearchDynamicContext,
|
|
} = require('@librechat/api');
|
|
const {
|
|
Tools,
|
|
Constants,
|
|
Permissions,
|
|
EToolResources,
|
|
PermissionTypes,
|
|
} = require('librechat-data-provider');
|
|
const {
|
|
availableTools,
|
|
manifestToolMap,
|
|
// Basic Tools
|
|
GoogleSearchAPI,
|
|
// Structured Tools
|
|
DALLE3,
|
|
FluxAPI,
|
|
OpenWeather,
|
|
StructuredSD,
|
|
StructuredACS,
|
|
TraversaalSearch,
|
|
StructuredWolfram,
|
|
TavilySearchResults,
|
|
createGeminiImageTool,
|
|
createOpenAIImageTools,
|
|
} = require('../');
|
|
const {
|
|
createMCPTool,
|
|
createMCPTools,
|
|
createMCPPermissionContext,
|
|
resolveConfigServers,
|
|
} = require('~/server/services/MCP');
|
|
const { getMCPRequestContext } = require('~/server/services/MCPRequestContext');
|
|
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
|
|
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
|
|
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
|
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
|
const { getMCPServerTools } = require('~/server/services/Config');
|
|
const { getMCPServersRegistry } = require('~/config');
|
|
const { getRoleByName, setMemory, deleteMemory, getFormattedMemories } = require('~/models');
|
|
|
|
/**
|
|
* Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values.
|
|
* Tools without required authentication or with valid authentication are considered valid.
|
|
*
|
|
* @param {Object} user The user object for whom to validate tool access.
|
|
* @param {Array<string>} tools An array of tool identifiers to validate. Defaults to an empty array.
|
|
* @returns {Promise<Array<string>>} A promise that resolves to an array of valid tool identifiers.
|
|
*/
|
|
const validateTools = async (user, tools = []) => {
|
|
try {
|
|
const validToolsSet = new Set(tools);
|
|
const availableToolsToValidate = availableTools.filter((tool) =>
|
|
validToolsSet.has(tool.pluginKey),
|
|
);
|
|
|
|
/**
|
|
* Validates the credentials for a given auth field or set of alternate auth fields for a tool.
|
|
* If valid admin or user authentication is found, the function returns early. Otherwise, it removes the tool from the set of valid tools.
|
|
*
|
|
* @param {string} authField The authentication field or fields (separated by "||" for alternates) to validate.
|
|
* @param {string} toolName The identifier of the tool being validated.
|
|
*/
|
|
const validateCredentials = async (authField, toolName) => {
|
|
const fields = authField.split('||');
|
|
for (const field of fields) {
|
|
const adminAuth = process.env[field];
|
|
if (adminAuth && adminAuth.length > 0) {
|
|
return;
|
|
}
|
|
|
|
let userAuth = null;
|
|
try {
|
|
userAuth = await getUserPluginAuthValue(user, field);
|
|
} catch (err) {
|
|
if (field === fields[fields.length - 1] && !userAuth) {
|
|
throw err;
|
|
}
|
|
}
|
|
if (userAuth && userAuth.length > 0) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
validToolsSet.delete(toolName);
|
|
};
|
|
|
|
for (const tool of availableToolsToValidate) {
|
|
if (!tool.authConfig || tool.authConfig.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
for (const auth of tool.authConfig) {
|
|
await validateCredentials(auth.authField, tool.pluginKey);
|
|
}
|
|
}
|
|
|
|
return Array.from(validToolsSet.values());
|
|
} catch (err) {
|
|
logger.error('[validateTools] There was a problem validating tools', err);
|
|
throw new Error(err);
|
|
}
|
|
};
|
|
|
|
/** @typedef {typeof import('@librechat/agents/langchain/tools').Tool} ToolConstructor */
|
|
/** @typedef {import('@librechat/agents/langchain/tools').Tool} Tool */
|
|
|
|
/**
|
|
* Initializes a tool with authentication values for the given user, supporting alternate authentication fields.
|
|
* Authentication fields can have alternates separated by "||", and the first defined variable will be used.
|
|
*
|
|
* @param {string} userId The user ID for which the tool is being loaded.
|
|
* @param {Array<string>} authFields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
|
|
* @param {ToolConstructor} ToolConstructor The constructor function for the tool to be initialized.
|
|
* @param {Object} options Optional parameters to be passed to the tool constructor alongside authentication values.
|
|
* @returns {() => Promise<Tool>} An Async function that, when called, asynchronously initializes and returns an instance of the tool with authentication.
|
|
*/
|
|
const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => {
|
|
return async function () {
|
|
const authValues = await loadAuthValues({ userId, authFields });
|
|
return new ToolConstructor({ ...options, ...authValues, userId });
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {string} toolKey
|
|
* @returns {Array<string>}
|
|
*/
|
|
const getAuthFields = (toolKey) => {
|
|
return manifestToolMap[toolKey]?.authConfig.map((auth) => auth.authField) ?? [];
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.user
|
|
* @param {Record<string, Record<string, string>>} [object.userMCPAuthMap]
|
|
* @param {AbortSignal} [object.signal]
|
|
* @param {Pick<Agent, 'id' | 'provider' | 'model'>} [params.agent]
|
|
* @param {string} [params.model]
|
|
* @param {EModelEndpoint} [params.endpoint]
|
|
* @param {LoadToolOptions} [params.options]
|
|
* @param {boolean} [params.useSpecs]
|
|
* @param {Array<string>} params.tools
|
|
* @param {boolean} [params.functions]
|
|
* @param {boolean} [params.returnMap]
|
|
* @param {AppConfig['webSearch']} [params.webSearch]
|
|
* @param {AppConfig['fileStrategy']} [params.fileStrategy]
|
|
* @param {AppConfig['imageOutputType']} [params.imageOutputType]
|
|
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any>, dynamicToolContextMap?: Object<string, any> } | Record<string,Tool>>}
|
|
*/
|
|
const loadTools = async ({
|
|
user,
|
|
agent,
|
|
model,
|
|
signal,
|
|
endpoint,
|
|
userMCPAuthMap,
|
|
tools = [],
|
|
options = {},
|
|
functions = true,
|
|
returnMap = false,
|
|
webSearch,
|
|
fileStrategy,
|
|
imageOutputType,
|
|
}) => {
|
|
const toolConstructors = {
|
|
flux: FluxAPI,
|
|
calculator: Calculator,
|
|
google: GoogleSearchAPI,
|
|
open_weather: OpenWeather,
|
|
wolfram: StructuredWolfram,
|
|
'stable-diffusion': StructuredSD,
|
|
'azure-ai-search': StructuredACS,
|
|
traversaal_search: TraversaalSearch,
|
|
tavily_search_results_json: TavilySearchResults,
|
|
};
|
|
|
|
const customConstructors = {
|
|
image_gen_oai: async (_toolContextMap, dynamicToolContextMap) => {
|
|
const authFields = getAuthFields('image_gen_oai');
|
|
const authValues = await loadAuthValues({ userId: user, authFields });
|
|
const imageFiles = options.tool_resources?.[EToolResources.image_edit]?.files ?? [];
|
|
const toolContext = buildImageToolContext({
|
|
imageFiles,
|
|
toolName: `${EToolResources.image_edit}_oai`,
|
|
contextDescription: 'image editing',
|
|
});
|
|
if (toolContext) {
|
|
dynamicToolContextMap.image_edit_oai = toolContext;
|
|
}
|
|
return createOpenAIImageTools({
|
|
...authValues,
|
|
isAgent: !!agent,
|
|
req: options.req,
|
|
imageOutputType,
|
|
fileStrategy,
|
|
imageFiles,
|
|
});
|
|
},
|
|
gemini_image_gen: async (_toolContextMap, dynamicToolContextMap) => {
|
|
const authFields = getAuthFields('gemini_image_gen');
|
|
const authValues = await loadAuthValues({ userId: user, authFields, throwError: false });
|
|
const imageFiles = options.tool_resources?.[EToolResources.image_edit]?.files ?? [];
|
|
const toolContext = buildImageToolContext({
|
|
imageFiles,
|
|
toolName: 'gemini_image_gen',
|
|
contextDescription: 'image context',
|
|
});
|
|
if (toolContext) {
|
|
dynamicToolContextMap.gemini_image_gen = toolContext;
|
|
}
|
|
return createGeminiImageTool({
|
|
...authValues,
|
|
isAgent: !!agent,
|
|
req: options.req,
|
|
imageFiles,
|
|
userId: user,
|
|
fileStrategy,
|
|
});
|
|
},
|
|
};
|
|
|
|
const requestedTools = {};
|
|
const hasMCPTools = tools.some((toolName) => toolName && mcpToolPattern.test(toolName));
|
|
const mcpPermissionContext =
|
|
options.mcpPermissionContext ?? createMCPPermissionContext(options.req);
|
|
const canUseMCP = hasMCPTools
|
|
? await mcpPermissionContext.canUseServers(options.req?.user)
|
|
: true;
|
|
let loggedMCPDenied = false;
|
|
|
|
if (functions === true) {
|
|
toolConstructors.dalle = DALLE3;
|
|
}
|
|
|
|
/** @type {ImageGenOptions} */
|
|
const imageGenOptions = {
|
|
isAgent: !!agent,
|
|
req: options.req,
|
|
fileStrategy,
|
|
processFileURL: options.processFileURL,
|
|
returnMetadata: options.returnMetadata,
|
|
uploadImageBuffer: options.uploadImageBuffer,
|
|
};
|
|
|
|
const toolOptions = {
|
|
flux: imageGenOptions,
|
|
dalle: imageGenOptions,
|
|
'stable-diffusion': imageGenOptions,
|
|
gemini_image_gen: imageGenOptions,
|
|
};
|
|
|
|
/** @type {Record<string, string>} */
|
|
const toolContextMap = {};
|
|
/** @type {Record<string, string>} */
|
|
const dynamicToolContextMap = {};
|
|
/**
|
|
* @type {import('@librechat/agents').CodeEnvFile[] | undefined}
|
|
* Captured by the `execute_code` factory when files are primed. Surfaced
|
|
* out of `loadTools` so client.js can seed `Graph.sessions[EXECUTE_CODE]`
|
|
* before run start — without that seed, the first `execute_code` /
|
|
* `bash_tool` call lands with empty `_injected_files` and the sandbox
|
|
* can't see the prior turn's generated artifacts.
|
|
*/
|
|
let primedCodeFiles;
|
|
const requestedMCPTools = {};
|
|
|
|
/** Resolve config-source servers for the current user/tenant context */
|
|
let configServers;
|
|
if (hasMCPTools && canUseMCP) {
|
|
configServers = await resolveConfigServers(options.req);
|
|
}
|
|
|
|
for (const tool of tools) {
|
|
if (tool === Tools.execute_code) {
|
|
requestedTools[tool] = async () => {
|
|
const { files, toolContext } = await primeCodeFiles({
|
|
...options,
|
|
agentId: agent?.id,
|
|
});
|
|
if (toolContext) {
|
|
dynamicToolContextMap[tool] = toolContext;
|
|
}
|
|
if (files?.length) {
|
|
primedCodeFiles = files;
|
|
}
|
|
return createCodeExecutionTool({
|
|
user_id: user,
|
|
files,
|
|
authHeaders: () => getCodeApiAuthHeaders(options.req),
|
|
});
|
|
};
|
|
continue;
|
|
} else if (tool === Tools.file_search) {
|
|
requestedTools[tool] = async () => {
|
|
const { files, toolContext } = await primeSearchFiles({
|
|
...options,
|
|
agentId: agent?.id,
|
|
});
|
|
if (toolContext) {
|
|
dynamicToolContextMap[tool] = toolContext;
|
|
}
|
|
|
|
/** @type {boolean | undefined} Check if user has FILE_CITATIONS permission */
|
|
let fileCitations;
|
|
if (fileCitations == null && options.req?.user != null) {
|
|
try {
|
|
fileCitations = await checkAccess({
|
|
user: options.req.user,
|
|
permissionType: PermissionTypes.FILE_CITATIONS,
|
|
permissions: [Permissions.USE],
|
|
getRoleByName,
|
|
});
|
|
} catch (error) {
|
|
logger.error('[handleTools] FILE_CITATIONS permission check failed:', error);
|
|
fileCitations = false;
|
|
}
|
|
}
|
|
|
|
return createFileSearchTool({
|
|
userId: user,
|
|
files,
|
|
entity_id: agent?.id,
|
|
fileCitations,
|
|
});
|
|
};
|
|
continue;
|
|
} else if (tool === Tools.web_search) {
|
|
const result = await loadWebSearchAuth({
|
|
userId: user,
|
|
loadAuthValues,
|
|
webSearchConfig: webSearch,
|
|
});
|
|
const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {};
|
|
requestedTools[tool] = async () => {
|
|
toolContextMap[tool] = buildWebSearchContext();
|
|
dynamicToolContextMap[tool] = buildWebSearchDynamicContext(
|
|
options.req?.conversationCreatedAt,
|
|
);
|
|
return createSearchTool({
|
|
...result.authResult,
|
|
onSearchResults,
|
|
onGetHighlights,
|
|
logger,
|
|
});
|
|
};
|
|
continue;
|
|
} else if (tool === SET_MEMORY_TOOL_NAME || tool === DELETE_MEMORY_TOOL_NAME) {
|
|
requestedTools[tool] = () =>
|
|
buildInlineMemoryTool({
|
|
toolName: tool,
|
|
req: options.req,
|
|
agent,
|
|
userId: user,
|
|
memoryMethods: { setMemory, deleteMemory, getFormattedMemories },
|
|
getRoleByName,
|
|
});
|
|
continue;
|
|
} else if (tool && mcpToolPattern.test(tool)) {
|
|
if (!canUseMCP) {
|
|
if (!loggedMCPDenied) {
|
|
logger.warn(
|
|
`[handleTools] User ${options.req?.user?.id} lacks MCP server use permission`,
|
|
);
|
|
loggedMCPDenied = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const [toolName, serverName] = tool.split(Constants.mcp_delimiter);
|
|
if (toolName === Constants.mcp_server) {
|
|
/** Placeholder used for UI purposes */
|
|
continue;
|
|
}
|
|
const serverConfig = serverName
|
|
? await getMCPServersRegistry().getServerConfig(serverName, user, configServers)
|
|
: null;
|
|
if (!serverConfig) {
|
|
logger.warn(
|
|
`MCP server "${serverName}" for "${toolName}" tool is not configured${agent?.id != null && agent.id ? ` but attached to "${agent.id}"` : ''}`,
|
|
);
|
|
continue;
|
|
}
|
|
if (toolName === Constants.mcp_all) {
|
|
requestedMCPTools[serverName] = [
|
|
{
|
|
type: 'all',
|
|
serverName,
|
|
config: serverConfig,
|
|
},
|
|
];
|
|
continue;
|
|
}
|
|
|
|
requestedMCPTools[serverName] = requestedMCPTools[serverName] || [];
|
|
requestedMCPTools[serverName].push({
|
|
type: 'single',
|
|
toolKey: tool,
|
|
serverName,
|
|
config: serverConfig,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const toolKey = customConstructors[tool] ? tool : toolkitParent[tool];
|
|
if (toolKey && customConstructors[toolKey]) {
|
|
if (!requestedTools[toolKey]) {
|
|
let cached;
|
|
requestedTools[toolKey] = async () => {
|
|
cached ??= customConstructors[toolKey](toolContextMap, dynamicToolContextMap);
|
|
return cached;
|
|
};
|
|
}
|
|
requestedTools[tool] = requestedTools[toolKey];
|
|
continue;
|
|
}
|
|
|
|
if (toolConstructors[tool]) {
|
|
const options = toolOptions[tool] || {};
|
|
const toolInstance = loadToolWithAuth(
|
|
user,
|
|
getAuthFields(tool),
|
|
toolConstructors[tool],
|
|
options,
|
|
);
|
|
requestedTools[tool] = toolInstance;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (returnMap) {
|
|
return requestedTools;
|
|
}
|
|
|
|
const toolPromises = [];
|
|
for (const tool of tools) {
|
|
const validTool = requestedTools[tool];
|
|
if (validTool) {
|
|
toolPromises.push(
|
|
validTool().catch((error) => {
|
|
logger.error(`Error loading tool ${tool}:`, error);
|
|
return null;
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
const loadedTools = (await Promise.all(toolPromises)).flatMap((plugin) => plugin || []);
|
|
const mcpToolPromises = [];
|
|
/** MCP server tools are initialized sequentially by server */
|
|
let index = -1;
|
|
const failedMCPServers = new Set();
|
|
const safeUser = createSafeUser(options.req?.user);
|
|
const requestScopedConnections =
|
|
options.requestScopedConnections ?? getMCPRequestContext(options.req, options.res);
|
|
|
|
for (const [serverName, toolConfigs] of Object.entries(requestedMCPTools)) {
|
|
index++;
|
|
/** @type {LCAvailableTools} */
|
|
let availableTools = options.mcpAvailableTools?.[serverName];
|
|
for (const config of toolConfigs) {
|
|
try {
|
|
if (failedMCPServers.has(serverName)) {
|
|
continue;
|
|
}
|
|
const mcpParams = {
|
|
mcpPermissionContext,
|
|
index,
|
|
signal,
|
|
user: safeUser,
|
|
userMCPAuthMap,
|
|
configServers,
|
|
requestBody: options.req?.body,
|
|
requestScopedConnections,
|
|
res: options.res,
|
|
streamId: options.req?._resumableStreamId || null,
|
|
model: agent?.model ?? model,
|
|
serverName: config.serverName,
|
|
provider: agent?.provider ?? endpoint,
|
|
config: config.config,
|
|
};
|
|
|
|
if (config.type === 'all' && toolConfigs.length === 1) {
|
|
/** Handle async loading for single 'all' tool config */
|
|
mcpToolPromises.push(
|
|
createMCPTools(mcpParams).catch((error) => {
|
|
logger.error(`Error loading ${serverName} tools:`, error);
|
|
return null;
|
|
}),
|
|
);
|
|
continue;
|
|
}
|
|
if (!availableTools) {
|
|
try {
|
|
availableTools = await getMCPServerTools(safeUser.id, serverName, config.config);
|
|
} 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,
|
|
onAvailableTools: (tools) => {
|
|
availableTools = tools;
|
|
},
|
|
});
|
|
|
|
if (Array.isArray(mcpTool)) {
|
|
loadedTools.push(...mcpTool);
|
|
} else if (mcpTool) {
|
|
loadedTools.push(mcpTool);
|
|
} else {
|
|
failedMCPServers.add(serverName);
|
|
logger.warn(
|
|
`MCP tool creation failed for "${config.toolKey}", server may be unavailable or unauthenticated.`,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Error loading MCP tool for server ${serverName}:`, error);
|
|
}
|
|
}
|
|
}
|
|
loadedTools.push(...(await Promise.all(mcpToolPromises)).flatMap((plugin) => plugin || []));
|
|
return { loadedTools, toolContextMap, dynamicToolContextMap, primedCodeFiles };
|
|
};
|
|
|
|
module.exports = {
|
|
loadToolWithAuth,
|
|
validateTools,
|
|
loadTools,
|
|
};
|