import React, { useState } from 'react'; import * as Ariakit from '@ariakit/react'; import { ChevronDown } from 'lucide-react'; import { useFormContext } from 'react-hook-form'; import { Constants } from 'librechat-data-provider'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import { Label, Checkbox, OGDialog, Accordion, TrashIcon, AccordionItem, CircleHelpIcon, OGDialogTrigger, AccordionContent, OGDialogTemplate, } from '@librechat/client'; import type { AgentForm, MCPServerInfo } from '~/common'; import { useLocalize, useMCPServerManager, useRemoveMCPTool } from '~/hooks'; import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon'; import MCPConfigDialog from '~/components/MCP/MCPConfigDialog'; import { cn } from '~/utils'; export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) { const localize = useLocalize(); const { removeTool } = useRemoveMCPTool(); const { getValues, setValue } = useFormContext(); const { getServerStatusIconProps, getConfigDialogProps } = useMCPServerManager(); const [isFocused, setIsFocused] = useState(false); const [isHovering, setIsHovering] = useState(false); const [accordionValue, setAccordionValue] = useState(''); const [hoveredToolId, setHoveredToolId] = useState(null); if (!serverInfo) { return null; } const currentServerName = serverInfo.serverName; const getSelectedTools = () => { if (!serverInfo?.tools) return []; const formTools = getValues('tools') || []; return serverInfo.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) => !serverInfo?.tools?.some((st) => st.tool_id === t), ); setValue('tools', [...otherTools, ...newSelectedTools]); }; const selectedTools = getSelectedTools(); const isExpanded = accordionValue === currentServerName; const statusIconProps = getServerStatusIconProps(currentServerName); const configDialogProps = getConfigDialogProps(); const statusIcon = statusIconProps && (
{ e.stopPropagation(); }} className="cursor-pointer rounded p-0.5 hover:bg-surface-secondary" >
); return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} onFocus={() => setIsFocused(true)} onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) { setIsFocused(false); } }} >
setAccordionValue((prev) => { if (prev) { return ''; } return currentServerName; }) } > {statusIcon &&
{statusIcon}
} {serverInfo.metadata.icon && (
)}
{currentServerName}
e.stopPropagation()} className="mt-1" > 0 } onCheckedChange={(checked) => { if (serverInfo.tools) { const newSelectedTools = checked ? serverInfo.tools.map((t) => t.tool_id) : [ `${Constants.mcp_server}${Constants.mcp_delimiter}${currentServerName}`, ]; updateFormTools(newSelectedTools); } }} className={cn( 'h-4 w-4 rounded border border-border-medium transition-all duration-200 hover:border-border-heavy', 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} aria-label={ selectedTools.length === serverInfo.tools?.length && selectedTools.length > 0 ? localize('com_ui_deselect_all') : localize('com_ui_select_all') } />
{/* Caret button for accordion */}
{serverInfo.tools?.map((subTool) => ( ))}
{localize('com_ui_delete_tool_confirm')} } selection={{ selectHandler: () => removeTool(currentServerName), 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'), }} /> {configDialogProps && } ); }