🧰 refactor: Decouple MCP Tools from System Tools (#9748)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run

This commit is contained in:
Danny Avila 2025-09-21 07:56:40 -04:00 committed by GitHub
parent 9d2aba5df5
commit 386900fb4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1032 additions and 1195 deletions

View file

@ -48,12 +48,12 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
const {
actions,
setAction,
regularTools,
agentsConfig,
startupConfig,
mcpServersMap,
setActivePanel,
endpointsConfig,
groupedTools: allTools,
} = useAgentPanelContext();
const {
@ -177,7 +177,7 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
Icon = icons[iconKey];
}
const { toolIds, mcpServerNames } = useVisibleTools(tools, allTools, mcpServersMap);
const { toolIds, mcpServerNames } = useVisibleTools(tools, regularTools, mcpServersMap);
return (
<>
@ -326,16 +326,15 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
</label>
<div>
<div className="mb-1">
{/* Render all visible IDs (including groups with subtools selected) */}
{/* Render all visible IDs */}
{toolIds.map((toolId, i) => {
if (!allTools) return null;
const tool = allTools[toolId];
const tool = regularTools?.find((t) => t.pluginKey === toolId);
if (!tool) return null;
return (
<AgentTool
key={`${toolId}-${i}-${agent_id}`}
tool={toolId}
allTools={allTools}
regularTools={regularTools}
agent_id={agent_id}
/>
);

View file

@ -1,68 +1,47 @@
import React, { useState } from 'react';
import * as Ariakit from '@ariakit/react';
import { ChevronDown } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import {
Accordion,
AccordionItem,
AccordionContent,
OGDialog,
TrashIcon,
CircleHelpIcon,
OGDialog,
OGDialogTrigger,
Label,
Checkbox,
OGDialogTemplate,
useToastContext,
OGDialogTrigger,
OGDialogTemplate,
} from '@librechat/client';
import type { AgentToolType } from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
export default function AgentTool({
tool,
allTools,
regularTools,
}: {
tool: string;
allTools?: Record<string, AgentToolType & { tools?: AgentToolType[] }>;
regularTools?: TPlugin[];
agent_id?: string;
}) {
const [isHovering, setIsHovering] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [hoveredToolId, setHoveredToolId] = useState<string | null>(null);
const [accordionValue, setAccordionValue] = useState<string>('');
const localize = useLocalize();
const { showToast } = useToastContext();
const updateUserPlugins = useUpdateUserPluginsMutation();
const { getValues, setValue } = useFormContext<AgentForm>();
if (!allTools) {
const [isFocused, setIsFocused] = useState(false);
const [isHovering, setIsHovering] = useState(false);
if (!regularTools) {
return null;
}
const currentTool = allTools[tool];
const getSelectedTools = () => {
if (!currentTool?.tools) return [];
const formTools = getValues('tools') || [];
return currentTool.tools.filter((t) => formTools.includes(t.tool_id)).map((t) => t.tool_id);
};
const updateFormTools = (newSelectedTools: string[]) => {
const currentTools = getValues('tools') || [];
const otherTools = currentTools.filter(
(t: string) => !currentTool?.tools?.some((st) => st.tool_id === t),
);
setValue('tools', [...otherTools, ...newSelectedTools]);
};
const currentTool = regularTools.find((t) => t.pluginKey === tool);
if (!currentTool) {
return null;
}
const removeTool = (toolId: string) => {
if (toolId) {
const toolIdsToRemove =
isGroup && currentTool.tools
? [toolId, ...currentTool.tools.map((t) => t.tool_id)]
: [toolId];
updateUserPlugins.mutate(
{ pluginKey: toolId, action: 'uninstall', auth: {}, isEntityTool: true },
{
@ -70,9 +49,7 @@ export default function AgentTool({
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
},
onSuccess: () => {
const remainingToolIds = getValues('tools')?.filter(
(toolId: string) => !toolIdsToRemove.includes(toolId),
);
const remainingToolIds = getValues('tools')?.filter((id: string) => id !== toolId);
setValue('tools', remainingToolIds);
showToast({ message: 'Tool deleted successfully', status: 'success' });
},
@ -81,327 +58,82 @@ export default function AgentTool({
}
};
if (!currentTool) {
return null;
}
const isGroup = currentTool.tools && currentTool.tools.length > 0;
const selectedTools = getSelectedTools();
const isExpanded = accordionValue === currentTool.tool_id;
if (!isGroup) {
return (
<OGDialog>
<div
className="group relative flex w-full items-center gap-1 rounded-lg p-1 text-sm hover:bg-gray-50 dark:hover:bg-gray-800/50"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onFocus={() => setIsFocused(true)}
onBlur={(e) => {
// Check if focus is moving to a child element
if (!e.currentTarget.contains(e.relatedTarget)) {
setIsFocused(false);
}
}}
>
<div className="flex grow items-center">
{currentTool.metadata.icon && (
<div className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full">
<div
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
style={{
backgroundImage: `url(${currentTool.metadata.icon})`,
backgroundSize: 'cover',
}}
/>
</div>
)}
<div
className="grow px-2 py-1.5"
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
>
{currentTool.metadata.name}
</div>
</div>
<OGDialogTrigger asChild>
<button
type="button"
className={cn(
'flex h-7 w-7 items-center justify-center rounded transition-all duration-200',
'hover:bg-gray-200 dark:hover:bg-gray-700',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
'focus:opacity-100',
isHovering || isFocused ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
aria-label={`Delete ${currentTool.metadata.name}`}
tabIndex={0}
onFocus={() => setIsFocused(true)}
>
<TrashIcon className="h-4 w-4" />
</button>
</OGDialogTrigger>
</div>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_tool')}
mainClassName="px-0"
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">
{localize('com_ui_delete_tool_confirm')}
</Label>
}
selection={{
selectHandler: () => removeTool(currentTool.tool_id),
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</OGDialog>
);
}
// Group tool with accordion
return (
<OGDialog>
<Accordion type="single" value={accordionValue} onValueChange={setAccordionValue} collapsible>
<AccordionItem value={currentTool.tool_id} className="group relative w-full border-none">
<div
className="relative flex w-full items-center gap-1 rounded-lg p-1 hover:bg-gray-50 dark:hover:bg-gray-800/50"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onFocus={() => setIsFocused(true)}
onBlur={(e) => {
// Check if focus is moving to a child element
if (!e.currentTarget.contains(e.relatedTarget)) {
setIsFocused(false);
}
}}
>
<AccordionPrimitive.Header asChild>
<AccordionPrimitive.Trigger asChild>
<button
type="button"
className={cn(
'flex grow items-center gap-1 rounded bg-transparent p-0 text-left transition-colors',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
)}
>
{currentTool.metadata.icon && (
<div className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full">
<div
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
style={{
backgroundImage: `url(${currentTool.metadata.icon})`,
backgroundSize: 'cover',
}}
/>
</div>
)}
<div
className="grow px-2 py-1.5"
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
>
{currentTool.metadata.name}
</div>
<div className="flex items-center">
{/* Container for grouped checkbox and chevron */}
<div className="relative flex items-center">
{/* Grouped checkbox and chevron that slide together */}
<div
className={cn(
'flex items-center gap-2 transition-all duration-300',
isHovering || isFocused ? '-translate-x-8' : 'translate-x-0',
)}
>
<div
data-checkbox-container
onClick={(e) => e.stopPropagation()}
className="mt-1"
>
<Checkbox
id={`select-all-${currentTool.tool_id}`}
checked={selectedTools.length === currentTool.tools?.length}
onCheckedChange={(checked) => {
if (currentTool.tools) {
const newSelectedTools = checked
? currentTool.tools.map((t) => t.tool_id)
: [];
updateFormTools(newSelectedTools);
}
}}
className={cn(
'h-4 w-4 rounded border border-gray-300 transition-all duration-200 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500',
isExpanded ? 'visible' : 'pointer-events-none invisible',
)}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
const checkbox = e.currentTarget as HTMLButtonElement;
checkbox.click();
}
}}
tabIndex={isExpanded ? 0 : -1}
/>
</div>
<div
className={cn(
'pointer-events-none flex h-4 w-4 items-center justify-center transition-transform duration-300',
isExpanded ? 'rotate-180' : '',
)}
aria-hidden="true"
>
<ChevronDown className="h-4 w-4" />
</div>
</div>
{/* Delete button slides in from behind */}
<div
className={cn(
'absolute right-0 transition-all duration-300',
isHovering || isFocused
? 'translate-x-0 opacity-100'
: 'translate-x-8 opacity-0',
)}
>
<OGDialogTrigger asChild>
<button
type="button"
className={cn(
'flex h-7 w-7 items-center justify-center rounded transition-colors duration-200',
'hover:bg-gray-200 dark:hover:bg-gray-700',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
'focus:translate-x-0 focus:opacity-100',
)}
onClick={(e) => e.stopPropagation()}
aria-label={`Delete ${currentTool.metadata.name}`}
tabIndex={0}
onFocus={() => setIsFocused(true)}
>
<TrashIcon className="h-4 w-4" />
</button>
</OGDialogTrigger>
</div>
</div>
</div>
</button>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
</div>
<AccordionContent className="relative ml-1 pt-1 before:absolute before:bottom-2 before:left-0 before:top-0 before:w-0.5 before:bg-border-medium">
<div className="space-y-1">
{currentTool.tools?.map((subTool) => (
<label
key={subTool.tool_id}
htmlFor={subTool.tool_id}
className={cn(
'border-token-border-light hover:bg-token-surface-secondary flex cursor-pointer items-center rounded-lg border p-2',
'ml-2 mr-1 focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background',
)}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
e.stopPropagation();
}}
onMouseEnter={() => setHoveredToolId(subTool.tool_id)}
onMouseLeave={() => setHoveredToolId(null)}
>
<Checkbox
id={subTool.tool_id}
checked={selectedTools.includes(subTool.tool_id)}
onCheckedChange={(_checked) => {
const newSelectedTools = selectedTools.includes(subTool.tool_id)
? selectedTools.filter((t) => t !== subTool.tool_id)
: [...selectedTools, subTool.tool_id];
updateFormTools(newSelectedTools);
}}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const checkbox = e.currentTarget as HTMLButtonElement;
checkbox.click();
}
}}
onClick={(e) => e.stopPropagation()}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer rounded border border-gray-300 transition-[border-color] duration-200 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background dark:border-gray-600 dark:hover:border-gray-500"
/>
<span className="text-token-text-primary">{subTool.metadata.name}</span>
{subTool.metadata.description && (
<Ariakit.HovercardProvider placement="left-start">
<div className="ml-auto flex h-6 w-6 items-center justify-center">
<Ariakit.HovercardAnchor
render={
<Ariakit.Button
className={cn(
'flex h-5 w-5 cursor-help items-center rounded-full text-text-secondary transition-opacity duration-200',
hoveredToolId === subTool.tool_id ? 'opacity-100' : 'opacity-0',
)}
aria-label={localize('com_ui_tool_info')}
>
<CircleHelpIcon className="h-4 w-4" />
<Ariakit.VisuallyHidden>
{localize('com_ui_tool_info')}
</Ariakit.VisuallyHidden>
</Ariakit.Button>
}
/>
<Ariakit.HovercardDisclosure
className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring"
aria-label={localize('com_ui_tool_more_info')}
aria-expanded={hoveredToolId === subTool.tool_id}
aria-controls={`tool-description-${subTool.tool_id}`}
>
<Ariakit.VisuallyHidden>
{localize('com_ui_tool_more_info')}
</Ariakit.VisuallyHidden>
<ChevronDown className="h-4 w-4" />
</Ariakit.HovercardDisclosure>
</div>
<Ariakit.Hovercard
id={`tool-description-${subTool.tool_id}`}
gutter={14}
shift={40}
flip={false}
className="z-[999] w-80 scale-95 rounded-2xl border border-border-medium bg-surface-secondary p-4 text-text-primary opacity-0 shadow-md transition-all duration-200 data-[enter]:scale-100 data-[leave]:scale-95 data-[enter]:opacity-100 data-[leave]:opacity-0"
portal={true}
unmountOnHide={true}
role="tooltip"
aria-label={subTool.metadata.description}
>
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{subTool.metadata.description}
</p>
</div>
</Ariakit.Hovercard>
</Ariakit.HovercardProvider>
)}
</label>
))}
<div
className="group relative flex w-full items-center gap-1 rounded-lg p-1 text-sm hover:bg-gray-50 dark:hover:bg-gray-800/50"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onFocus={() => setIsFocused(true)}
onBlur={(e) => {
// Check if focus is moving to a child element
if (!e.currentTarget.contains(e.relatedTarget)) {
setIsFocused(false);
}
}}
>
<div className="flex grow items-center">
{currentTool.icon && (
<div className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full">
<div
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
style={{
backgroundImage: `url(${currentTool.icon})`,
backgroundSize: 'cover',
}}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
<div
className="grow px-2 py-1.5"
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
>
{currentTool.name}
</div>
</div>
<OGDialogTrigger asChild>
<button
type="button"
className={cn(
'flex h-7 w-7 items-center justify-center rounded transition-all duration-200',
'hover:bg-gray-200 dark:hover:bg-gray-700',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
'focus:opacity-100',
isHovering || isFocused ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
aria-label={`Delete ${currentTool.name}`}
tabIndex={0}
onFocus={() => setIsFocused(true)}
>
<TrashIcon className="h-4 w-4" />
</button>
</OGDialogTrigger>
</div>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_tool')}
mainClassName="px-0"
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">
{localize('com_ui_delete_tool_confirm')}
</Label>
<>
<div className="flex w-full flex-col items-start gap-2 text-sm text-text-secondary">
<p>
{localize('com_ui_delete_tool_confirm')}{' '}
<strong>&quot;{currentTool.name}&quot;</strong>?
</p>
{currentTool.description && (
<div className="flex items-start gap-2">
<CircleHelpIcon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
<p className="text-sm">{currentTool.description}</p>
</div>
)}
</div>
</>
}
selection={{
selectHandler: () => removeTool(currentTool.tool_id),
selectHandler: () => removeTool(tool),
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
'bg-red-700 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>

View file

@ -56,7 +56,7 @@ function MCPPanelContent() {
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
await Promise.all([
queryClient.refetchQueries([QueryKeys.tools]),
queryClient.refetchQueries([QueryKeys.mcpTools]),
queryClient.refetchQueries([QueryKeys.mcpAuthValues]),
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]),
]);

View file

@ -7,8 +7,8 @@ import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-quer
import type { TError, AgentToolType } from 'librechat-data-provider';
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
import { useLocalize, usePluginDialogHelpers, useMCPServerManager } from '~/hooks';
import { useGetStartupConfig, useAvailableToolsQuery } from '~/data-provider';
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
import { useGetStartupConfig, useMCPToolsQuery } from '~/data-provider';
import { PluginPagination } from '~/components/Plugins/Store';
import { useAgentPanelContext } from '~/Providers';
import MCPToolItem from './MCPToolItem';
@ -27,8 +27,8 @@ function MCPToolSelectDialog({
const { mcpServersMap } = useAgentPanelContext();
const { initializeServer } = useMCPServerManager();
const { data: startupConfig } = useGetStartupConfig();
const { refetch: refetchMCPTools } = useMCPToolsQuery();
const { getValues, setValue } = useFormContext<AgentForm>();
const { refetch: refetchAvailableTools } = useAvailableToolsQuery(EModelEndpoint.agents);
const [isInitializing, setIsInitializing] = useState<string | null>(null);
const [configuringServer, setConfiguringServer] = useState<string | null>(null);
@ -90,15 +90,15 @@ function MCPToolSelectDialog({
setIsInitializing(null);
},
onSuccess: async () => {
const { data: updatedAvailableTools } = await refetchAvailableTools();
const { data: updatedMCPTools } = await refetchMCPTools();
const currentTools = getValues('tools') || [];
const toolsToAdd: string[] = [
`${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`,
];
if (updatedAvailableTools) {
updatedAvailableTools.forEach((tool) => {
if (updatedMCPTools) {
updatedMCPTools.forEach((tool) => {
if (tool.pluginKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) {
toolsToAdd.push(tool.pluginKey);
}

View file

@ -8,7 +8,7 @@ import type {
AssistantsEndpoint,
EModelEndpoint,
TPluginAction,
AgentToolType,
TPlugin,
TError,
} from 'librechat-data-provider';
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
@ -27,7 +27,8 @@ function ToolSelectDialog({
const localize = useLocalize();
const isAgentTools = isAgentsEndpoint(endpoint);
const { getValues, setValue } = useFormContext<AgentForm>();
const { groupedTools, pluginTools } = useAgentPanelContext();
// Only use regular tools, not MCP tools
const { regularTools } = useAgentPanelContext();
const {
maxPage,
@ -68,19 +69,8 @@ function ToolSelectDialog({
const handleInstall = (pluginAction: TPluginAction) => {
const addFunction = () => {
const installedToolIds: string[] = getValues('tools') || [];
// Add the parent
installedToolIds.push(pluginAction.pluginKey);
// If this tool is a group, add subtools too
const groupObj = groupedTools?.[pluginAction.pluginKey];
if (groupObj?.tools && groupObj.tools.length > 0) {
for (const sub of groupObj.tools) {
if (!installedToolIds.includes(sub.tool_id)) {
installedToolIds.push(sub.tool_id);
}
}
}
setValue('tools', Array.from(new Set(installedToolIds))); // no duplicates just in case
setValue('tools', Array.from(new Set(installedToolIds)));
};
if (!pluginAction.auth) {
@ -98,19 +88,12 @@ function ToolSelectDialog({
};
const onRemoveTool = (toolId: string) => {
const groupObj = groupedTools?.[toolId];
const toolIdsToRemove = [toolId];
if (groupObj?.tools && groupObj.tools.length > 0) {
toolIdsToRemove.push(...groupObj.tools.map((sub) => sub.tool_id));
}
// Remove these from the formTools
updateUserPlugins.mutate(
{ pluginKey: toolId, action: 'uninstall', auth: {}, isEntityTool: true },
{
onError: (error: unknown) => handleInstallError(error as TError),
onSuccess: () => {
const remainingToolIds =
getValues('tools')?.filter((toolId) => !toolIdsToRemove.includes(toolId)) || [];
const remainingToolIds = getValues('tools')?.filter((id) => id !== toolId) || [];
setValue('tools', remainingToolIds);
},
},
@ -119,7 +102,8 @@ function ToolSelectDialog({
const onAddTool = (pluginKey: string) => {
setShowPluginAuthForm(false);
const availablePluginFromKey = pluginTools?.find((p) => p.pluginKey === pluginKey);
// Find the tool in regularTools
const availablePluginFromKey = regularTools?.find((p) => p.pluginKey === pluginKey);
setSelectedPlugin(availablePluginFromKey);
const { authConfig, authenticated = false } = availablePluginFromKey ?? {};
@ -134,30 +118,19 @@ function ToolSelectDialog({
}
};
const filteredTools = Object.values(groupedTools || {}).filter(
(currentTool: AgentToolType & { tools?: AgentToolType[] }) => {
if (currentTool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase())) {
return true;
}
if (currentTool.tools) {
return currentTool.tools.some((childTool) =>
childTool.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase()),
);
}
return false;
},
);
const filteredTools = (regularTools || []).filter((tool: TPlugin) => {
return tool.name?.toLowerCase().includes(searchValue.toLowerCase());
});
useEffect(() => {
if (filteredTools) {
setMaxPage(Math.ceil(Object.keys(filteredTools || {}).length / itemsPerPage));
setMaxPage(Math.ceil(filteredTools.length / itemsPerPage));
if (searchChanged) {
setCurrentPage(1);
setSearchChanged(false);
}
}
}, [
pluginTools,
searchValue,
itemsPerPage,
filteredTools,
@ -254,10 +227,13 @@ function ToolSelectDialog({
.map((tool, index) => (
<ToolItem
key={index}
tool={tool}
isInstalled={getValues('tools')?.includes(tool.tool_id) || false}
onAddTool={() => onAddTool(tool.tool_id)}
onRemoveTool={() => onRemoveTool(tool.tool_id)}
tool={{
tool_id: tool.pluginKey,
metadata: tool,
}}
isInstalled={getValues('tools')?.includes(tool.pluginKey) || false}
onAddTool={() => onAddTool(tool.pluginKey)}
onRemoveTool={() => onRemoveTool(tool.pluginKey)}
/>
))}
</div>