mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-25 20:04:09 +01:00
* fix: Convert `const` to `enum` in MCP tool schemas for Gemini/Vertex AI compatibility Gemini/Vertex AI rejects the JSON Schema `const` keyword in function declarations with a 400 error. Previously, the Zod conversion layer accidentally stripped `const`, but after migrating to pass raw JSON schemas directly to providers, the unsupported keyword now reaches Gemini verbatim. Add `normalizeJsonSchema` to recursively convert `const: X` → `enum: [X]`, which is semantically equivalent per the JSON Schema spec and supported by all providers. * fix: Update secure cookie handling in AuthService to use dynamic secure flag Replaced the static `secure: isProduction` with a call to `shouldUseSecureCookie()` in the `setOpenIDAuthTokens` function. This change ensures that the secure cookie setting is evaluated at runtime, improving cookie handling in development environments while maintaining security in production. * refactor: Simplify MCP tool key formatting and remove unused mocks in tests - Updated MCP test suite to replace static tool key formatting with a dynamic delimiter from Constants, enhancing consistency and maintainability. - Removed unused mock implementations for `@langchain/core/tools` and `@librechat/agents`, streamlining the test setup. - Adjusted related test cases to reflect the new tool key format, ensuring all tests remain functional. * chore: import order
226 lines
6.9 KiB
TypeScript
226 lines
6.9 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, actionDelimiter } 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';
|
|
|
|
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 (toolName.includes(actionDelimiter)) {
|
|
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,
|
|
});
|
|
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,
|
|
};
|
|
}
|