Skip to main content
Glama
ToolsDetails.tsx11.9 kB
import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { McpServer, McpServerTool } from "@/types"; import { formatDateTime, isActive } from "@/utils"; import { CircleX, Clock, Server, Wrench } from "lucide-react"; import { useMemo, useRef, useState } from "react"; import { Badge } from "../ui/badge"; import { Combobox } from "../ui/combobox"; import { DashboardScrollArea } from "./DashboardScrollArea"; import { Checkbox } from "../ui/checkbox"; import { Label } from "../ui/label"; export type ToolsDetailsProps = { servers: McpServer[]; }; export const ToolsDetails = ({ servers }: ToolsDetailsProps) => { const [search, setSearch] = useState(""); const [serversFilter, setServersFilter] = useState<string[]>([]); const [onlyUsed, setOnlyUsed] = useState(false); const filteredServers = useMemo(() => { if (!serversFilter.length) return servers; return servers.filter((server) => serversFilter.some((s) => server.name.toLowerCase().includes(s.toLowerCase()), ), ); }, [servers, serversFilter]); const totalTools = useMemo( () => servers.reduce((acc, server) => acc + server.tools.length, 0), [servers], ); const filteredTools: McpServerTool[] = useMemo( () => filteredServers .flatMap((server) => { return server.tools.map((tool) => ({ name: tool.name, description: tool.description || "", invocations: tool.invocations, lastCalledAt: tool.lastCalledAt, })); }) .filter((tool) => tool.name.toLowerCase().includes(search.toLowerCase()), ) .filter((tool) => { if (!onlyUsed) return true; return tool.invocations > 0; }), [filteredServers, onlyUsed, search], ); const inputRef = useRef<HTMLInputElement>(null); const clearFilters = () => { setSearch(""); setServersFilter([]); setOnlyUsed(false); }; if (!servers?.length) { return ( <DashboardScrollArea> <div className="h-full flex items-center justify-center p-4"> <div className="text-center text-[var(--color-text-secondary)]"> <Server className="w-8 h-8 mx-auto mb-2 opacity-50" /> <p className="text-sm"> No servers available. Please add a server to start using tools. </p> </div> </div> </DashboardScrollArea> ); } if (!totalTools) { return ( <div className="text-center p-20 text-[var(--color-text-secondary)]"> <Wrench className="w-4 h-4 mx-auto mb-0.5 opacity-50" /> No tools configured </div> ); } return ( <DashboardScrollArea> <div className="flex flex-col h-full relative px-3 gap-3"> <div className="sticky top-0 z-10 flex-shrink-0 bg-[var(--color-bg-container)] py-2"> <div className="flex items-start justify-between gap-3"> <Card className="bg-background"> <CardContent className="pt-6 grid gap-4 grid-cols-[120px_1px_120px]"> <div className="flex-col items-start justify-center"> <div className="text-sm font-medium text-[var(--color-text-primary)]"> Tools Available </div> <div className="text-2xl font-bold text-[var(--color-text-primary)]"> {totalTools} </div> </div> <Separator orientation="vertical" className="bg-border h-auto" /> <div className="flex-col items-start justify-center"> <div className="text-sm font-medium text-[var(--color-text-primary)]"> Servers Available </div> <div className="text-2xl font-bold text-[var(--color-text-primary)]"> {servers.length} </div> </div> </CardContent> </Card> <div className="grid grid-cols-[1fr] gap-1.5"> <div className="flex items-center focus-within:border-[var(--color-border-secondary)] focus-within:border-solid self-start"> <Input className="bg-background shadow-none rounded-md border-[1px] border-[var(--color-border-interactive)] focus-visible:ring-0 placeholder:text-[var(--color-text-secondary)] font-normal text-sm h-7.5 w-[180px]" placeholder="Search tools..." value={search} onChange={(e) => setSearch(e.target.value)} ref={inputRef} /> <Tooltip> <TooltipTrigger asChild> <Button onClick={() => { setSearch(""); inputRef.current?.focus(); }} variant="vanilla" className="background-transparent focus-visible:ring-0 hover:text-[var(--color-fg-interactive)] focus:text-[var(--color-fg-interactive)] focus-visible:bg-[var(--color-bg-container-overlay)] h-7 w-4 rounded-none" > <CircleX /> </Button> </TooltipTrigger> <TooltipContent align="center" className="shadow bg-[var(--color-bg-container)] text-[var(--color-fg-info)] text-xs" > Clear search </TooltipContent> </Tooltip> </div> <div className="flex items-center focus-within:border-[var(--color-border-secondary)] focus-within:border-solid self-start rounded"> <Combobox autocompleteNoResultsText="No servers found" autocompletePlaceholder="Search servers" buttonLabel={ serversFilter.length ? serversFilter.length > 1 ? `${serversFilter.length} selected` : serversFilter[0] : "Select servers" } buttonProps={{ className: `h-[30px] w-[180px] px-3 hover:text-[var(--color-text-primary)] focus:text-[var(--color-text-primary)] focus-visible:bg-[var(--color-bg-container-overlay)] border-[var(--color-border-interactive)] ${ serversFilter.length ? " text-[var(--color-text-primary)]" : " text-[var(--color-text-secondary)]" }`, }} onChange={(values: string[]) => setServersFilter(values)} options={servers.map((server) => ({ value: server.name, label: server.name, }))} values={serversFilter} multiple /> <Tooltip> <TooltipTrigger asChild> <Button onClick={() => { setServersFilter([]); }} variant="vanilla" className="focus-visible:ring-0 hover:text-[var(--color-fg-interactive)] focus:text-[var(--color-fg-interactive)] focus-visible:bg-[var(--color-bg-container-overlay)] h-7 w-4 rounded-none" > <CircleX /> </Button> </TooltipTrigger> <TooltipContent align="center" className="shadow bg-[var(--color-bg-container)] text-[var(--color-fg-info)] text-xs" > Clear servers </TooltipContent> </Tooltip> </div> <Label className="bg-background hover:bg-accent/50 flex items-center justify-between gap-3 px-3 py-1 rounded-md cursor-pointer border border-[var(--color-border-interactive)] text-[var(--color-text-secondary)] has-[[aria-checked=true]]:text-[var(--color-text-primary)] hover:text-[var(--color-text-primary)] w-[180px]"> <span className="text-sm">Only used tools</span> <Checkbox checked={onlyUsed} onCheckedChange={(checked) => setOnlyUsed(checked === true)} /> </Label> </div> </div> </div> {(serversFilter.length || search || onlyUsed) && filteredTools.length === 0 && ( <div className="h-full flex items-center justify-center p-4"> <div className="text-center text-[var(--color-text-secondary)]"> <Server className="w-8 h-8 mx-auto mb-2 opacity-50" /> <p>No results found</p> <Button variant="secondary" size="sm" className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:hover:bg-background disabled:hover:text-[var(--color-fg-interactive)] disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border bg-background shadow-sm hover:text-accent-foreground text-[9px] px-1 py-0.5 border-[var(--color-border-interactive)] text-[var(--color-fg-interactive)] hover:bg-[var(--color-bg-interactive-hover)] mt-4 hover:bg-[var(--color-bg-container-overlay)] text-[var(--color-text-secondary)] text-sm px-2 py-1" onClick={clearFilters} > <CircleX className="w-3 h-3 text-[var(--color-fg-interactive)] cursor-pointer" /> Clear Filters </Button> </div> </div> )} {filteredTools.map((tool, index) => { const isToolActive: boolean = isActive(tool.lastCalledAt); return ( <div key={index} className={`flex items-start justify-between p-3 rounded-md shadow border mb-1 ${ !isToolActive && tool.invocations > 0 ? "bg-[var(--color-bg-warning)] border-[var(--color-border-warning)]" : "bg-[var(--color-bg-container-overlay)] border-[var(--color-border-info)]" }`} > <div className="flex-1"> <div className="flex items-center gap-1 mb-0.5"> <h5 className="font-medium text-lg text-[var(--color-text-primary)]"> {tool.name} </h5> {!isToolActive && tool.invocations > 0 && ( <Clock className="w-2.5 h-2.5 text-[var(--color-fg-warning)]" /> )} </div> <p className="text-sm text-[var(--color-text-secondary)] leading-tight mb-0.5 whitespace-pre-wrap max-w-[100%]"> {tool.description} </p> <p className="text-xs text-[var(--color-text-primary)]"> Last called: {formatDateTime(tool.lastCalledAt)} </p> </div> <Badge variant="outline" className="text-sm px-1 py-0 ml-1 bg-[var(--color-bg-container)] border-[var(--color-border-primary)] text-[var(--color-text-secondary)] whitespace-nowrap" > {tool.invocations} calls </Badge> </div> ); })} </div> </DashboardScrollArea> ); };

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