Skip to main content
Glama
mcp-demo-section.tsx•20.3 kB
"use client" import type React from "react" import { useState, useEffect, useRef, useMemo } from "react" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Loader2, Send, Settings2, TrendingUp, ShoppingCart, CheckCircle, XCircle, Info, AlertTriangle, } from "lucide-react" import { useMCPAgent } from "@/hooks/store/mcp/use-mcp-agent" import { useAgentMcpContext } from "@/contexts/agent-mcp-context" import { cn } from "@/lib/utils" import type { MCPServerConfig, MCPTask } from "@/lib/mcp" import { ConnectingStickerAnimated } from "./connecting-sticker-animated" import { MarkdownText } from "./markdown-text" import { ThinkingBlock } from "./thinking-block" interface MCPDemoSectionProps { className?: string } const staticShoppingSuggestions = [ "Find the best deals on summer dresses", "Compare prices for Nike Air Max sneakers", "Show me popular electronics on sale", "What are the latest fashion trends for men?", "Add a new sofa to my cart", ] function getRandomUniqueSuggestions(allPrompts: string[], count: number): string[] { if (!allPrompts || allPrompts.length === 0) { return [] } const uniquePrompts = Array.from(new Set(allPrompts)) const shuffled = [...uniquePrompts].sort(() => 0.5 - Math.random()) return shuffled.slice(0, Math.min(count, shuffled.length)) } function getTaskDisplayName(task: MCPTask): string { const robotIcon = task.reasoningType === "Thinking" ? "🧠" : "🤖" return `${task.name} ${robotIcon} ${task.model}` } export function MCPDemoSection({ className }: MCPDemoSectionProps) { const [input, setInput] = useState("") const { messages, isLoading, error, sendMessage, clearMessages, pendingApprovalRequest, handleApproval } = useMCPAgent() const { tasks, chatActiveTaskId, setChatActiveTaskId } = useAgentMcpContext() const textareaRef = useRef<HTMLTextAreaElement>(null) const scrollAreaRef = useRef<HTMLDivElement>(null) const activeChatTask = tasks.find((t) => t.id === chatActiveTaskId) const currentSuggestions = useMemo(() => { const aggregatedSuggestions: string[] = [] if (activeChatTask && activeChatTask.servers && activeChatTask.servers.length > 0) { activeChatTask.servers.forEach((server: MCPServerConfig) => { if (server.suggestedPrompts && server.suggestedPrompts.length > 0) { aggregatedSuggestions.push(...server.suggestedPrompts) } }) } if (aggregatedSuggestions.length > 0) { return getRandomUniqueSuggestions(aggregatedSuggestions, 5) } return staticShoppingSuggestions }, [activeChatTask]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!input.trim() || isLoading || pendingApprovalRequest || !activeChatTask) return await sendMessage(input) setInput("") } const handleSuggestionClick = (suggestion: string) => { setInput(suggestion) textareaRef.current?.focus() } const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() handleSubmit(e as unknown as React.FormEvent) } } useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto" textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px` } }, [input]) useEffect(() => { // console.log("Messages updated:", messages) if (scrollAreaRef.current) { const scrollViewport = scrollAreaRef.current.querySelector('div[style*="overflow: scroll;"]') as HTMLElement if (scrollViewport) { scrollViewport.scrollTop = scrollViewport.scrollHeight } else { const mainDiv = scrollAreaRef.current.children[0] as HTMLElement if (mainDiv && mainDiv.children[0]) { const contentWrapper = mainDiv.children[0] as HTMLElement contentWrapper.scrollTop = contentWrapper.scrollHeight } } } }, [messages]) const showSuggestions = messages.length === 0 && !pendingApprovalRequest && activeChatTask && activeChatTask.servers.length > 0 && currentSuggestions.length > 0 const activeServerForApproval = pendingApprovalRequest ? activeChatTask?.servers.find((s) => s.label === pendingApprovalRequest.serverLabel) : null const lightBg = "bg-[#FBF9F7]" const darkBg = "dark:bg-[#2B2623]" const lightText = "text-[#4A403A]" const darkText = "dark:text-[#E8E3E0]" const lightBorder = "border-[#E0D9D3]" const darkBorder = "dark:border-[#423A36]" const lightCardBg = "bg-[#F5F1ED]" const darkCardBg = "dark:bg-[#362F2C]" const lightMutedText = "text-[#8C7F78]" const darkMutedText = "dark:text-[#A39B96]" const lightAccent = "text-[#D8A0A7]" const darkAccent = "dark:text-[#C98A9A]" const lightInputBg = "bg-[#F3EFEC]" const darkInputBg = "dark:bg-[#312B28]" const lightPrimaryButtonBg = "bg-[#D8A0A7]" const darkPrimaryButtonBg = "dark:bg-[#C98A9A]" const lightPrimaryButtonText = "text-[#FFFFFF]" const darkPrimaryButtonText = "dark:text-[#FFFFFF]" const lightSecondaryButtonBg = "bg-[#E0D9D3]" const darkSecondaryButtonBg = "dark:bg-[#423A36]" const lightSecondaryButtonText = "text-[#4A403A]" const darkSecondaryButtonText = "dark:text-[#E8E3E0]" const approvalResultBg = "bg-[#FFFFE0]" const approvalResultText = "text-[#D90429]" const approvalResultBorder = "border-2 border-[#D90429]" return ( <Card className={cn("flex flex-col h-full", lightBg, darkBg, lightText, darkText, lightBorder, darkBorder, className)} > <CardHeader className={cn("flex flex-row items-center justify-between", "border-b", lightBorder, darkBorder)}> <div className="flex items-center space-x-2"> <CardTitle className={cn("font-normal", lightText, darkText)}>MCP Playground</CardTitle> {isLoading && !pendingApprovalRequest && <ConnectingStickerAnimated />} </div> <Button variant="outline" size="sm" onClick={clearMessages} disabled={messages.length === 0 && !pendingApprovalRequest} className={cn( lightSecondaryButtonBg, darkSecondaryButtonBg, lightSecondaryButtonText, darkSecondaryButtonText, lightBorder, darkBorder, "hover:bg-[#F3EFEC] dark:hover:bg-[#312B28]", )} > Clear Chat </Button> </CardHeader> <CardContent className={cn("flex-1 flex flex-col space-y-4 overflow-hidden p-4", { "justify-center": messages.length === 0 && !pendingApprovalRequest, })} > {messages.length === 0 && !pendingApprovalRequest && ( <div className={cn("text-center text-2xl font-normal mb-8", lightMutedText, darkMutedText)}> {tasks.length === 0 ? "No tasks available. Please create one in configuration." : !activeChatTask ? "Select a task from the 'Task' menu to begin." : "What can I help with?"} </div> )} <ScrollArea className="flex-1" ref={scrollAreaRef}> <div className="p-4 space-y-4"> {messages.map((message, index) => { // Skip rendering assistant messages with empty content if (message.role === "assistant" && (!message.content || message.content.trim() === "")) { return null } if (message.role === "thinking") { return <ThinkingBlock key={message.id} message={message} provider={message.provider} className="mb-4" /> } if (message.role === "system") { return ( <div key={message.id} className={cn( "p-3 my-2 max-w-[90%] mx-auto rounded-md shadow-md", approvalResultBg, approvalResultText, approvalResultBorder, )} > <div className="flex items-center text-sm font-semibold mb-1"> <AlertTriangle className={cn("h-5 w-5 mr-2", approvalResultText)} /> MCP Tool Approval Result </div> <div className={cn("whitespace-pre-wrap text-sm font-medium", approvalResultText)}> {message.content} </div> </div> ) } return ( <div key={message.id} className={`p-3 rounded-lg shadow-sm break-words text-sm ${ message.role === "user" ? `bg-[#D8A0A7]/20 dark:bg-[#C98A9A]/20 ${lightText} ${darkText} ml-auto max-w-[80%]` : `${lightCardBg} ${darkCardBg} ${lightText} ${darkText} mr-auto max-w-[80%]` }`} > <div className={cn("font-semibold mb-1 capitalize", lightMutedText, darkMutedText)}> {message.role} </div> <MarkdownText>{message.content}</MarkdownText> </div> ) })} </div> </ScrollArea> {error && ( <Alert variant="destructive" className="mt-auto bg-red-100 dark:bg-red-900/30 border-red-400 dark:border-red-700 text-red-700 dark:text-red-300" > <AlertTitle>Error</AlertTitle> <AlertDescription>{error}</AlertDescription> </Alert> )} <Dialog open={!!pendingApprovalRequest} onOpenChange={(isOpen) => { if (!isOpen && pendingApprovalRequest) handleApproval(false) }} > <DialogContent className={cn("sm:max-w-md", lightCardBg, darkCardBg, lightText, darkText, lightBorder, darkBorder)} > <DialogHeader> <DialogTitle className={cn("flex items-center", lightText, darkText)}> <Info className={cn("mr-2 h-5 w-5", lightAccent, darkAccent)} /> MCP Tool Approval Request </DialogTitle> </DialogHeader> {pendingApprovalRequest && ( <div className="space-y-3 py-4 text-sm"> <p>The AI assistant wants to use the following tool:</p> <div> <div className={cn("text-xs mb-1", lightMutedText, darkMutedText)}>On MCP Server:</div> <p> <span className="font-medium">{pendingApprovalRequest.serverLabel}</span> </p> {activeServerForApproval && ( <p className="text-xs text-gray-500 dark:text-gray-400 break-all"> {activeServerForApproval.url}</p> )} </div> <div className="my-3"> <div className={cn("text-xs mb-1", lightMutedText, darkMutedText)}>Tool to be used:</div> <div className="flex items-center p-3 bg-blue-50 dark:bg-blue-900/30 rounded-md"> <Settings2 className="h-6 w-6 mr-3 text-blue-600 dark:text-blue-400 shrink-0" /> <span className="text-lg font-semibold text-blue-700 dark:text-blue-300 break-words"> {pendingApprovalRequest.toolName} </span> </div> </div> <div> <div className={cn("text-xs mb-1", lightMutedText, darkMutedText)}>With arguments:</div> <ScrollArea className=" h-40 w-full max-h-40 mt-1 rounded bg-gray-100 dark:bg-gray-800 border dark:border-gray-700"> <div className="p-2"> <pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-all"> {JSON.stringify(JSON.parse(pendingApprovalRequest.toolArguments || "{}"), null, 2)} </pre> </div> </ScrollArea> </div> </div> )} <DialogFooter className="gap-2 sm:justify-end"> <Button variant="outline" onClick={() => handleApproval(false)} className={cn( lightSecondaryButtonBg, darkSecondaryButtonBg, lightSecondaryButtonText, darkSecondaryButtonText, lightBorder, darkBorder, "hover:bg-[#F3EFEC] dark:hover:bg-[#312B28]", )} > <XCircle className="mr-2 h-4 w-4" /> Decline </Button> <Button onClick={() => handleApproval(true)} className={cn( lightPrimaryButtonBg, darkPrimaryButtonBg, lightPrimaryButtonText, darkPrimaryButtonText, "hover:bg-[#C98A9A] dark:hover:bg-[#D8A0A7]", )} > <CheckCircle className="mr-2 h-4 w-4" /> Approve </Button> </DialogFooter> </DialogContent> </Dialog> <div className={cn( "mt-auto pt-4 border-t", lightBorder, darkBorder, messages.length === 0 && !pendingApprovalRequest ? "max-w-2xl mx-auto w-full" : "", )} > <form onSubmit={handleSubmit} className="relative"> <div className={cn( "flex items-start gap-2 border rounded-lg p-2 pr-14", lightInputBg, darkInputBg, lightBorder, darkBorder, )} > <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="sm" className={cn( "flex items-center gap-1 px-2 shrink-0 mt-1.5", lightMutedText, darkMutedText, `hover:${lightText} dark:hover:${darkText}`, )} disabled={!!pendingApprovalRequest || tasks.length === 0} > <Settings2 className="h-4 w-4" /> Task </Button> </DropdownMenuTrigger> <DropdownMenuContent align="start" className={cn(lightCardBg, darkCardBg, lightText, darkText, lightBorder, darkBorder)} > {tasks.length === 0 && ( <DropdownMenuItem disabled className={cn(lightMutedText, darkMutedText)}> No tasks available </DropdownMenuItem> )} {tasks.map((task) => ( <DropdownMenuItem key={task.id} onClick={() => setChatActiveTaskId(task.id)} disabled={chatActiveTaskId === task.id} className={cn(`hover:${lightInputBg} dark:hover:${darkInputBg}`)} > {getTaskDisplayName(task)} </DropdownMenuItem> ))} </DropdownMenuContent> </DropdownMenu> {activeChatTask && !pendingApprovalRequest && ( <Badge variant="secondary" className={cn( "flex items-center gap-1 shrink-0 mt-1.5 bg-[#D8A0A7]/30 dark:bg-[#C98A9A]/30 text-[#4A403A] dark:text-[#E8E3E0] border-transparent", )} > <ShoppingCart className="h-3 w-3" /> {activeChatTask.name} </Badge> )} <Textarea ref={textareaRef} value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder={ pendingApprovalRequest ? "Awaiting your approval for the tool call above..." : tasks.length === 0 ? "No tasks available. Configure tasks first." : !activeChatTask ? "Select a task from the 'Task' menu to begin..." : !activeChatTask.servers.length ? `Add MCP servers to "${activeChatTask.name}" in configuration...` : "Type your message..." } className={cn( "flex-1 min-h-[20px] h-auto py-1.5 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 resize-none bg-transparent", `placeholder:${lightMutedText} dark:placeholder:${darkMutedText}`, lightText, darkText, )} rows={1} disabled={ isLoading || !!pendingApprovalRequest || tasks.length === 0 || !activeChatTask || (!activeChatTask.servers.length && !pendingApprovalRequest) } /> </div> <Button type="submit" size="icon" className={cn( "absolute right-2 top-1/2 -translate-y-1/2", lightPrimaryButtonBg, darkPrimaryButtonBg, lightPrimaryButtonText, darkPrimaryButtonText, "hover:bg-[#C98A9A] dark:hover:bg-[#D8A0A7]", )} disabled={ isLoading || !!pendingApprovalRequest || !input.trim() || !activeChatTask || (!activeChatTask.servers.length && !pendingApprovalRequest) } > {isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} </Button> {tasks.length > 0 && !activeChatTask && messages.length === 0 && !pendingApprovalRequest && ( <div className={cn("text-xs mt-2 text-center", lightMutedText, darkMutedText)}> Please select a task from the "Task" menu to begin your chat. </div> )} </form> {showSuggestions && ( <div className={cn("w-full mx-auto pt-4", messages.length === 0 ? "max-w-2xl" : "")}> <div className="grid grid-cols-1 sm:grid-cols-1 gap-2"> {currentSuggestions.map((suggestion, index) => ( <Button key={index} variant="ghost" className={cn( "justify-start text-left h-auto py-2 font-light", lightMutedText, darkMutedText, `hover:${lightInputBg} dark:hover:${darkInputBg}`, `hover:${lightText} dark:hover:${darkText}`, )} onClick={() => handleSuggestionClick(suggestion)} disabled={!!pendingApprovalRequest} > <TrendingUp className="h-4 w-4 mr-2 shrink-0" /> {suggestion} </Button> ))} </div> </div> )} </div> </CardContent> </Card> ) }

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/myeong-ga/research-agent-mcp-0.36-pro-preview-06-01'

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