mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-11 08:43:48 +00:00
* fix: prevent MCP tools with `_action` in name from being misclassified as OpenAPI action tools
Add `isActionTool()` helper that checks for the `_action_` delimiter
while guarding against cross-delimiter collision with `_mcp_`. Replace
all `includes(actionDelimiter)` classification checks with the new
helper across backend and frontend.
* test: add coverage for MCP/action cross-delimiter collision
Verify that `isActionTool` correctly rejects MCP tool names containing
`_action` and that `loadAgentTools` does not filter them based on
`actionsEnabled`. Add ToolIcon and definitions test cases.
* fix: simplify isActionTool to handle all MCP name patterns
- Use `!toolName.includes('_mcp_')` instead of checking only after the
first `_action_` occurrence, which missed MCP tools with `_action_` in
the middle of their name (e.g. `get_action_data_mcp_myserver`).
- Reference `Constants.mcp_delimiter` value via a local const to avoid
circular import from config.ts, with a comment explaining why.
- Remove dead `actionDelimiter` import from definitions.ts.
- Replace double-filter with single-pass partition in loadToolsForExecution.
- Add test for mid-name `_action_` collision case.
* fix: narrow MCP exclusion to delimiter position in isActionTool
Only reject when `_mcp_` appears after `_action_` (the MCP suffix
position). `_mcp_` before `_action_` is part of the operationId and
is valid — e.g. `sync_mcp_state_action_api---example---com` is a
legitimate action tool whose operationId happens to contain `_mcp_`.
* fix: document positional _mcp_ guard and known RFC-invalid domain limitation
Expand JSDoc on isActionTool to explain the action/MCP format
disambiguation and the theoretical false negative for non-RFC-compliant
domains containing `_mcp_`. Add test documenting this known edge case.
241 lines
7.4 KiB
TypeScript
241 lines
7.4 KiB
TypeScript
/**
|
|
* @fileoverview Tool definitions loader for event-driven mode.
|
|
* Loads tool definitions without creating tool instances for efficient initialization.
|
|
*
|
|
* @module packages/api/src/tools/definitions
|
|
*/
|
|
|
|
import { Constants, isActionTool } from 'librechat-data-provider';
|
|
import type { AgentToolOptions } from 'librechat-data-provider';
|
|
import type { LCToolRegistry, JsonSchemaType, LCTool, GenericTool } from '@librechat/agents';
|
|
import type { ToolDefinition } from './classification';
|
|
import { resolveJsonSchemaRefs, normalizeJsonSchema } from '~/mcp/zod';
|
|
import { buildToolClassification } from './classification';
|
|
import { getToolDefinition } from './registry/definitions';
|
|
import { toolkitExpansion } from './toolkits/mapping';
|
|
|
|
export interface MCPServerTool {
|
|
function?: {
|
|
name?: string;
|
|
description?: string;
|
|
parameters?: JsonSchemaType;
|
|
};
|
|
}
|
|
|
|
export type MCPServerTools = Record<string, MCPServerTool>;
|
|
|
|
export interface LoadToolDefinitionsParams {
|
|
/** User ID for MCP server tool lookup */
|
|
userId: string;
|
|
/** Agent ID for tool classification */
|
|
agentId: string;
|
|
/** Agent's tool list (tool names/identifiers) */
|
|
tools: string[];
|
|
/** Agent-specific tool options */
|
|
toolOptions?: AgentToolOptions;
|
|
/** Whether deferred tools feature is enabled */
|
|
deferredToolsEnabled?: boolean;
|
|
}
|
|
|
|
export interface ActionToolDefinition {
|
|
name: string;
|
|
description?: string;
|
|
parameters?: JsonSchemaType;
|
|
}
|
|
|
|
export interface LoadToolDefinitionsDeps {
|
|
/** Gets MCP server tools - first checks cache, then initializes server if needed */
|
|
getOrFetchMCPServerTools: (userId: string, serverName: string) => Promise<MCPServerTools | null>;
|
|
/** Checks if a tool name is a known built-in tool */
|
|
isBuiltInTool: (toolName: string) => boolean;
|
|
/** Loads auth values for tool search (passed to buildToolClassification) */
|
|
loadAuthValues: (params: {
|
|
userId: string;
|
|
authFields: string[];
|
|
}) => Promise<Record<string, string>>;
|
|
/** Loads action tool definitions (schemas) from OpenAPI specs */
|
|
getActionToolDefinitions?: (
|
|
agentId: string,
|
|
actionToolNames: string[],
|
|
) => Promise<ActionToolDefinition[]>;
|
|
}
|
|
|
|
export interface LoadToolDefinitionsResult {
|
|
toolDefinitions: (ToolDefinition | LCTool)[];
|
|
toolRegistry: LCToolRegistry;
|
|
hasDeferredTools: boolean;
|
|
}
|
|
|
|
const mcpToolPattern = /_mcp_/;
|
|
|
|
/**
|
|
* Loads tool definitions without creating tool instances.
|
|
* This is the efficient path for event-driven mode where tools are loaded on-demand.
|
|
*/
|
|
export async function loadToolDefinitions(
|
|
params: LoadToolDefinitionsParams,
|
|
deps: LoadToolDefinitionsDeps,
|
|
): Promise<LoadToolDefinitionsResult> {
|
|
const { userId, agentId, tools, toolOptions = {}, deferredToolsEnabled = false } = params;
|
|
const { getOrFetchMCPServerTools, isBuiltInTool, loadAuthValues, getActionToolDefinitions } =
|
|
deps;
|
|
|
|
const emptyResult: LoadToolDefinitionsResult = {
|
|
toolDefinitions: [],
|
|
toolRegistry: new Map(),
|
|
hasDeferredTools: false,
|
|
};
|
|
|
|
if (!tools || tools.length === 0) {
|
|
return emptyResult;
|
|
}
|
|
|
|
const mcpServerToolsCache = new Map<string, MCPServerTools>();
|
|
const mcpToolDefs: ToolDefinition[] = [];
|
|
const builtInToolDefs: ToolDefinition[] = [];
|
|
let actionToolDefs: ToolDefinition[] = [];
|
|
const actionToolNames: string[] = [];
|
|
|
|
const mcpAllPattern = `${Constants.mcp_all}${Constants.mcp_delimiter}`;
|
|
|
|
for (const toolName of tools) {
|
|
if (isActionTool(toolName)) {
|
|
actionToolNames.push(toolName);
|
|
continue;
|
|
}
|
|
|
|
if (!mcpToolPattern.test(toolName)) {
|
|
if (!isBuiltInTool(toolName)) {
|
|
continue;
|
|
}
|
|
const registryDef = getToolDefinition(toolName);
|
|
if (!registryDef) {
|
|
continue;
|
|
}
|
|
builtInToolDefs.push({
|
|
name: toolName,
|
|
description: registryDef.description,
|
|
parameters: registryDef.schema as JsonSchemaType | undefined,
|
|
});
|
|
|
|
const extraTools = toolkitExpansion[toolName as keyof typeof toolkitExpansion];
|
|
if (extraTools) {
|
|
for (const extra of extraTools) {
|
|
const extraDef = getToolDefinition(extra);
|
|
if (extraDef) {
|
|
builtInToolDefs.push({
|
|
name: extra,
|
|
description: extraDef.description,
|
|
parameters: extraDef.schema as JsonSchemaType | undefined,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const parts = toolName.split(Constants.mcp_delimiter);
|
|
const serverName = parts[parts.length - 1];
|
|
|
|
if (!mcpServerToolsCache.has(serverName)) {
|
|
const serverTools = await getOrFetchMCPServerTools(userId, serverName);
|
|
mcpServerToolsCache.set(serverName, serverTools || {});
|
|
}
|
|
|
|
const serverTools = mcpServerToolsCache.get(serverName);
|
|
if (!serverTools) {
|
|
continue;
|
|
}
|
|
|
|
if (toolName.startsWith(mcpAllPattern)) {
|
|
for (const [actualToolName, toolDef] of Object.entries(serverTools)) {
|
|
if (toolDef?.function) {
|
|
mcpToolDefs.push({
|
|
name: actualToolName,
|
|
description: toolDef.function.description,
|
|
parameters: toolDef.function.parameters
|
|
? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters))
|
|
: undefined,
|
|
serverName,
|
|
});
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const toolDef = serverTools[toolName];
|
|
if (toolDef?.function) {
|
|
mcpToolDefs.push({
|
|
name: toolName,
|
|
description: toolDef.function.description,
|
|
parameters: toolDef.function.parameters
|
|
? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters))
|
|
: undefined,
|
|
serverName,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (actionToolNames.length > 0 && getActionToolDefinitions) {
|
|
const fetchedActionDefs = await getActionToolDefinitions(agentId, actionToolNames);
|
|
actionToolDefs = fetchedActionDefs.map((def) => ({
|
|
name: def.name,
|
|
description: def.description,
|
|
parameters: def.parameters,
|
|
}));
|
|
}
|
|
|
|
const loadedTools = mcpToolDefs.map((def) => ({
|
|
name: def.name,
|
|
description: def.description,
|
|
mcp: true as const,
|
|
mcpJsonSchema: def.parameters,
|
|
})) as unknown as GenericTool[];
|
|
|
|
const classificationResult = await buildToolClassification({
|
|
userId,
|
|
agentId,
|
|
loadedTools,
|
|
loadAuthValues,
|
|
deferredToolsEnabled,
|
|
definitionsOnly: true,
|
|
agentToolOptions: toolOptions,
|
|
});
|
|
|
|
const { toolDefinitions, hasDeferredTools } = classificationResult;
|
|
const toolRegistry: LCToolRegistry = classificationResult.toolRegistry ?? new Map();
|
|
|
|
for (const actionDef of actionToolDefs) {
|
|
if (!toolRegistry.has(actionDef.name)) {
|
|
toolRegistry.set(actionDef.name, {
|
|
name: actionDef.name,
|
|
description: actionDef.description,
|
|
parameters: actionDef.parameters,
|
|
allowed_callers: ['direct'],
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const builtInDef of builtInToolDefs) {
|
|
if (!toolRegistry.has(builtInDef.name)) {
|
|
toolRegistry.set(builtInDef.name, {
|
|
name: builtInDef.name,
|
|
description: builtInDef.description,
|
|
parameters: builtInDef.parameters,
|
|
allowed_callers: ['direct'],
|
|
});
|
|
}
|
|
}
|
|
|
|
const allDefinitions: (ToolDefinition | LCTool)[] = [
|
|
...toolDefinitions,
|
|
...actionToolDefs.filter((d) => !toolDefinitions.some((td) => td.name === d.name)),
|
|
...builtInToolDefs.filter((d) => !toolDefinitions.some((td) => td.name === d.name)),
|
|
];
|
|
|
|
return {
|
|
toolDefinitions: allDefinitions,
|
|
toolRegistry,
|
|
hasDeferredTools,
|
|
};
|
|
}
|