LibreChat/client/src/hooks/Agents/useMCPToolOptions.ts

184 lines
6 KiB
TypeScript
Raw Normal View History

🎯 feat: Add Programmatic Tool Calling UI for MCP Tools (#11604) * feat: MCP Tool Functionality with Tool Options Management - Introduced `MCPToolItem` component for better handling of individual tool options, including selection, deferral, and programmatic invocation. - Added `useMCPToolOptions` hook to manage tool options state, enabling deferred loading and programmatic calling for tools. - Updated `MCPTool` component to integrate new tool options management, improving user interaction with tool selection and settings. - Enhanced localization support for new tool options in translation files. This update streamlines the management of MCP tools, allowing for more flexible configurations and improved user experience. * feat: MCP Tool UI for Programmatic Tools - Added support for programmatic tools in the MCPTool and MCPToolItem components, allowing for conditional rendering based on the availability of programmatic capabilities. - Updated the useAgentCapabilities hook to include programmaticToolsEnabled, enhancing the capability checks for agents. - Enhanced unit tests for useAgentCapabilities to validate the new programmatic tools functionality. - Improved localization for programmatic tool descriptions, ensuring clarity in user interactions. This update improves the flexibility and usability of the MCP Tool, enabling users to leverage programmatic tools effectively. * fix: Update localization for MCP Tool UI - Removed outdated descriptions for programmatic tool interactions in the translation file. - Enhanced clarity in user-facing text for tool options, ensuring accurate representation of functionality. This update improves the user experience by providing clearer instructions and descriptions for programmatic tools in the MCP Tool UI. * chore: ESLint fix * feat: Add unit tests for useMCPToolOptions hook - Introduced comprehensive tests for the useMCPToolOptions hook, covering functionalities such as tool deferral and programmatic calling. - Implemented tests for toggling tool options, ensuring correct state management and preservation of existing configurations. - Enhanced mock implementations for useFormContext and useWatch to facilitate testing scenarios. This update improves test coverage and reliability for the MCP Tool options management, ensuring robust validation of expected behaviors. * fix: Adjust gap spacing in MCPToolItem component - Updated the gap spacing in the MCPToolItem component from 1 to 1.5 for improved layout consistency. - This change enhances the visual alignment of icons and text within the component, contributing to a better user interface experience. * fix: Comment out programmatic tools in default agent capabilities - Commented out the inclusion of programmatic_tools in the defaultAgentCapabilities array, as it requires the latest Code Interpreter API. - This change ensures compatibility and prevents potential issues until the necessary API updates are integrated.
2026-02-02 08:37:17 -05:00
import { useCallback } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import type { AgentToolOptions, AllowedCaller, AgentToolType } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
interface UseMCPToolOptionsReturn {
formToolOptions: AgentToolOptions | undefined;
isToolDeferred: (toolId: string) => boolean;
isToolProgrammatic: (toolId: string) => boolean;
toggleToolDefer: (toolId: string) => void;
toggleToolProgrammatic: (toolId: string) => void;
areAllToolsDeferred: (tools: AgentToolType[]) => boolean;
areAllToolsProgrammatic: (tools: AgentToolType[]) => boolean;
toggleDeferAll: (tools: AgentToolType[]) => void;
toggleProgrammaticAll: (tools: AgentToolType[]) => void;
}
export default function useMCPToolOptions(): UseMCPToolOptionsReturn {
const { getValues, setValue, control } = useFormContext<AgentForm>();
const formToolOptions = useWatch({ control, name: 'tool_options' });
const isToolDeferred = useCallback(
(toolId: string): boolean => formToolOptions?.[toolId]?.defer_loading === true,
[formToolOptions],
);
const isToolProgrammatic = useCallback(
(toolId: string): boolean =>
formToolOptions?.[toolId]?.allowed_callers?.includes('code_execution') === true,
[formToolOptions],
);
const toggleToolDefer = useCallback(
(toolId: string) => {
const currentOptions = getValues('tool_options') || {};
const currentToolOptions = currentOptions[toolId] || {};
const newDeferred = !currentToolOptions.defer_loading;
const updatedOptions: AgentToolOptions = { ...currentOptions };
if (newDeferred) {
updatedOptions[toolId] = {
...currentToolOptions,
defer_loading: true,
};
} else {
const { defer_loading: _, ...restOptions } = currentToolOptions;
if (Object.keys(restOptions).length === 0) {
delete updatedOptions[toolId];
} else {
updatedOptions[toolId] = restOptions;
}
}
setValue('tool_options', updatedOptions, { shouldDirty: true });
},
[getValues, setValue],
);
const toggleToolProgrammatic = useCallback(
(toolId: string) => {
const currentOptions = getValues('tool_options') || {};
const currentToolOptions = currentOptions[toolId] || {};
const currentCallers = currentToolOptions.allowed_callers || [];
const isProgrammatic = currentCallers.includes('code_execution');
const updatedOptions: AgentToolOptions = { ...currentOptions };
if (isProgrammatic) {
const newCallers = currentCallers.filter((c: AllowedCaller) => c !== 'code_execution');
if (newCallers.length === 0) {
const { allowed_callers: _, ...restOptions } = currentToolOptions;
if (Object.keys(restOptions).length === 0) {
delete updatedOptions[toolId];
} else {
updatedOptions[toolId] = restOptions;
}
} else {
updatedOptions[toolId] = {
...currentToolOptions,
allowed_callers: newCallers,
};
}
} else {
updatedOptions[toolId] = {
...currentToolOptions,
allowed_callers: ['code_execution'] as AllowedCaller[],
};
}
setValue('tool_options', updatedOptions, { shouldDirty: true });
},
[getValues, setValue],
);
const areAllToolsDeferred = useCallback(
(tools: AgentToolType[]): boolean =>
tools.length > 0 &&
tools.every((tool) => formToolOptions?.[tool.tool_id]?.defer_loading === true),
[formToolOptions],
);
const areAllToolsProgrammatic = useCallback(
(tools: AgentToolType[]): boolean =>
tools.length > 0 &&
tools.every(
(tool) =>
formToolOptions?.[tool.tool_id]?.allowed_callers?.includes('code_execution') === true,
),
[formToolOptions],
);
const toggleDeferAll = useCallback(
(tools: AgentToolType[]) => {
if (tools.length === 0) return;
const shouldDefer = !areAllToolsDeferred(tools);
const currentOptions = getValues('tool_options') || {};
const updatedOptions: AgentToolOptions = { ...currentOptions };
for (const tool of tools) {
if (shouldDefer) {
updatedOptions[tool.tool_id] = {
...(updatedOptions[tool.tool_id] || {}),
defer_loading: true,
};
} else {
if (updatedOptions[tool.tool_id]) {
delete updatedOptions[tool.tool_id].defer_loading;
if (Object.keys(updatedOptions[tool.tool_id]).length === 0) {
delete updatedOptions[tool.tool_id];
}
}
}
}
setValue('tool_options', updatedOptions, { shouldDirty: true });
},
[getValues, setValue, areAllToolsDeferred],
);
const toggleProgrammaticAll = useCallback(
(tools: AgentToolType[]) => {
if (tools.length === 0) return;
const shouldBeProgrammatic = !areAllToolsProgrammatic(tools);
const currentOptions = getValues('tool_options') || {};
const updatedOptions: AgentToolOptions = { ...currentOptions };
for (const tool of tools) {
const currentToolOptions = updatedOptions[tool.tool_id] || {};
if (shouldBeProgrammatic) {
updatedOptions[tool.tool_id] = {
...currentToolOptions,
allowed_callers: ['code_execution'] as AllowedCaller[],
};
} else {
if (updatedOptions[tool.tool_id]) {
delete updatedOptions[tool.tool_id].allowed_callers;
if (Object.keys(updatedOptions[tool.tool_id]).length === 0) {
delete updatedOptions[tool.tool_id];
}
}
}
}
setValue('tool_options', updatedOptions, { shouldDirty: true });
},
[getValues, setValue, areAllToolsProgrammatic],
);
return {
formToolOptions,
isToolDeferred,
isToolProgrammatic,
toggleToolDefer,
toggleToolProgrammatic,
areAllToolsDeferred,
areAllToolsProgrammatic,
toggleDeferAll,
toggleProgrammaticAll,
};
}