Skip to main content
Glama

Bybit MCP Server

by sammcj
ChatApp.ts31.7 kB
/** * Main chat application component */ import type { ChatUIMessage, ChatState, ChatMessage } from '@/types/ai'; import { aiClient, generateSystemPrompt } from '@/services/aiClient'; import { multiStepAgent } from '@/services/multiStepAgent'; import { agentConfigService } from '@/services/agentConfig'; import { mcpClient } from '@/services/mcpClient'; import { configService } from '@/services/configService'; import { citationProcessor } from '@/services/citationProcessor'; import { citationStore } from '@/services/citationStore'; import type { WorkflowEvent } from '@/types/workflow'; import { marked } from 'marked'; import { MessageRenderer, type MessageData } from './chat/MessageRenderer'; export class ChatApp { private state: ChatState = { messages: [], isLoading: false, isConnected: false, currentStreamingId: undefined, }; private chatMessages: HTMLElement; private chatInput: HTMLTextAreaElement; private sendBtn: HTMLButtonElement; private connectionStatus: HTMLElement; private typingIndicator: HTMLElement; private fullConversationHistory: ChatMessage[] = []; // Track complete conversation including tool calls private workflowEventsContainer: HTMLElement | null = null; private useAgent: boolean = false; // Toggle between agent and legacy client - temporarily disabled for testing private lastCitationAttachTime: number = 0; private citationAttachThrottle: number = 300; // Throttle to 300ms private messageRenderer: MessageRenderer; private retryCount: number = 0; private maxRetries: number = 2; // Maximum number of automatic retries private processingMessage: HTMLElement | null = null; constructor() { // Configure marked for safe HTML rendering marked.setOptions({ breaks: true, // Convert line breaks to <br> gfm: true, // GitHub Flavored Markdown }); // Get DOM elements this.chatMessages = document.getElementById('chat-messages')!; this.chatInput = document.getElementById('chat-input') as HTMLTextAreaElement; this.sendBtn = document.getElementById('send-btn') as HTMLButtonElement; this.connectionStatus = document.getElementById('connection-status')!; this.typingIndicator = document.getElementById('typing-indicator')!; this.workflowEventsContainer = document.getElementById('workflow-events'); // Initialize the enhanced message renderer this.messageRenderer = new MessageRenderer(this.chatMessages); this.initialize(); } private initialize(): void { this.setupEventListeners(); this.updateConnectionStatus(); this.loadWelcomeMessage(); } private setupEventListeners(): void { // Send button click this.sendBtn.addEventListener('click', () => { this.sendMessage(); }); // Enter key to send (Shift+Enter for new line) this.chatInput.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.sendMessage(); } }); // Auto-resize textarea this.chatInput.addEventListener('input', () => { this.autoResizeTextarea(); this.updateSendButton(); }); // Update send button state on input this.chatInput.addEventListener('input', () => { this.updateSendButton(); }); // Listen for configuration changes configService.subscribe(() => { this.updateConnectionStatus(); }); } private autoResizeTextarea(): void { this.chatInput.style.height = 'auto'; this.chatInput.style.height = Math.min(this.chatInput.scrollHeight, 128) + 'px'; } private updateSendButton(): void { const hasText = this.chatInput.value.trim().length > 0; this.sendBtn.disabled = !hasText || this.state.isLoading; } private async updateConnectionStatus(): Promise<void> { try { const [aiConnected, mcpConnected, agentConnected] = await Promise.all([ aiClient.isConnected(), mcpClient.isConnected(), multiStepAgent.isConnected(), ]); if (this.useAgent) { this.state.isConnected = agentConnected; if (agentConnected) { this.connectionStatus.innerHTML = '🤖 Agent Ready'; this.connectionStatus.className = 'connection-status text-success'; } else { this.connectionStatus.innerHTML = '🔴 Agent Offline'; this.connectionStatus.className = 'connection-status text-danger'; } } else { // Legacy mode this.state.isConnected = aiConnected && mcpConnected; if (this.state.isConnected) { this.connectionStatus.innerHTML = '🟢 Connected'; this.connectionStatus.className = 'connection-status text-success'; } else if (aiConnected && !mcpConnected) { this.connectionStatus.innerHTML = '🟡 AI Only'; this.connectionStatus.className = 'connection-status text-warning'; } else if (!aiConnected && mcpConnected) { this.connectionStatus.innerHTML = '🟡 MCP Only'; this.connectionStatus.className = 'connection-status text-warning'; } else { this.connectionStatus.innerHTML = '🔴 Disconnected'; this.connectionStatus.className = 'connection-status text-danger'; } } } catch (error) { console.error('Failed to check connection status:', error); this.connectionStatus.innerHTML = '🔴 Error'; this.connectionStatus.className = 'connection-status text-danger'; } } private loadWelcomeMessage(): void { // Clear any existing messages this.state.messages = []; // The welcome message is already in the HTML, so we don't need to add it programmatically // Just ensure the chat messages container is ready for new messages } /** * Show processing indicator */ private showProcessingIndicator(isRetry: boolean = false): void { this.hideProcessingIndicator(); // Remove any existing indicator this.processingMessage = document.createElement('div'); this.processingMessage.className = 'processing-message'; this.processingMessage.innerHTML = ` <div class="message-header"> <div class="message-avatar">AI</div> <span class="message-name">Assistant</span> <span class="message-time">${new Date().toLocaleTimeString()}</span> </div> <div class="message-content processing"> <div class="processing-indicator"> <div class="processing-dots"> <span>•</span> <span>•</span> <span>•</span> </div> <span class="processing-text"> ${isRetry ? `🔄 Retrying... (attempt ${this.retryCount + 1}/${this.maxRetries + 1})` : '🤔 Processing your request...'} </span> </div> </div> `; this.chatMessages.appendChild(this.processingMessage); this.scrollToBottom(); } /** * Hide processing indicator */ private hideProcessingIndicator(): void { if (this.processingMessage) { this.processingMessage.remove(); this.processingMessage = null; } } /** * Check if AI response is empty or invalid */ private isEmptyResponse(content: string): boolean { if (!content) return true; const trimmed = content.trim(); if (!trimmed) return true; // Remove the overly strict length check - valid responses can be short // Only check for truly empty or nonsensical patterns const emptyPatterns = [ /^\.+$/, // Only dots /^-+$/, // Only dashes /^\*+$/, // Only asterisks /^_+$/, // Only underscores /^\?+$/, // Only question marks /^!+$/ // Only exclamation marks ]; return emptyPatterns.some(pattern => pattern.test(trimmed)); } public async sendMessage(content?: string): Promise<void> { const messageContent = content || this.chatInput.value.trim(); if (!messageContent || this.state.isLoading) { return; } // Reset retry count for new messages if (!content) { this.retryCount = 0; } // Clear input if using the input field if (!content) { this.chatInput.value = ''; this.autoResizeTextarea(); this.updateSendButton(); } // Create user message (only for new messages, not retries) if (this.retryCount === 0) { const userMessage: ChatUIMessage = { id: this.generateMessageId(), role: 'user', content: messageContent, timestamp: Date.now(), }; this.addMessage(userMessage); } // Set loading state this.state.isLoading = true; this.updateSendButton(); this.showTypingIndicator(); this.showProcessingIndicator(this.retryCount > 0); try { console.log('💬 Starting chat request...'); if (this.useAgent) { // Use multi-step agent with streaming await this.handleAgentChat(messageContent); } else { // Use legacy AI client await this.handleLegacyChat(messageContent); } // Reset retry count on success this.retryCount = 0; } catch (error) { console.error('❌ Failed to send message:', error); console.error('❌ Error details:', { name: error instanceof Error ? error.name : 'Unknown', message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, details: (error as any)?.details }); // Check if we should retry if (this.retryCount < this.maxRetries) { this.retryCount++; console.log(`🔄 Retrying... attempt ${this.retryCount}/${this.maxRetries}`); // Hide current processing indicator and show retry indicator this.hideProcessingIndicator(); // Wait a moment before retrying setTimeout(() => { this.sendMessage(messageContent); }, 1000); return; } // Max retries reached, show error const errorMessage: ChatUIMessage = { id: this.generateMessageId(), role: 'assistant', content: `Sorry, I encountered an error after ${this.maxRetries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`, timestamp: Date.now(), error: error instanceof Error ? error.message : 'Unknown error', }; this.addMessage(errorMessage); this.retryCount = 0; // Reset for next message } finally { this.state.isLoading = false; this.state.currentStreamingId = undefined; this.updateSendButton(); this.hideTypingIndicator(); this.hideProcessingIndicator(); } } /** * Handle chat using multi-step agent with streaming and workflow events */ private async handleAgentChat(messageContent: string): Promise<void> { console.log('🤖 Using multi-step agent...'); // Create assistant message for streaming const assistantMessage: ChatUIMessage = { id: this.generateMessageId(), role: 'assistant', content: '', timestamp: Date.now(), isStreaming: true, }; this.addMessage(assistantMessage); // Get the message element for updating const messageElement = this.chatMessages.querySelector(`[data-message-id="${assistantMessage.id}"]`); const contentElement = messageElement?.querySelector('[data-content]') as HTMLElement; if (!contentElement) { throw new Error('Could not find message content element'); } try { // Stream chat with the agent await multiStepAgent.streamChat( messageContent, (chunk: string) => { // Update the streaming message content assistantMessage.content += chunk; contentElement.innerHTML = this.formatMessageContent(assistantMessage.content || '') + '<span class="cursor">|</span>'; // Only attach citation listeners if the content contains citation patterns // This avoids excessive listener attachment during streaming if (assistantMessage.content && assistantMessage.content.includes('[REF') && messageElement) { this.addCitationEventListenersThrottled(messageElement as HTMLElement); } this.scrollToBottom(); }, (event: WorkflowEvent) => { // Handle workflow events this.handleWorkflowEvent(event); } ); // Check if response is empty if (this.isEmptyResponse(assistantMessage.content || '')) { throw new Error('Received empty response from agent'); } // Remove streaming cursor and finalize assistantMessage.isStreaming = false; contentElement.innerHTML = this.formatMessageContent(assistantMessage.content || ''); // Final citation event listener attachment - always do this at the end if (messageElement) { this.addCitationEventListeners(messageElement as HTMLElement); } } catch (error) { // Update message with error assistantMessage.content = `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`; assistantMessage.error = error instanceof Error ? error.message : 'Unknown error'; assistantMessage.isStreaming = false; contentElement.innerHTML = this.formatMessageContent(assistantMessage.content); throw error; } } /** * Handle chat using legacy AI client (fallback) */ private async handleLegacyChat(messageContent: string): Promise<void> { console.log('🔄 Using legacy AI client...'); // Use the full conversation history if available, otherwise prepare from UI messages let aiMessages: ChatMessage[]; if (this.fullConversationHistory.length > 0) { // Use the complete conversation history (includes tool calls and responses) aiMessages = [...this.fullConversationHistory]; // Add the new user message aiMessages.push({ role: 'user', content: messageContent, }); } else { // First message - prepare from UI messages const messages = this.state.messages.map(msg => ({ role: msg.role, content: msg.content, })); // Add dynamic system prompt with current timestamp const systemPrompt = generateSystemPrompt(); aiMessages = [ { role: 'system' as const, content: systemPrompt }, ...messages, ]; } console.log('📝 Prepared messages:', aiMessages); // Use tool calling console.log('🔄 Calling aiClient.chatWithTools...'); const conversationMessages = await aiClient.chatWithTools(aiMessages); console.log('✅ Got conversation messages:', conversationMessages.length); // Update the full conversation history with the complete conversation this.fullConversationHistory = conversationMessages; console.log('📝 Updated full conversation history:', this.fullConversationHistory.length, 'messages'); // Find new assistant messages to add to the UI const assistantMessages = conversationMessages.filter(msg => msg.role === 'assistant'); const lastAssistantMessage = assistantMessages[assistantMessages.length - 1]; if (lastAssistantMessage && lastAssistantMessage.content) { // Check if response is empty if (this.isEmptyResponse(lastAssistantMessage.content)) { throw new Error('Received empty response from AI'); } // Add the final assistant response to the UI const assistantMessage: ChatUIMessage = { id: this.generateMessageId(), role: 'assistant', content: lastAssistantMessage.content, timestamp: Date.now(), tool_calls: lastAssistantMessage.tool_calls, }; this.addMessage(assistantMessage); } else { // No content in the final response console.warn('⚠️ No content in final assistant message:', lastAssistantMessage); throw new Error('Received empty response from AI'); } } /** * Handle workflow events from the agent */ private handleWorkflowEvent(event: WorkflowEvent): void { if (!agentConfigService.getConfig().showWorkflowSteps) { return; } console.log('🔄 Workflow event:', event); // Display workflow events in the UI if container exists if (this.workflowEventsContainer) { const eventElement = document.createElement('div'); eventElement.className = 'workflow-event'; eventElement.innerHTML = ` <div class="event-type">${event.type}</div> <div class="event-time">${new Date(event.timestamp).toLocaleTimeString()}</div> <div class="event-data">${JSON.stringify(event.data, null, 2)}</div> `; this.workflowEventsContainer.appendChild(eventElement); this.workflowEventsContainer.scrollTop = this.workflowEventsContainer.scrollHeight; } } private addMessage(message: ChatUIMessage): void { this.state.messages.push(message); this.renderMessage(message); this.scrollToBottom(); } private renderMessage(message: ChatUIMessage): void { console.log('[ChatApp] renderMessage called with message:', JSON.parse(JSON.stringify(message))); // DEV_PLAN debug // Remove welcome message if this is the first real message if (this.state.messages.length === 1) { const welcomeMessage = this.chatMessages.querySelector('.welcome-message'); if (welcomeMessage) { welcomeMessage.remove(); } } // Check if this message has tool calls or citations and should use enhanced rendering const hasToolCalls = message.tool_calls && message.tool_calls.length > 0; const hasCitations = message.content && /\[REF\d+\]/.test(message.content); const shouldUseEnhancedRenderer = (hasToolCalls || hasCitations) && !message.isStreaming; console.log('[ChatApp] Rendering decision:', { hasToolCalls, hasCitations, isStreaming: message.isStreaming, shouldUseEnhancedRenderer }); // DEV_PLAN debug if (shouldUseEnhancedRenderer) { console.log('[ChatApp] Using enhanced renderer'); // DEV_PLAN debug // Use enhanced MessageRenderer for messages with tool results this.renderEnhancedMessage(message); } else { console.log('[ChatApp] Using simple renderer'); // DEV_PLAN debug // Use existing rendering for streaming messages and simple text this.renderSimpleMessage(message); } } /** * Render message using the enhanced MessageRenderer (for tool results) */ private renderEnhancedMessage(message: ChatUIMessage): void { console.log('[ChatApp] renderEnhancedMessage called'); // DEV_PLAN debug // Extract tool result from citation data if available const toolResult = this.extractToolResultFromCitations(message); console.log('[ChatApp] Extracted tool result:', toolResult); // DEV_PLAN debug // Convert ChatUIMessage to MessageData format const messageData: MessageData = { content: message.content || '', role: message.role, timestamp: message.timestamp, toolCall: toolResult ? { name: toolResult.toolName, result: toolResult.data } : undefined }; console.log('[ChatApp] Prepared MessageData for MessageRenderer:', JSON.parse(JSON.stringify(messageData))); // DEV_PLAN debug // Create a container for this message const messageContainer = document.createElement('div'); messageContainer.className = 'enhanced-message-container'; messageContainer.dataset.messageId = message.id; // Use MessageRenderer to render the enhanced message console.log('[ChatApp] Calling messageRenderer.renderMessage...'); // DEV_PLAN debug const renderedMessage = this.messageRenderer.renderMessage(messageData); messageContainer.appendChild(renderedMessage); this.chatMessages.appendChild(messageContainer); // Add citation event listeners if this is an assistant message if (message.role === 'assistant') { setTimeout(() => { this.addCitationEventListeners(messageContainer); }, 0); } } /** * Render message using the existing simple approach (for streaming and simple messages) */ private renderSimpleMessage(message: ChatUIMessage): void { const messageElement = document.createElement('div'); messageElement.className = 'chat-message'; messageElement.dataset.messageId = message.id; const avatar = message.role === 'user' ? 'U' : 'AI'; const avatarClass = message.role === 'user' ? 'user' : ''; const contentClass = message.role === 'user' ? 'user' : ''; const name = message.role === 'user' ? 'You' : 'Assistant'; const time = new Date(message.timestamp).toLocaleTimeString(); messageElement.innerHTML = ` <div class="message-header"> <div class="message-avatar ${avatarClass}">${avatar}</div> <span class="message-name">${name}</span> <span class="message-time">${time}</span> </div> <div class="message-content ${contentClass}" data-content> ${this.formatMessageContent(message.content || '')} ${message.isStreaming ? '<span class="cursor">|</span>' : ''} </div> `; this.chatMessages.appendChild(messageElement); // Add citation event listeners if this is an assistant message if (message.role === 'assistant') { setTimeout(() => { this.addCitationEventListeners(messageElement); }, 0); } } /** * Extract tool result from citation data */ private extractToolResultFromCitations(message: ChatUIMessage): { toolName: string; data: any } | null { console.log('[ChatApp] extractToolResultFromCitations called with message content:', message.content); // DEV_PLAN 1.22 debug const content = message.content || ''; // Look for citation references in the message content const citationMatches = content.match(/\[REF\d+\]/g); console.log('[ChatApp] Found citation matches:', citationMatches); // DEV_PLAN 1.22 debug if (!citationMatches) { return null; } // Get the first citation reference const firstCitation = citationMatches[0]; console.log('[ChatApp] Processing first citation:', firstCitation); // DEV_PLAN 1.22 debug // Remove brackets from citation reference for store lookup const referenceId = firstCitation.replace(/[\[\]]/g, ''); // Remove [ and ] console.log('[ChatApp] Looking up citation with ID:', referenceId); // DEV_PLAN 1.22 debug // Access the citation store to get the stored data try { const citationData = citationStore.getCitation(referenceId); console.log('[ChatApp] Retrieved citation data from store (type:', typeof citationData, '):', citationData); // DEV_PLAN 1.22 debug if (citationData && citationData.rawData) { const result = { toolName: citationData.toolName || 'unknown', data: citationData.rawData }; console.log('[ChatApp] Returning tool result:', JSON.parse(JSON.stringify(result))); // DEV_PLAN 1.22 debug return result; } else { console.log('[ChatApp] No rawData found in citation data. Available keys:', citationData ? Object.keys(citationData) : 'null'); // DEV_PLAN 1.22 debug } } catch (error) { console.warn('[ChatApp] Failed to extract citation data:', error); // DEV_PLAN 1.22 debug console.warn('[ChatApp] Citation data that caused error:', citationStore.getCitation(firstCitation)); // DEV_PLAN 1.22 debug } return null; } private formatMessageContent(content: string): string { // Process citations first const processedMessage = citationProcessor.processMessage(content); try { // Use marked to render markdown to HTML (synchronous) const htmlContent = marked.parse(processedMessage.processedContent, { async: false }) as string; // Basic sanitization - remove potentially dangerous elements return this.sanitizeHtml(htmlContent); } catch (error) { console.warn('Failed to parse markdown, falling back to basic formatting:', error); // Fallback to basic formatting if marked fails return processedMessage.processedContent .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/`(.*?)`/g, '<code>$1</code>') .replace(/\n/g, '<br>'); } } /** * Basic HTML sanitization to remove potentially dangerous elements */ private sanitizeHtml(html: string): string { // Remove script tags and event handlers return html .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') .replace(/on\w+="[^"]*"/gi, '') .replace(/on\w+='[^']*'/gi, '') .replace(/javascript:/gi, ''); } /** * Throttled version of addCitationEventListeners to reduce spam during streaming */ private addCitationEventListenersThrottled(messageElement: HTMLElement): void { const now = Date.now(); if (now - this.lastCitationAttachTime < this.citationAttachThrottle) { return; // Skip if called too recently } this.lastCitationAttachTime = now; this.addCitationEventListeners(messageElement); } /** * Add event listeners for citation interactions */ private addCitationEventListeners(messageElement: HTMLElement): void { const citationRefs = messageElement.querySelectorAll('.citation-ref'); // Reduce logging spam - only log once when citations are found if (citationRefs.length > 0) { console.log(`🎯 Attaching listeners to ${citationRefs.length} citation references`); } citationRefs.forEach((citationRef) => { const element = citationRef as HTMLElement; const referenceId = element.dataset.referenceId; if (!referenceId) { return; } // Remove default browser tooltip to avoid double tooltips element.removeAttribute('title'); // Add click handler element.addEventListener('click', () => { console.log(`🖱️ Citation clicked: ${referenceId}`); this.handleCitationClick(referenceId); }); // Add hover handlers for tooltip let tooltipTimeout: NodeJS.Timeout; let tooltip: HTMLElement | null = null; element.addEventListener('mouseenter', () => { tooltipTimeout = setTimeout(() => { tooltip = this.showCitationTooltip(element, referenceId); }, 500); // Show tooltip after 500ms hover }); element.addEventListener('mouseleave', () => { clearTimeout(tooltipTimeout); if (tooltip) { tooltip.remove(); tooltip = null; } }); // Add keyboard support element.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.handleCitationClick(referenceId); } }); }); } /** * Handle citation click to show full data */ private handleCitationClick(referenceId: string): void { console.log(`🔍 Handling citation click for: ${referenceId}`); const tooltipData = citationProcessor.getCitationTooltipData(referenceId); console.log(`📊 Citation tooltip data:`, tooltipData); if (!tooltipData) { console.warn(`❌ No citation data found for ${referenceId}`); return; } // Create and show modal with full citation data this.showCitationModal(tooltipData); } /** * Show citation tooltip on hover */ private showCitationTooltip(element: HTMLElement, referenceId: string): HTMLElement | null { const tooltipData = citationProcessor.getCitationTooltipData(referenceId); if (!tooltipData) { return null; } const tooltip = document.createElement('div'); tooltip.className = 'citation-tooltip-container'; tooltip.innerHTML = citationProcessor.createTooltipContent(tooltipData); // Add to DOM first to get dimensions document.body.appendChild(tooltip); // Position tooltip relative to the citation element const rect = element.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); // Calculate position (prefer below, but above if no space) let top = rect.bottom + window.scrollY + 8; let left = rect.left + window.scrollX; // Adjust if tooltip would go off screen if (left + tooltipRect.width > window.innerWidth) { left = window.innerWidth - tooltipRect.width - 10; } if (top + tooltipRect.height > window.innerHeight + window.scrollY) { top = rect.top + window.scrollY - tooltipRect.height - 8; } tooltip.style.position = 'absolute'; tooltip.style.top = `${top}px`; tooltip.style.left = `${left}px`; tooltip.style.zIndex = '1000'; return tooltip; } /** * Show citation modal with full data */ private showCitationModal(tooltipData: any): void { // Create modal overlay const overlay = document.createElement('div'); overlay.className = 'citation-modal-overlay'; const modal = document.createElement('div'); modal.className = 'citation-modal'; modal.innerHTML = ` <div class="citation-modal-header"> <h3>Citation Data: ${tooltipData.referenceId}</h3> <button class="citation-modal-close" aria-label="Close">&times;</button> </div> <div class="citation-modal-content"> <div class="citation-info"> <p><strong>Tool:</strong> ${tooltipData.toolName}</p> <p><strong>Timestamp:</strong> ${citationProcessor.formatTimestamp(tooltipData.timestamp)}</p> ${tooltipData.endpoint ? `<p><strong>Endpoint:</strong> ${tooltipData.endpoint}</p>` : ''} </div> <div class="citation-raw-data"> <h4>Raw Data:</h4> <pre><code>${JSON.stringify(tooltipData, null, 2)}</code></pre> </div> </div> `; overlay.appendChild(modal); document.body.appendChild(overlay); // Add event listeners const closeBtn = modal.querySelector('.citation-modal-close'); const closeModal = () => overlay.remove(); closeBtn?.addEventListener('click', closeModal); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); // Close on Escape key const handleKeydown = (e: KeyboardEvent) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', handleKeydown); } }; document.addEventListener('keydown', handleKeydown); } private showTypingIndicator(): void { this.typingIndicator.classList.remove('hidden'); } private hideTypingIndicator(): void { this.typingIndicator.classList.add('hidden'); } private scrollToBottom(): void { this.chatMessages.scrollTop = this.chatMessages.scrollHeight; } private generateMessageId(): string { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Public methods for external use public getMessages(): ChatUIMessage[] { return [...this.state.messages]; } public clearMessages(): void { this.state.messages = []; this.fullConversationHistory = []; this.retryCount = 0; this.chatMessages.innerHTML = ''; this.loadWelcomeMessage(); } public isLoading(): boolean { return this.state.isLoading; } public toggleAgentMode(useAgent: boolean): void { this.useAgent = useAgent; this.updateConnectionStatus(); console.log(`🔄 Switched to ${useAgent ? 'agent' : 'legacy'} mode`); } public isUsingAgent(): boolean { return this.useAgent; } public isAgentModeEnabled(): boolean { return this.useAgent; } public getAgentState() { return multiStepAgent.getState(); } }

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/sammcj/bybit-mcp'

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