Skip to main content
Glama
ToolGroupSheet.tsx11.1 kB
import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Sheet, SheetContent, SheetDescription, SheetHeader, } from "@/components/ui/sheet"; import { FileEdit, Search, Trash2, Wrench } from "lucide-react"; import { useState } from "react"; import McpIcon from "../dashboard/SystemConnectivity/nodes/Mcpx_Icon.svg?react"; import { useDomainIcon } from "@/hooks/useDomainIcon"; import type { ToolGroup } from "@/store/access-controls"; import type { TargetServerNew } from "@mcpx/shared-model"; export const validateToolGroupName = ( name: string, ): { isValid: boolean; error?: string } => { const trimmedName = name.trim(); if (!trimmedName) { return { isValid: false, error: "Tool Group name is required" }; } const allowed = /^[A-Za-z0-9_\s-]+$/; if (!allowed.test(trimmedName)) { return { isValid: false, error: "Only letters, digits, spaces, dash (-) and underscore (_) are allowed", }; } return { isValid: true }; }; interface ToolGroupSheetProps { isOpen: boolean; onOpenChange: (open: boolean) => void; selectedToolGroup: ToolGroup | null; toolGroups: ToolGroup[]; providers: TargetServerNew[]; onEditGroup?: (group: ToolGroup) => void; onEditToolGroup?: (group: ToolGroup) => void; onDeleteGroup?: (group: ToolGroup) => void; } function DomainIcon({ provider, size = 16, }: { provider: TargetServerNew; size?: number; }) { const iconSrc = useDomainIcon(provider.name); let imageColor = "black"; if (!iconSrc) { imageColor = provider.icon || imageColor; } return iconSrc ? ( <img src={iconSrc} alt="favicon" className="object-contain" style={{ width: size, height: size }} /> ) : ( <McpIcon style={{ color: imageColor, width: size, height: size }} /> ); } export function ToolGroupSheet({ isOpen, onOpenChange, selectedToolGroup, toolGroups, providers, onEditGroup, onEditToolGroup, onDeleteGroup, }: ToolGroupSheetProps) { const [searchQuery, setSearchQuery] = useState(""); return ( <Sheet open={isOpen} onOpenChange={onOpenChange}> <SheetContent side="right" className="w-[600px] !max-w-[600px] bg-white p-0 flex flex-col [&>button]:hidden gap-0 overflow-x-hidden" style={{ overflowX: "hidden" }} > <SheetHeader className="px-6"> <div className="flex items-center justify-between mt-6 gap-2"> <div className="flex-1 text-xl font-semibold text-gray-900" style={{ fontWeight: 600 }} > {toolGroups.find((g) => g.id === selectedToolGroup?.id)?.name || selectedToolGroup?.name || ""} </div> <div className="flex items-center gap-1"> {onEditToolGroup && selectedToolGroup && ( <Button variant="ghost" size="sm" onClick={() => onEditToolGroup(selectedToolGroup)} className="p-2" title="Edit Tool Group" > <FileEdit className="w-4 h-4" /> </Button> )} {onEditGroup && selectedToolGroup && ( <Button variant="ghost" size="sm" onClick={() => onEditGroup(selectedToolGroup)} className="p-2" title="Update Tools" > <Wrench className="w-4 h-4" /> </Button> )} {onDeleteGroup && selectedToolGroup && ( <Button variant="ghost" size="sm" onClick={() => onDeleteGroup(selectedToolGroup)} className="p-2" title="Delete" > <Trash2 className="w-4 h-4" /> </Button> )} </div> </div> <SheetDescription></SheetDescription> </SheetHeader> {/* Description */} {(() => { const actualGroup = toolGroups.find( (g) => g.id === selectedToolGroup?.id, ); const description = actualGroup?.description || selectedToolGroup?.description; if (!description) return null; const truncatedDescription = description.length > 200 ? `${description.substring(0, 200)}...` : description; return ( <div className="px-6 overflow-hidden"> <p className="text-sm break-words" style={{ fontSize: "14px", wordBreak: "break-word", overflowWrap: "break-word", maxWidth: "100%", overflow: "hidden", }} title={description.length > 200 ? description : undefined} > {truncatedDescription} </p> </div> ); })()} {/* Search */} <div className="px-6 py-2"> <div className="relative"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" /> <Input placeholder="Search tools and servers..." className="pl-10" style={{ backgroundColor: "#FBFBFF", border: "1px solid #E2E2E2", color: "#000000", }} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} /> </div> </div> {/* Tools Section */} {/* Content */} <div className="px-6 py-2 space-y-4 overflow-y-auto"> {selectedToolGroup && (() => { const actualToolGroup = toolGroups.find( (group) => group.id === selectedToolGroup.id, ); if (!actualToolGroup) return null; // Get providers that are in this tool group const groupProviders = providers.filter( (provider) => actualToolGroup.services && Object.keys(actualToolGroup.services).includes(provider.name), ); const filteredProviders = groupProviders .map((provider) => { const toolNames = actualToolGroup.services[provider.name] || []; let providerTools = provider.originalTools.filter((tool) => toolNames.includes(tool.name ?? ""), ); // If no tools match the configured names, show all tools for this provider // This handles cases where tool group was configured with incorrect tool names if (providerTools.length === 0 && toolNames.length > 0) { providerTools = provider.originalTools || []; } // Filter tools by search query if (searchQuery) { const searchLower = searchQuery.toLowerCase(); // Check if provider name matches search const providerMatches = provider.name .toLowerCase() .includes(searchLower); // Filter tools by name and description providerTools = providerTools.filter( (tool) => tool.name.toLowerCase().includes(searchLower) || (tool.description && tool.description.toLowerCase().includes(searchLower)), ); // If provider name matches but no tools match, still show the provider if (providerMatches && providerTools.length === 0) { providerTools = provider.originalTools.filter((tool) => toolNames.includes(tool.name), ); } } // Don't render provider if no tools match the search (unless provider name matches) if (providerTools.length === 0) return null; return { provider, tools: providerTools, }; }) .filter((item) => item !== null); // Show "No tools found" message if search query doesn't match anything if (searchQuery && filteredProviders.length === 0) { return ( <div className="text-center py-8"> <div className="text-gray-500 text-sm"> No tools found matching "{searchQuery}" </div> </div> ); } return filteredProviders.map(({ provider, tools }) => ( <div key={provider.name} className="border border-gray-200 rounded-lg p-4 space-y-4 bg-white shadow-sm" > <div className="flex items-center gap-2"> <DomainIcon provider={provider} size={32} /> <div className="flex-1"> <h3 className="capitalize font-semibold text-gray-900 text-lg"> {provider.name} </h3> </div> </div> <div className="space-y-2"> <p className="text-sm " style={{ color: "#231A4D" }}> Tools for interacting with the {provider.name} API... </p> {tools.map((tool, toolIndex) => ( <div key={toolIndex} className="flex items-center justify-between rounded-lg p-4" style={{ backgroundColor: "white", border: "1px solid #E2E2E2", }} > <div className="flex flex-col items-start gap-0.5"> {/* Tool Name */} <p style={{ color: "#231A4D", fontWeight: 600 }}> {tool.name} </p> <p style={{ color: "#231A4D", fontWeight: 400 }}> {tool.description} </p> </div> </div> ))} <div className="text-xs text-gray-500 mt-2"> {tools.length} tool{tools.length !== 1 ? "s" : ""} </div> </div> </div> )); })()} </div> </SheetContent> </Sheet> ); }

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