Skip to main content
Glama
NewToolCatalog.tsx24.8 kB
import { Loader2, Search } from "lucide-react"; import { Input } from "@/components/ui/input"; import { ToolGroupSheet } from "@/components/tools/ToolGroupSheet"; import { CustomToolDialog } from "@/components/tools/CustomToolDialog"; import { AddServerModal } from "@/components/dashboard/AddServerModal"; import { ToolDetailsDialog } from "@/components/tools/ToolDetailsDialog"; import { ToolGroupsSection } from "@/components/tools/ToolGroupsSection"; import { ToolsCatalogSection } from "@/components/tools/ToolsCatalogSection"; import { SelectionPanel } from "@/components/tools/SelectionPanel"; import { CreateToolGroupModal } from "@/components/tools/CreateToolGroupModal"; import { EditToolGroupModal } from "@/components/tools/EditToolGroupModal"; import { toast, useToast } from "@/components/ui/use-toast"; import { useToolCatalog } from "@/hooks/useToolCatalog"; import { ToolsItem } from "@/types"; import type { ToolCardTool } from "@/components/tools/ToolCard"; import { TargetServerNew } from "@mcpx/shared-model"; import { Button } from "@/components/ui/button"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; interface NewToolCatalogProps { searchFilter?: string; toolsList?: Array<ToolsItem>; handleEditClick: (tool: ToolsItem) => void; handleDuplicateClick: (tool: ToolsItem) => void; handleDeleteTool: (tool: ToolsItem) => void; handleCustomizeTool: (tool: ToolsItem) => void; dismissDeleteToast?: () => void; onToolGroupEditModeChange?: ( isEdit: boolean, cancelHandler: () => void, ) => void; } // Type for tool data in create/save handlers type CustomToolData = { server: string; tool: string; name: string; description: string; parameters: Array<{ name: string; description: string; value: string }>; originalName?: string; }; export default function NewToolCatalog({ toolsList = [], dismissDeleteToast, onToolGroupEditModeChange, }: NewToolCatalogProps) { useToast(); // Hook must be called even if not using its return const { selectedTools, setSelectedTools, showCreateModal, newGroupName, newGroupDescription, handleNewGroupDescriptionChange, createGroupError, handleNewGroupNameChange, isCreating, showEditGroupModal, editingGroupName, editingGroupDescription, handleOpenEditGroupModal, handleCloseEditGroupModal, handleEditGroupNameChange, handleEditGroupDescriptionChange, handleSaveGroupNameChanges, editGroupError, isSavingGroupName, currentGroupIndex, setCurrentGroupIndex, selectedToolGroup, setSelectedToolGroup, expandedProviders, setExpandedProviders, isToolGroupDialogOpen, setIsToolGroupDialogOpen, selectedToolGroupForDialog, setSelectedToolGroupForDialog, isEditMode, setIsEditMode, isCustomToolFullDialogOpen, setIsCustomToolFullDialogOpen, isEditCustomToolDialogOpen, setIsEditCustomToolDialogOpen, editingToolData, setEditingToolData, editDialogMode, isSavingCustomTool, searchQuery, setSearchQuery, isAddServerModalOpen, setIsAddServerModalOpen, isToolDetailsDialogOpen, setIsToolDetailsDialogOpen, selectedToolForDetails, setSelectedToolForDetails, editingGroup, originalSelectedTools, isSavingGroupChanges, providers, totalFilteredTools, transformedToolGroups, toolGroups, areSetsEqual, handleToolSelectionChange, handleSelectAllTools, handleCreateToolGroup, handleSaveToolGroup, handleCloseCreateModal, handleGroupNavigation, handleGroupClick, handleProviderClick, handleEditGroup, handleDeleteGroup, handleSaveGroupChanges, handleCancelGroupEdit, handleCreateCustomTool, handleEditCustomTool, handleSaveCustomTool, handleDeleteCustomTool, handleDuplicateCustomTool, handleCustomizeToolDialog, handleClickAddCustomToolMode, handleCancelAddCustomToolMode, isAddCustomToolMode, selectedCustomToolKey, setSelectedCustomToolKey, toolGroupOperation, } = useToolCatalog(toolsList); // Notify parent about edit mode changes useEffect(() => { if (onToolGroupEditModeChange) { onToolGroupEditModeChange(isEditMode, handleCancelGroupEdit); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isEditMode]); // Wrapper for delete tool that uses handleDeleteCustomTool from useToolCatalog const handleDeleteToolWrapper = async (tool: ToolCardTool) => { // Dismiss delete toast when deleting dismissDeleteToast?.(); // Use the handler from useToolCatalog which has the loader logic await handleDeleteCustomTool(tool); }; // Handle tool customization/edit based on tool type const handleToolAction = (tool: ToolCardTool) => { if (tool.isCustom) { // Dismiss delete toast when editing custom tool dismissDeleteToast?.(); handleEditCustomTool(tool); } else { handleCancelAddCustomToolMode(); // Exit add custom tool mode // Dismiss the add custom tool toast if (toastRef2.current) { toastRef2.current.dismiss?.(); toastRef2.current = null; } handleCustomizeToolDialog(tool); } }; const handleCloseCustomToolFullDialog = () => { // Dismiss delete toast when closing the custom tool dialog dismissDeleteToast?.(); setIsCustomToolFullDialogOpen(false); setEditingToolData(null); }; const handleCloseEditCustomToolDialog = () => { // Dismiss delete toast when closing the edit custom tool dialog dismissDeleteToast?.(); setIsEditCustomToolDialogOpen(false); setEditingToolData(null); }; const handleToolClick = (tool: ToolCardTool) => { // Dismiss delete toast when opening details drawer for custom tools if (tool.isCustom) { dismissDeleteToast?.(); } setSelectedToolForDetails(tool); setIsToolDetailsDialogOpen(true); }; const toastRef = useRef<ReturnType<typeof toast> | null>(null); const toastRef2 = useRef<ReturnType<typeof toast> | null>(null); // Track recently customized tools for loading animation const [recentlyCustomizedTools, setRecentlyCustomizedTools] = useState< Set<string> >(new Set()); // Track tools that are currently being customized (dialog is open) const currentlyCustomizingTools = useMemo(() => { const customizing = new Set<string>(); if ( editingToolData && (isCustomToolFullDialogOpen || isEditCustomToolDialogOpen) ) { // For editing custom tools, use the custom name (editingToolData.name) // For creating new custom tools, use the tool name (editingToolData.tool) const toolName = editingToolData.name || editingToolData.tool; customizing.add(`${editingToolData.server}:${toolName}`); } return customizing; }, [editingToolData, isCustomToolFullDialogOpen, isEditCustomToolDialogOpen]); // Wrapper function to trigger loading animation after customization const handleCreateCustomToolWithLoading = useCallback( async (toolData: CustomToolData) => { await handleCreateCustomTool(toolData); // Trigger loading animation for the customized tool if (toolData) { const toolKey = `${toolData.server}:${toolData.name}`; setRecentlyCustomizedTools((prev) => new Set([...prev, toolKey])); // Clear the loading trigger after animation completes setTimeout(() => { setRecentlyCustomizedTools((prev) => { const newSet = new Set(prev); newSet.delete(toolKey); return newSet; }); }, 3000); // Wait longer than skeleton duration } }, [handleCreateCustomTool], ); // Wrapper function to trigger loading animation after editing custom tools const handleSaveCustomToolWithLoading = useCallback( async (toolData: CustomToolData) => { await handleSaveCustomTool(toolData); // Trigger loading animation for the edited tool if (toolData) { // Use current name since that's what the UI will show after update const toolKey = `${toolData.server}:${toolData.name}`; setRecentlyCustomizedTools((prev) => new Set([...prev, toolKey])); // Clear the loading trigger after animation completes setTimeout(() => { setRecentlyCustomizedTools((prev) => { const newSet = new Set(prev); newSet.delete(toolKey); return newSet; }); }, 3000); // Wait longer than skeleton duration } }, [handleSaveCustomTool], ); const handleClickCreateNewTollGroup = () => { setIsEditMode(true); const newExpanded = new Set(providers.map((provider) => provider.name)); setExpandedProviders(newExpanded); }; const handleClickAddCustomTool = () => { if (isAddCustomToolMode) { handleCancelAddCustomToolMode(); setSelectedTools(new Set()); setSelectedCustomToolKey(null); setExpandedProviders(new Set()); setIsCustomToolFullDialogOpen(false); setEditingToolData(null); if (toastRef2.current) { toastRef2.current.dismiss?.(); toastRef2.current = null; } return; } handleClickAddCustomToolMode(); toastRef2.current = toast({ title: "Add Custom Tool", description: `Select 1 tool to customize`, isClosable: false, duration: 1000000, variant: "info", position: "bottom-left", }); }; const handleClickCreateToolGroup = () => { if (toastRef.current) { toastRef.current?.dismiss(); } if (isEditMode) { handleCancelGroupEdit(); } else { setIsEditMode(true); const newExpanded = new Set(providers.map((provider) => provider.name)); setExpandedProviders(newExpanded); } }; return ( <> {/* Full-page loader overlay for tool group operations */} {isCreating && toolGroupOperation && ( <div className="fixed inset-0 bg-white/80 backdrop-blur-sm z-[9999] flex items-center justify-center"> <div className="flex flex-col items-center gap-4"> <Loader2 className="w-12 h-12 animate-spin text-blue-600" /> <p className="text-lg font-medium text-gray-700"> {toolGroupOperation === "creating" && "Creating tool group..."} {toolGroupOperation === "editing" && "Updating tool group..."} {toolGroupOperation === "deleting" && "Deleting tool group..."} {!toolGroupOperation && "Processing..."} </p> </div> </div> )} <div className={`${styles.container} bg-gray-100`}> <div className={styles.content}> <div className="mb-6"> <div className="flex items-center gap-4"> <h1 className="text-3xl font-bold text-gray-900">Tool Catalog</h1> </div> {/* Search Bar */} <div className="mt-6 flex justify-between gap-2"> <div className="relative w-80"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> <Input type="text" placeholder="Search for tool..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-10 pr-4 py-2 border-gray-300 rounded-lg focus:ring-2 focus:ring-[#4F33CC] focus:border-transparent bg-white" /> </div> <div className="flex justify-end gap-3"> {!isEditMode && !showCreateModal && ( <Button onClick={handleClickAddCustomTool} className={`border-[#5147E4] border-2 ${isAddCustomToolMode ? "text-white bg-[#5147E4] hover:bg-[#5147E4]/90" : "text-[#5147E4] hover:bg-[#5147E4] hover:text-white bg-transparent"} px-4 py-2 rounded-lg font-medium transition-colors text-sm`} > {isAddCustomToolMode ? "Cancel" : "Add Custom Tool"} </Button> )} {!isAddCustomToolMode && ( <Button onClick={handleClickCreateToolGroup} disabled={isAddCustomToolMode} className={`${styles.editModeButton}`} > {isEditMode ? "Cancel" : "Create Tool Group"} </Button> )} </div> </div> </div> <ToolGroupsSection onEditGroup={handleEditGroup} onEditToolGroup={handleOpenEditGroupModal} onDeleteGroup={handleDeleteGroup} providers={providers as TargetServerNew[]} transformedToolGroups={transformedToolGroups} toolGroups={toolGroups} currentGroupIndex={currentGroupIndex} selectedToolGroup={selectedToolGroup} onGroupNavigation={handleGroupNavigation} onGroupClick={handleGroupClick} onEditModeToggle={handleClickCreateNewTollGroup} isEditMode={isEditMode} isAddCustomToolMode={isAddCustomToolMode} setCurrentGroupIndex={setCurrentGroupIndex} selectedToolGroupForDialog={selectedToolGroupForDialog ?? undefined} /> <ToolsCatalogSection providers={providers as TargetServerNew[]} totalFilteredTools={totalFilteredTools} selectedToolGroup={selectedToolGroup} toolGroups={toolGroups} expandedProviders={expandedProviders} isEditMode={isEditMode} isAddCustomToolMode={isAddCustomToolMode} selectedTools={selectedTools} searchQuery={searchQuery} onProviderClick={handleProviderClick} onToolSelectionChange={handleToolSelectionChange} onSelectAllTools={handleSelectAllTools} onEditClick={(tool) => handleEditCustomTool(tool)} onDuplicateClick={(tool) => handleDuplicateCustomTool(tool)} onDeleteTool={handleDeleteToolWrapper} onCustomizeTool={(tool) => handleToolAction(tool)} onToolClick={handleToolClick} onAddServerClick={() => setIsAddServerModalOpen(true)} onShowAllTools={() => setSelectedToolGroup(null)} onAddCustomToolClick={() => { console.log("[NewToolCatalog] Opening custom tool dialog"); setIsCustomToolFullDialogOpen(true); }} recentlyCustomizedTools={recentlyCustomizedTools} currentlyCustomizingTools={currentlyCustomizingTools} onEditModeToggle={() => { if (isEditMode) { handleCancelGroupEdit(); } else { setIsEditMode(true); } }} selectedToolForDetails={selectedToolForDetails ?? undefined} /> <SelectionPanel selectedTools={selectedTools} isAddCustomToolMode={isAddCustomToolMode} editingGroup={editingGroup} originalSelectedTools={originalSelectedTools} isSavingGroupChanges={isSavingGroupChanges} areSetsEqual={areSetsEqual} showCreateModal={showCreateModal} onSaveGroupChanges={handleSaveGroupChanges} onClearSelection={() => { setSelectedTools(new Set()); setSelectedCustomToolKey(null); }} onCreateToolGroup={() => { handleCreateToolGroup(); }} onCustomizeSelectedTool={() => { if (!selectedCustomToolKey) return; const [providerName, toolName] = selectedCustomToolKey.split(":"); const provider = providers.find((p) => p.name === providerName); const tool = provider?.originalTools.find( (t) => t.name === toolName, ); if (!tool) return; // Exit add custom tool mode before opening the dialog handleCancelAddCustomToolMode(); // Dismiss the add custom tool toast if (toastRef2.current) { toastRef2.current.dismiss?.(); toastRef2.current = null; } handleCustomizeToolDialog({ name: tool.name, serviceName: providerName, inputSchema: tool.inputSchema, description: tool.description, }); }} /> </div> </div> <CreateToolGroupModal isOpen={showCreateModal} onClose={handleCloseCreateModal} newGroupName={newGroupName} onGroupNameChange={handleNewGroupNameChange} newGroupDescription={newGroupDescription} onGroupDescriptionChange={handleNewGroupDescriptionChange} error={createGroupError} onSave={handleSaveToolGroup} isCreating={isCreating} selectedToolsCount={selectedTools.size} /> <EditToolGroupModal isOpen={showEditGroupModal} onClose={handleCloseEditGroupModal} groupName={editingGroupName} onGroupNameChange={handleEditGroupNameChange} groupDescription={editingGroupDescription} onGroupDescriptionChange={handleEditGroupDescriptionChange} error={editGroupError} onSave={handleSaveGroupNameChanges} isSaving={isSavingGroupName} /> {/* Tool Group Side Sheet */} <ToolGroupSheet isOpen={isToolGroupDialogOpen} onOpenChange={(open) => { setIsToolGroupDialogOpen(open); if (!open) { setSelectedToolGroupForDialog(null); } }} selectedToolGroup={selectedToolGroupForDialog} toolGroups={toolGroups} providers={providers} onEditGroup={handleEditGroup} onEditToolGroup={handleOpenEditGroupModal} onDeleteGroup={handleDeleteGroup} /> {/* Add Server Modal */} {isAddServerModalOpen && ( <AddServerModal onClose={() => setIsAddServerModalOpen(false)} /> )} {/* Tool Details Dialog */} {selectedToolForDetails && ( <ToolDetailsDialog isOpen={isToolDetailsDialogOpen} onClose={() => { // Dismiss delete toast when closing details drawer for custom tools if (selectedToolForDetails?.isCustom) { dismissDeleteToast?.(); } setIsToolDetailsDialogOpen(false); setSelectedToolForDetails(null); }} tool={selectedToolForDetails} providers={providers} onEdit={() => { setIsToolDetailsDialogOpen(false); // Dismiss delete toast when editing from tool details dialog dismissDeleteToast?.(); handleEditCustomTool(selectedToolForDetails); }} onDuplicate={() => { setIsToolDetailsDialogOpen(false); // Dismiss delete toast when duplicating from tool details dialog dismissDeleteToast?.(); handleDuplicateCustomTool(selectedToolForDetails); }} onDelete={() => { setIsToolDetailsDialogOpen(false); if (selectedToolForDetails) { handleDeleteToolWrapper(selectedToolForDetails); } }} onCustomize={() => { setIsToolDetailsDialogOpen(false); handleCustomizeToolDialog(selectedToolForDetails); }} /> )} {/* Custom Tool Dialogs - Moved to end for proper positioning */} <CustomToolDialog isOpen={isCustomToolFullDialogOpen} onOpenChange={handleCloseCustomToolFullDialog} providers={providers} onClose={handleCloseCustomToolFullDialog} onCreate={handleCreateCustomToolWithLoading} editDialogMode={editDialogMode} preSelectedServer={editingToolData?.server} preSelectedTool={editingToolData?.tool} preFilledData={ editingToolData ? { name: editingToolData.name, description: editingToolData.description, parameters: editingToolData.parameters, } : undefined } isLoading={isSavingCustomTool} /> <CustomToolDialog isOpen={isEditCustomToolDialogOpen} onOpenChange={handleCloseEditCustomToolDialog} providers={providers} onClose={handleCloseEditCustomToolDialog} onCreate={handleSaveCustomToolWithLoading} editDialogMode={editDialogMode} preSelectedServer={editingToolData?.server} preSelectedTool={editingToolData?.tool} preFilledData={ editingToolData ? { name: editingToolData.name, description: editingToolData.description, parameters: editingToolData.parameters, } : undefined } isLoading={isSavingCustomTool} /> </> ); } const styles = { // Container styles container: " w-full relative", content: "container mx-auto py-8 px-4", // Header styles header: "flex justify-between items-start gap-12 whitespace-nowrap mb-0", title: "text-3xl font-bold tracking-tight", titleSection: "flex flex-col gap-2", filterInfo: "flex flex-wrap items-center gap-2 text-sm mb-2", filterBadge: "bg-[#4F33CC1A] text-[#4F33CC] px-2 py-1 rounded-full font-medium", searchTerm: "bg-gray-200 text-gray-700 px-2 py-1 rounded", customToolsFilter: "bg-[#4F33CC1A] text-[#4F33CC] px-2 py-1 rounded-full font-medium", editModeButton: " bg-[#5147E4] text-white px-4 py-2 rounded-lg font-medium transition-colors text-sm", editModeButtonActive: "bg-[#4F33CC] text-white hover:bg-[#4F33CC]", editModeButtonInactive: "bg-gray-200 text-gray-800 hover:bg-gray-300", // Empty state styles emptyState: "text-center py-12", emptyStateTitle: "text-gray-500 text-lg", emptyStateSubtitle: "text-gray-400 text-sm mt-2", // Accordion styles accordion: "space-y-4", accordionItem: "border-b border-gray-200", accordionTrigger: "hover:no-underline", accordionHeader: "flex items-center justify-between gap-3 flex-1", providerInfo: "flex items-center gap-3 flex-1", providerIcon: "text-xl", providerName: "font-semibold text-gray-800", // Status badge styles statusBadgeConnected: "bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium ml-8 mr-2", statusBadgePending: "bg-yellow-100 text-yellow-800 text-xs px-2 py-1 rounded-full font-medium ml-12 mr-2", statusBadgeFailed: "bg-red-100 text-red-800 text-xs px-2 py-1 rounded-full font-medium ml-8 mr-2", statusBadgeUnauthorized: "bg-gray-100 text-gray-600 text-xs px-2 py-1 rounded-full font-medium flex items-center gap-1 ml-8 mr-2", statusBadgeIcon: "w-3 h-3", // Tools container styles toolsContainer: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pb-2", toolWrapper: "w-full", scrollIndicator: "hidden", scrollIcon: "w-5 h-5 text-gray-400", noToolsMessage: "col-span-full text-center py-8 text-gray-500 text-sm", // Selection panel styles selectionPanel: "fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-white border border-gray-200 rounded-lg shadow-lg p-4 z-50", selectionPanelContent: "flex items-center gap-6", selectionInfo: "flex items-center", toolCounter: "flex items-center gap-2", toolCounterIcon: "bg-[#4F33CC] text-white w-6 h-6 rounded-full flex items-center justify-center text-sm font-medium", toolCounterText: "text-sm text-gray-700 font-medium", selectionActions: "flex items-center gap-2", createButton: "bg-[#4F33CC] text-white px-4 py-2 rounded-lg font-medium transition-colors text-sm hover:bg-[#4F33CC]", removeButton: "border-gray-300 text-gray-700 px-4 py-2 rounded-lg font-medium transition-colors text-sm hover:bg-gray-50", // Modal and form styles modalContent: "max-w-md", modalSpace: "space-y-4 py-4", modalLabel: "text-sm font-medium", modalInput: "w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500", modalCharacterCount: "text-xs text-gray-500", modalFooter: "flex justify-end gap-2", modalCancelButton: "px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors", modalCreateButton: "px-4 py-2 bg-[#4F33CC] text-white rounded-md text-sm font-medium hover:bg-[#4F33CC] transition-colors disabled:opacity-50 disabled:cursor-not-allowed", };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server