Skip to main content
Glama

Sentry MCP

Official
by getsentry
chat.tsx19.1 kB
"use client"; import { useChat } from "@ai-sdk/react"; import { useEffect, useRef, useCallback, useState } from "react"; import { AuthForm, ChatUI } from "."; import { useAuth } from "../../contexts/auth-context"; import { Loader2 } from "lucide-react"; import type { ChatProps } from "./types"; import { usePersistedChat } from "../../hooks/use-persisted-chat"; import TOOL_DEFINITIONS from "@sentry/mcp-server/toolDefinitions"; import { useMcpMetadata } from "../../hooks/use-mcp-metadata"; import { useStreamingSimulation } from "../../hooks/use-streaming-simulation"; import { SlidingPanel } from "../ui/sliding-panel"; import { PromptDialog } from "../ui/prompt-dialog"; import { isAuthError } from "../../utils/chat-error-handler"; // We don't need user info since we're using MCP tokens // The MCP server handles all Sentry authentication internally export function Chat({ isOpen, onClose, onLogout }: ChatProps) { const { isLoading, isAuthenticated, authError, handleOAuthLogin } = useAuth(); // Use persisted chat to save/load messages from localStorage const { initialMessages, saveMessages, clearPersistedMessages } = usePersistedChat(isAuthenticated); // Fetch MCP metadata immediately when authenticated const { metadata: mcpMetadata, isLoading: isMetadataLoading, error: metadataError, } = useMcpMetadata(isAuthenticated); // Initialize streaming simulation first (without scroll callback) const { isStreaming: isLocalStreaming, startStreaming, isMessageStreaming, } = useStreamingSimulation(); const { messages, input, handleInputChange, handleSubmit, status, stop, error, reload, setMessages, setInput, append, } = useChat({ api: "/api/chat", // No auth header needed - server reads from cookie // No ID to disable useChat's built-in persistence // We handle persistence manually via usePersistedChat hook initialMessages, // Enable sending the data field with messages for custom message types sendExtraMessageFields: true, }); // No need for custom scroll handling - react-scroll-to-bottom handles it // Clear messages function - used locally for /clear command and logout const clearMessages = useCallback(() => { setMessages([]); clearPersistedMessages(); }, [setMessages, clearPersistedMessages]); // Prompt dialog state const [selectedPrompt, setSelectedPrompt] = useState<any>(null); const [isPromptDialogOpen, setIsPromptDialogOpen] = useState(false); // Get MCP metadata from the dedicated endpoint const getMcpMetadata = useCallback(() => { return mcpMetadata; }, [mcpMetadata]); // Generate metadata-based messages for custom commands const createPromptsMessage = useCallback(() => { const metadata = getMcpMetadata(); // Determine content based on loading/error state let content: string; let messageMetadata: any; if (isMetadataLoading) { content = "🔄 Loading prompts from MCP server..."; messageMetadata = { type: "prompts-loading" }; } else if (metadataError) { content = `❌ Failed to load prompts: ${metadataError}\n\nPlease check your connection and try again.`; messageMetadata = { type: "prompts-error", error: metadataError }; } else if ( !metadata || !metadata.prompts || !Array.isArray(metadata.prompts) ) { content = "No prompts are currently available. The MCP server may not have loaded prompt definitions yet.\n\nPlease check your connection and try again."; messageMetadata = { type: "prompts-empty" }; } else { content = "These prompts provide guided workflows for complex Sentry tasks. Click any prompt below to fill in parameters and execute it."; messageMetadata = { type: "prompts-list", prompts: metadata.prompts, }; } return { content, data: messageMetadata, }; }, [getMcpMetadata, isMetadataLoading, metadataError]); // Generate tools-based messages for custom commands const createToolsMessage = useCallback(() => { const metadata = getMcpMetadata(); let content: string; let messageMetadata: Record<string, unknown>; if (isMetadataLoading) { content = "🔄 Loading tools from MCP server..."; messageMetadata = { type: "tools-loading" }; } else if (metadataError) { content = `❌ Failed to load tools: ${metadataError}\n\nPlease check your connection and try again.`; messageMetadata = { type: "tools-error", error: metadataError }; } else if (!metadata || !metadata.tools || !Array.isArray(metadata.tools)) { content = "No tools are currently available. The MCP server may not have loaded tools yet.\n\nPlease check your connection and try again."; messageMetadata = { type: "tools-empty" }; } else { // Build detailed tool list for UI component rendering const definitionsByName = new Map( TOOL_DEFINITIONS.map((t) => [t.name, t]), ); const detailed = metadata.tools .slice() .sort((a, b) => a.localeCompare(b)) .map((name) => { const def = definitionsByName.get(name); return { name, description: def ? def.description.split("\n")[0] : "", } as { name: string; description: string }; }); content = "These tools are available right now. Ask the assistant to use one.\n\nNote: This list reflects the permissions you approved during sign‑in. Granting additional scopes will enable more tools."; messageMetadata = { type: "tools-list", tools: metadata.tools, toolsDetailed: detailed, }; } return { content, data: messageMetadata, }; }, [getMcpMetadata, isMetadataLoading, metadataError]); // Generate resources-based messages for custom commands const createResourcesMessage = useCallback(() => { const metadata = getMcpMetadata(); let content: string; let messageMetadata: Record<string, unknown>; if (isMetadataLoading) { content = "🔄 Loading resources from MCP server..."; messageMetadata = { type: "resources-loading" }; } else if (metadataError) { content = `❌ Failed to load resources: ${metadataError}\n\nPlease check your connection and try again.`; messageMetadata = { type: "resources-error", error: metadataError }; } else if ( !metadata || !metadata.resources || !Array.isArray(metadata.resources) ) { content = "No resources are currently available. The MCP server may not have loaded resources yet.\n\nPlease check your connection and try again."; messageMetadata = { type: "resources-empty" }; } else { const resourcesList = metadata.resources .slice() .sort((a, b) => a.name.localeCompare(b.name)) .map((r) => `- ${r.name}: ${r.description}`) .join("\n"); content = `Available MCP Resources:\n\n${resourcesList}`; messageMetadata = { type: "resources-list", resources: metadata.resources, }; } return { content, data: messageMetadata, }; }, [getMcpMetadata, isMetadataLoading, metadataError]); const createHelpMessage = useCallback(() => { const content = `Welcome to the Sentry Model Context Protocol chat interface! This AI assistant helps you test and explore Sentry functionality. ## Available Slash Commands - **\`/help\`** - Show this help message - **\`/tools\`** - List all available MCP tools - **\`/resources\`** - List all available MCP resources - **\`/prompts\`** - List all available MCP prompts with detailed information - **\`/clear\`** - Clear all chat messages - **\`/logout\`** - Log out of the current session ## What I Can Help With 🔍 **Explore Your Sentry Data** - Browse organizations, projects, and teams - Find recent issues and errors - Analyze performance data and releases 🛠️ **Test MCP Tools** - Demonstrate how MCP tools work with your data - Search for specific errors in files - Get detailed issue information 🤖 **Try Sentry's AI Features** - Use Seer for automatic issue analysis and fixes - Get AI-powered debugging suggestions - Generate fix recommendations ## Getting Started Try asking me things like: - "What organizations do I have access to?" - "Show me my recent issues" - "Help me find errors in my React components" - "Use Seer to analyze issue ABC-123" Or use \`/prompts\` to see available guided workflows for complex tasks. **Need more help?** Visit [Sentry Documentation](https://docs.sentry.io/) or check out our [careers page](https://sentry.io/careers/) if you're interested in working on projects like this! 🐱`; return { content, data: { type: "help-message", hasSlashCommands: true, }, }; }, []); // Handle prompt selection const handlePromptSelect = useCallback((prompt: any) => { setSelectedPrompt(prompt); setIsPromptDialogOpen(true); }, []); // Handle prompt execution const handlePromptExecute = useCallback( (promptName: string, parameters: Record<string, string>) => { // Create a formatted prompt message that includes the prompt name and parameters // This will be displayed to the user let displayContent = `Execute prompt: ${promptName}`; if (Object.keys(parameters).length > 0) { const paramList = Object.entries(parameters) .map(([key, value]) => `${key}: ${value}`) .join(", "); displayContent += ` with ${paramList}`; } // Send a message with custom data indicating this is a prompt execution // The server will recognize this and execute the prompt handler append({ role: "user", content: displayContent, data: { type: "prompt-execution", promptName, parameters, }, } as any); }, [append], ); // Close prompt dialog const handlePromptDialogClose = useCallback(() => { setIsPromptDialogOpen(false); setSelectedPrompt(null); }, []); // Track previous auth state to detect logout events const prevAuthStateRef = useRef(isAuthenticated); // Clear messages when user logs out (auth state changes from authenticated to not) useEffect(() => { const wasAuthenticated = prevAuthStateRef.current; // Detect logout: was authenticated but now isn't if (wasAuthenticated && !isAuthenticated) { clearMessages(); } // Update the ref for next comparison prevAuthStateRef.current = isAuthenticated; }, [isAuthenticated, clearMessages]); // Save messages when they change useEffect(() => { saveMessages(messages); }, [messages, saveMessages]); // Track if we had an auth error before const hadAuthErrorRef = useRef(false); const wasAuthenticatedRef = useRef(isAuthenticated); // Handle auth error detection and retry after reauthentication useEffect(() => { // If we get an auth error, record it if (error && isAuthError(error) && !hadAuthErrorRef.current) { hadAuthErrorRef.current = true; } // If we had an auth error and just re-authenticated, retry once if ( hadAuthErrorRef.current && !wasAuthenticatedRef.current && isAuthenticated ) { hadAuthErrorRef.current = false; // Retry the failed message reload(); } // Reset retry state on successful completion (no error) if (!error) { hadAuthErrorRef.current = false; } // Update auth state ref wasAuthenticatedRef.current = isAuthenticated; }, [isAuthenticated, error, reload]); // Handle slash commands const handleSlashCommand = useCallback( (command: string) => { // Always clear the input first for all commands setInput(""); // Add the slash command as a user message first const userMessage = { id: Date.now().toString(), role: "user" as const, content: `/${command}`, createdAt: new Date(), }; if (command === "clear") { // Clear everything clearMessages(); } else if (command === "logout") { // Add message, then logout setMessages((prev: any[]) => [...prev, userMessage]); onLogout(); } else if (command === "help") { // Add user message first setMessages((prev: any[]) => [...prev, userMessage]); // Create help message with metadata and add after a brief delay for better UX setTimeout(() => { const helpMessageData = createHelpMessage(); const helpMessage = { id: (Date.now() + 1).toString(), role: "system" as const, content: helpMessageData.content, createdAt: new Date(), data: { ...helpMessageData.data, simulateStreaming: true }, }; setMessages((prev) => [...prev, helpMessage]); // Start streaming simulation startStreaming(helpMessage.id, 1200); }, 100); } else if (command === "prompts") { // Add user message first setMessages((prev: any[]) => [...prev, userMessage]); // Create prompts message with metadata and add after a brief delay for better UX setTimeout(() => { const promptsMessageData = createPromptsMessage(); const promptsMessage = { id: (Date.now() + 1).toString(), role: "system" as const, content: promptsMessageData.content, createdAt: new Date(), data: { ...promptsMessageData.data, simulateStreaming: true }, }; setMessages((prev) => [...prev, promptsMessage]); // Start streaming simulation startStreaming(promptsMessage.id, 800); }, 100); } else if (command === "tools") { // Add user message first setMessages((prev: any[]) => [...prev, userMessage]); // Create tools message setTimeout(() => { const toolsMessageData = createToolsMessage(); const toolsMessage = { id: (Date.now() + 1).toString(), role: "system" as const, content: toolsMessageData.content, createdAt: new Date(), data: { ...toolsMessageData.data, simulateStreaming: true }, }; setMessages((prev) => [...prev, toolsMessage]); startStreaming(toolsMessage.id, 600); }, 100); } else if (command === "resources") { // Add user message first setMessages((prev: any[]) => [...prev, userMessage]); // Create resources message setTimeout(() => { const resourcesMessageData = createResourcesMessage(); const resourcesMessage = { id: (Date.now() + 1).toString(), role: "system" as const, content: resourcesMessageData.content, createdAt: new Date(), data: { ...resourcesMessageData.data, simulateStreaming: true }, }; setMessages((prev) => [...prev, resourcesMessage]); startStreaming(resourcesMessage.id, 600); }, 100); } else { // Handle unknown slash commands - add user message and error const errorMessage = { id: (Date.now() + 1).toString(), role: "system" as const, content: `Unknown command: /${command}. Available commands: /help, /tools, /resources, /prompts, /clear, /logout`, createdAt: new Date(), }; setMessages((prev) => [...prev, userMessage, errorMessage]); } }, [ clearMessages, onLogout, setInput, setMessages, createHelpMessage, createPromptsMessage, createToolsMessage, createResourcesMessage, startStreaming, ], ); // Handle sending a prompt programmatically const handleSendPrompt = useCallback( (prompt: string) => { // Check if prompt is a slash command if (prompt.startsWith("/")) { const command = prompt.slice(1).toLowerCase().trim(); handleSlashCommand(command); return; } // Clear the input and directly send the message using append append({ role: "user", content: prompt }); }, [append, handleSlashCommand], ); // Wrap form submission to ensure scrolling const handleFormSubmit = useCallback( (e: React.FormEvent<HTMLFormElement>) => { handleSubmit(e); }, [handleSubmit], ); // Show loading state while checking auth session if (isLoading) { return ( <SlidingPanel isOpen={isOpen} onClose={onClose}> <div className="h-full flex items-center justify-center"> <div className="animate-pulse text-slate-400"> <Loader2 className="h-8 w-8 animate-spin" /> </div> </div> </SlidingPanel> ); } // Use a single SlidingPanel and transition between auth and chat states return ( <SlidingPanel isOpen={isOpen} onClose={onClose}> {/* Auth form with fade transition */} <div className={`absolute inset-0 h-full flex flex-col items-center justify-center transition-all duration-500 ease-in-out ${ !isAuthenticated ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none" }`} style={{ visibility: !isAuthenticated ? "visible" : "hidden", transitionProperty: "opacity, transform", transform: !isAuthenticated ? "scale(1)" : "scale(0.95)", }} > <AuthForm authError={authError} onOAuthLogin={handleOAuthLogin} /> </div> {/* Chat UI with fade transition */} <div className={`absolute inset-0 transition-all duration-500 ease-in-out ${ isAuthenticated ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none" }`} style={{ visibility: isAuthenticated ? "visible" : "hidden", transitionProperty: "opacity, transform", transform: isAuthenticated ? "scale(1)" : "scale(1.05)", }} > <ChatUI messages={messages} input={input} error={error} isChatLoading={status === "streaming" || status === "submitted"} isLocalStreaming={isLocalStreaming} isMessageStreaming={isMessageStreaming} isOpen={isOpen} showControls onInputChange={handleInputChange} onSubmit={handleFormSubmit} onStop={stop} onRetry={reload} onClose={onClose} onLogout={onLogout} onSlashCommand={handleSlashCommand} onSendPrompt={handleSendPrompt} onPromptSelect={handlePromptSelect} /> </div> {/* Prompt Dialog */} <PromptDialog prompt={selectedPrompt} isOpen={isPromptDialogOpen} onClose={handlePromptDialogClose} onExecute={handlePromptExecute} /> </SlidingPanel> ); }

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/getsentry/sentry-mcp'

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