Skip to main content
Glama
ToolCard.tsx9.69 kB
import { Settings, Square, Trash2 } from "lucide-react"; import CustomBadge from "@/components/CustomBadge"; import { EllipsisActions } from "@/components/ui/ellipsis-action"; import { useCallback, useState } from "react"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; export interface ToolCardTool { name: string; description?: string; inputSchema?: Tool["inputSchema"]; isCustom?: boolean; originalToolName?: string; originalToolId?: string; serviceName?: string; } interface ToolCardProps { tool: ToolCardTool; isEditMode: boolean; isAddCustomToolMode: boolean; isSelected: boolean; selectionLocked?: boolean; onToggleSelection: () => void; onToolClick?: () => void; onCustomizeTool?: (tool: ToolCardTool) => void; onClick?: () => void; onDeleteTool?: (tool: ToolCardTool) => void; isDrawerOpen?: boolean; isLoading?: boolean; triggerLoading?: boolean; isCustomizing?: boolean; // New prop to show loading when customization starts isDeleting?: boolean; // New prop to trigger delete animation providerName?: string; // Provider name for data attributes isInactive?: boolean; // Whether the provider/server is inactive } const styles = { toolCard: "bg-white rounded-lg p-3 border-2 border-gray-200 hover:border-[#4F33CC] hover:shadow-md transition-all duration-200 min-h-[120px] flex flex-col", toolCardSelected: " border-[#4F33CC] hover:border-[#4F33CC]", toolCardHeader: "flex justify-between items-start relative", checkboxButton: "text-gray-500 transition-colors absolute top-0 right-0 z-10", checkboxIcon: "w-4 h-4", purpleCheckbox: "bg-[#4F33CC] text-white w-4 h-4 rounded flex items-center justify-center", toolCardContent: "flex-1 flex flex-col justify-between", toolTitle: "font-medium text-gray-900 text-sm mb-1 truncate min-h-[20px] ", toolDescription: "text-gray-600 text-xs text-overflow-ellipsis leading-relaxed max-w-[100%] h-[40px] ", }; const customStyles = ` .text-overflow-ellipsis { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; } `; export const ToolCard: React.FC<ToolCardProps> = ({ tool, isEditMode, isAddCustomToolMode, isSelected, selectionLocked = false, onToggleSelection, onToolClick, onCustomizeTool, onDeleteTool, isDrawerOpen = false, providerName, isInactive = false, }) => { const [internalLoading, _setInternalLoading] = useState(false); // Simple loading logic: show skeleton for title during customization // useEffect(() => { // if (isCustomizing || triggerLoading) { // setInternalLoading(true); // } else { // // Hide loading after a brief delay // const timer = setTimeout(() => { // setInternalLoading(false); // }, 100); // return () => clearTimeout(timer); // } // }, [isCustomizing, triggerLoading]); const isSelectionMode = isEditMode || isAddCustomToolMode; const isOriginalTool = !tool.isCustom; const isSelectable = isSelectionMode && (isEditMode || (isAddCustomToolMode && isOriginalTool)) && (!selectionLocked || isSelected) && !isInactive; const handleClick = useCallback(() => { if (isSelectable) { onToggleSelection(); } else if (!selectionLocked && onToolClick) { onToolClick(); } }, [isSelectable, selectionLocked, onToggleSelection, onToolClick]); const handleKeyDown = useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (!isSelectable) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onToggleSelection(); } }, [isSelectable, onToggleSelection], ); return ( <> <style>{customStyles}</style> <div className={`${styles.toolCard} ${isSelectionMode && isSelected ? styles.toolCardSelected : ""} ${ isDrawerOpen ? "!border-[#B4108B] !shadow-lg !shadow-[#B4108B]/40" : "" } ${isInactive ? "!bg-gray-100 !text-[#C3C4CD]" : ""}`} data-tool-name={tool.name} data-provider={providerName} title={tool.name} onClick={handleClick} onKeyDown={handleKeyDown} role={isSelectionMode ? "checkbox" : undefined} aria-checked={isSelectionMode ? isSelected : undefined} tabIndex={isSelectable ? 0 : -1} style={{ cursor: isSelectable ? "pointer" : selectionLocked ? "not-allowed" : onToolClick ? "pointer" : "default", backgroundColor: isInactive ? "#F3F4F6" : undefined, opacity: isInactive ? 0.65 : selectionLocked && !isSelected ? 0.6 : 1, }} > <div className={styles.toolCardHeader}> {isSelectionMode && !internalLoading && !isInactive && ( <div className={styles.checkboxButton}> {isSelected ? ( <div className={styles.purpleCheckbox}> <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" > <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> </svg> </div> ) : ( <Square className={styles.checkboxIcon} /> )} </div> )} <div className={styles.toolCardContent}> {/* Normal content state - always show content, just skeleton the title when loading */} <div className="flex justify-between items-start h-full"> <div className=" flex flex-col min-h-0"> <div className=" flex flex-col min-h-0"> <div className="w-[200px]"> {internalLoading ? ( // Skeleton only for title <div className="h-4 bg-gray-200 rounded w-3/4 animate-pulse"></div> ) : ( <h3 className={`${styles.toolTitle} ${isInactive ? "!text-[#C3C4CD]" : ""}`} title={tool.name} > {tool.name || ( <div className="h-4 bg-gray-200 rounded w-3/4 animate-pulse"></div> )} </h3> )} </div> <div className=" min-h-0 overflow-hidden"> <p className={`${styles.toolDescription} h-full ${ isInactive ? "!text-[#C3C4CD]" : "" }`} title={tool.description || "No description available"} > {tool.description || "No description available"} </p> </div> </div> {tool.isCustom && ( <div className="mt-2 flex flex-shrink-0"> <CustomBadge color="blue" size="xs" rounded="lg" label={<span>CUSTOM</span>} icon={ <svg className="w-4 h-4" style={{ color: "#4F33CC" }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" > <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /> <polyline points="3.27,6.96 12,12.01 20.73,6.96" /> <line x1="12" y1="22.08" x2="12" y2="12" /> </svg> } /> </div> )} </div> {/* EllipsisActions for tool customization/edit - hidden in edit or add modes */} {!isEditMode && !isAddCustomToolMode && !internalLoading && ( <div className="ml-2 flex-shrink-0" onClick={(e) => e.stopPropagation()} > <EllipsisActions items={[ ...(onCustomizeTool ? [ { label: tool.isCustom ? "Edit" : "Customize", icon: <Settings className="w-4 h-4" />, callback: () => onCustomizeTool(tool), }, ] : []), ...(tool.isCustom && onDeleteTool ? [ { label: "Delete", icon: <Trash2 className="w-4 h-4" />, callback: () => onDeleteTool(tool), }, ] : []), ]} /> </div> )} </div> </div> </div> </div> </> ); };

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