UNPKG

@vectorchat/mcp-server

Version:

VectorChat MCP Server - Encrypted AI-to-AI communication with hardware security (YubiKey/TPM). 45+ MCP tools for Windsurf, Claude, and AI assistants. Model-based identity with EMDM encryption. Dynamic AI playbook system, communication zones, message relay

868 lines (725 loc) 25.2 kB
/** * VectorChat Web Services * Provides all the core functionality for the web application */ // WebSocket Service class WebSocketService { constructor(serverUrl = 'ws://localhost:8765') { this.serverUrl = serverUrl; this.websocket = null; this.isConnected = false; this.username = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 2000; // Event callbacks this.onConnected = () => {}; this.onDisconnected = () => {}; this.onMessageReceived = (message) => {}; this.onSystemMessage = (message) => {}; this.onUserListUpdate = (userList) => {}; this.onError = (error) => {}; } async connect(username) { this.username = username; try { this.websocket = new WebSocket(this.serverUrl); return new Promise((resolve, reject) => { this.websocket.onopen = () => { console.log('WebSocket connected'); this.isConnected = true; this.reconnectAttempts = 0; // Register with daemon this.send({ route: 'register', username: username, client_type: 'web_client', session_duration: 300 }); this.onConnected(); resolve(); }; this.websocket.onmessage = (event) => { try { const data = JSON.parse(event.data); this.handleMessage(data); } catch (e) { console.error('Error parsing WebSocket message:', e); this.onError(e); } }; this.websocket.onclose = () => { console.log('WebSocket disconnected'); this.isConnected = false; this.onDisconnected(); // Auto-reconnect this.attemptReconnect(); }; this.websocket.onerror = (error) => { console.error('WebSocket error:', error); this.onError(error); reject(error); }; // Timeout setTimeout(() => { if (!this.isConnected) { reject(new Error('Connection timeout')); } }, 10000); }); } catch (error) { console.error('Connection error:', error); throw error; } } disconnect() { if (this.websocket) { this.websocket.close(); this.websocket = null; } this.isConnected = false; } send(data) { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { this.websocket.send(JSON.stringify(data)); } else { console.warn('WebSocket not connected, message not sent:', data); } } sendMessage(recipient, message) { this.send({ route: 'message', recipient: recipient, message: message }); } handleMessage(data) { console.log('Received message:', data); switch (data.type) { case 'registered': console.log('Successfully registered as:', data.username); break; case 'message': this.onMessageReceived({ sender: data.sender, message: data.message, timestamp: new Date(data.timestamp || Date.now()) }); break; case 'user_presence': this.handleUserPresence(data); break; case 'welcome': console.log('Connected to daemon:', data.daemon_info); break; case 'error': this.onSystemMessage(`Error: ${data.error}`); break; default: console.log('Unhandled message type:', data.type); } } handleUserPresence(data) { if (data.event === 'user_joined') { this.onSystemMessage(`${data.username} joined (${data.client_type})`); } else if (data.event === 'user_left') { this.onSystemMessage(`${data.username} left`); } } attemptReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; console.log(`Attempting reconnect ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); setTimeout(() => { this.connect(this.username); }, this.reconnectDelay * this.reconnectAttempts); } } } // Chat History Service class ChatHistoryService { constructor() { this.history = {}; this.maxMessagesPerChat = 1000; this.enabled = true; } saveMessage(chatId, message) { if (!this.enabled) return; if (!this.history[chatId]) { this.history[chatId] = []; } this.history[chatId].push({ ...message, id: Date.now() + Math.random(), timestamp: new Date() }); // Trim old messages if (this.history[chatId].length > this.maxMessagesPerChat) { this.history[chatId] = this.history[chatId].slice(-this.maxMessagesPerChat); } this.persist(); } getMessages(chatId) { return this.history[chatId] || []; } clearChat(chatId) { if (this.history[chatId]) { delete this.history[chatId]; this.persist(); } } persist() { if (this.enabled) { localStorage.setItem('vectorchat-web-history', JSON.stringify(this.history)); } } load() { if (!this.enabled) return; const saved = localStorage.getItem('vectorchat-web-history'); if (saved) { try { this.history = JSON.parse(saved); } catch (e) { console.error('Error loading chat history:', e); } } } } // AI Conversation Service class AIConversationService { constructor() { this.isActive = false; this.participants = []; this.currentUser = ''; this.topic = ''; // Event callbacks this.onStarted = () => {}; this.onEnded = () => {}; this.onUpdate = (message) => {}; } startConversation(participants, currentUser, topic = '') { this.isActive = true; this.participants = participants; this.currentUser = currentUser; this.topic = topic; this.onStarted(participants); this.onUpdate(`AI conversation started with: ${participants.join(', ')}`); } endConversation() { this.isActive = false; this.participants = []; this.topic = ''; this.onEnded(); this.onUpdate('AI conversation ended'); } isAIConversationRequest(message, availableAIs) { const aiKeywords = ['talk to', 'ask', 'discuss with', 'chat with', 'speak to']; const lowerMessage = message.toLowerCase(); return aiKeywords.some(keyword => lowerMessage.includes(keyword)) && availableAIs.some(ai => lowerMessage.includes(ai.toLowerCase())); } extractAIParticipants(message, availableAIs) { const participants = []; availableAIs.forEach(ai => { if (message.toLowerCase().includes(ai.toLowerCase())) { participants.push(ai); } }); return participants; } extractTopic(message) { // Simple topic extraction - look for patterns like "about X" const topicMatch = message.match(/about\s+(.+?)(?:\s|$|\.)/i); return topicMatch ? topicMatch[1].trim() : ''; } } // Settings Service class SettingsService { constructor() { this.settings = this.loadDefaults(); this.load(); } loadDefaults() { return { username: '', websocketUrl: 'ws://localhost:8765', theme: 'light', aiConversationsEnabled: true, chatHistoryEnabled: true, autoConnectEnabled: false, encryptionEnabled: true, notificationsEnabled: true, lastModelPath: '', userIdentity: '' }; } load() { const saved = localStorage.getItem('vectorchat-web-settings'); if (saved) { try { this.settings = { ...this.settings, ...JSON.parse(saved) }; } catch (e) { console.error('Error loading settings:', e); } } } save() { localStorage.setItem('vectorchat-web-settings', JSON.stringify(this.settings)); } setUsername(username) { this.settings.username = username; this.save(); } setUserIdentity(identity) { this.settings.userIdentity = identity; this.save(); } } // Identity Manager class IdentityManager { constructor() { this.identities = []; this.currentIdentity = null; } async initialize() { await this.loadIdentities(); } async loadIdentities() { // Load identities from daemon API try { const response = await fetch('http://localhost:3737/api/scan-models', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ paths: ['~/.vectorchat/models', '~/.lmstudio/models', '~/.ollama/models'] }) }); const data = await response.json(); this.identities = data.models || []; } catch (error) { console.error('Error loading identities:', error); } } getAllIdentities() { return this.identities; } getIdentityByPath(modelPath) { return this.identities.find(identity => identity.path === modelPath); } } // Daemon Status Service class DaemonStatusService { constructor() { this.status = {}; this.statusInterval = null; } startMonitoring() { this.statusInterval = setInterval(async () => { await this.updateStatus(); }, 5000); // Update every 5 seconds } stopMonitoring() { if (this.statusInterval) { clearInterval(this.statusInterval); this.statusInterval = null; } } async updateStatus() { try { const response = await fetch('http://localhost:3737/health'); const data = await response.json(); this.status = { ...this.status, webInterface: data.status === 'ok', timestamp: Date.now() }; } catch (error) { this.status.webInterface = false; } // Update UI this.updateUI(); } updateUI() { const statusElement = document.getElementById('connectionStatusText'); if (statusElement) { if (this.status.webInterface) { statusElement.textContent = 'Daemon Online'; } else { statusElement.textContent = 'Daemon Offline'; } } } } // GPU Detection Service class GPUDetectionService { constructor() { this.gpuInfo = null; } async detectGPU() { try { // Try CUDA detection first const cudaResponse = await fetch('http://localhost:3737/api/detect-cuda'); const cudaData = await cudaResponse.json(); if (cudaData.available) { this.gpuInfo = { type: 'CUDA', ...cudaData }; return this.gpuInfo; } // Try ROCm detection const rocmResponse = await fetch('http://localhost:3737/api/detect-rocm'); const rocmData = await rocmResponse.json(); if (rocmData.available) { this.gpuInfo = { type: 'ROCm', ...rocmData }; return this.gpuInfo; } // Try Intel NPU detection const npuResponse = await fetch('http://localhost:3737/api/detect-npu'); const npuData = await npuResponse.json(); if (npuData.available) { this.gpuInfo = { type: 'NPU', ...npuData }; return this.gpuInfo; } // Fallback to CPU const cpuResponse = await fetch('http://localhost:3737/api/detect-cpu'); const cpuData = await cpuResponse.json(); this.gpuInfo = { type: 'CPU', ...cpuData }; return this.gpuInfo; } catch (error) { console.error('Error detecting GPU:', error); this.gpuInfo = { type: 'Unknown', error: error.message }; return this.gpuInfo; } } } // Model Service class ModelService { constructor() { this.models = []; this.currentModel = null; } async loadModels() { try { const response = await fetch('http://localhost:3737/api/scan-models', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ paths: ['~/.vectorchat/models', '~/.lmstudio/models', '~/.ollama/models'] }) }); const data = await response.json(); this.models = data.models || []; return this.models; } catch (error) { console.error('Error loading models:', error); return []; } } async downloadModel(modelName, ipfsCid) { try { const response = await fetch('http://localhost:3737/api/download-model', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: modelName, ipfsCid: ipfsCid }) }); if (response.ok) { return true; } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.error('Error downloading model:', error); throw error; } } getModelByName(name) { return this.models.find(model => model.name.includes(name)); } } // Notification Service class NotificationService { constructor() { this.notifications = []; this.maxNotifications = 10; } show(message, type = 'info', duration = 3000) { const notification = { id: Date.now() + Math.random(), message, type, timestamp: Date.now() }; this.notifications.unshift(notification); // Trim old notifications if (this.notifications.length > this.maxNotifications) { this.notifications = this.notifications.slice(0, this.maxNotifications); } // Create DOM element this.createNotificationElement(notification); // Auto-remove after duration if (duration > 0) { setTimeout(() => { this.remove(notification.id); }, duration); } return notification.id; } createNotificationElement(notification) { const element = document.createElement('div'); element.className = `notification notification-${notification.type}`; element.id = `notification-${notification.id}`; element.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; font-weight: 500; z-index: 3000; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; max-width: 400px; word-wrap: break-word; `; let backgroundColor; switch (notification.type) { case 'error': backgroundColor = '#f44336'; break; case 'success': backgroundColor = '#4caf50'; break; case 'warning': backgroundColor = '#ff9800'; break; default: backgroundColor = '#667eea'; } element.style.backgroundColor = backgroundColor; element.textContent = notification.message; document.body.appendChild(element); // Animate in setTimeout(() => { element.style.opacity = '1'; element.style.transform = 'translateX(0)'; }, 100); // Store reference for removal notification.element = element; } remove(id) { const notification = this.notifications.find(n => n.id === id); if (notification && notification.element) { notification.element.style.opacity = '0'; notification.element.style.transform = 'translateX(100%)'; setTimeout(() => { if (notification.element.parentNode) { notification.element.parentNode.removeChild(notification.element); } }, 300); } this.notifications = this.notifications.filter(n => n.id !== id); } clear() { this.notifications.forEach(n => this.remove(n.id)); } } // Main App Controller class VectorChatWebApp { constructor() { this.wsService = new WebSocketService(); this.chatHistory = new ChatHistoryService(); this.aiConversation = new AIConversationService(); this.settings = new SettingsService(); this.identityManager = new IdentityManager(); this.daemonStatus = new DaemonStatusService(); this.gpuDetection = new GPUDetectionService(); this.modelService = new ModelService(); this.notifications = new NotificationService(); this.currentUser = null; this.selectedUser = null; this.availableUsers = []; this.isConnected = false; this.initialize(); } async initialize() { console.log('Initializing VectorChat Web App...'); // Load settings this.settings.load(); this.chatHistory.load(); // Setup event listeners this.setupEventListeners(); // Initialize services await this.identityManager.initialize(); // Check daemon status this.daemonStatus.startMonitoring(); // Auto-connect if enabled if (this.settings.settings.autoConnectEnabled && this.settings.settings.username) { setTimeout(() => { this.connect(); }, 1000); } console.log('VectorChat Web App initialized'); } setupEventListeners() { // WebSocket events this.wsService.onConnected = () => { this.isConnected = true; this.notifications.show('Connected to VectorChat daemon!', 'success'); this.updateConnectionStatus(); }; this.wsService.onDisconnected = () => { this.isConnected = false; this.notifications.show('Disconnected from daemon', 'warning'); this.updateConnectionStatus(); }; this.wsService.onMessageReceived = (message) => { this.addMessage(message.sender, message.message, false); }; this.wsService.onSystemMessage = (message) => { this.notifications.show(message, 'info'); }; this.wsService.onError = (error) => { this.notifications.show(`Connection error: ${error.message}`, 'error'); }; } async connect() { try { let username = this.settings.settings.username; if (!username) { username = prompt('Enter your username:'); if (!username) { this.notifications.show('Username required', 'warning'); return; } this.settings.setUsername(username); } await this.wsService.connect(username); this.currentUser = username; } catch (error) { this.notifications.show(`Connection failed: ${error.message}`, 'error'); } } disconnect() { this.wsService.disconnect(); } addMessage(sender, message, isUser = false) { const container = document.getElementById('messagesContainer'); const messageElement = document.createElement('div'); messageElement.className = `message ${isUser ? 'user' : ''}`; const time = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); messageElement.innerHTML = ` <div class="message-content">${this.escapeHtml(message)}</div> <div class="message-time">${time}</div> `; container.appendChild(messageElement); container.scrollTop = container.scrollHeight; // Save to history if (this.selectedUser) { this.chatHistory.saveMessage(this.selectedUser, { sender: isUser ? this.currentUser : sender, message: message, isUser: isUser }); } } selectUser(username) { this.selectedUser = username; // Update UI document.querySelectorAll('.user-item').forEach(item => { item.classList.remove('active'); }); const userList = document.getElementById('userList'); const userItems = userList.querySelectorAll('.user-item'); for (let item of userItems) { if (item.textContent.includes(username)) { item.classList.add('active'); break; } } // Update chat header const userType = this.availableUsers.includes(username) ? 'ai_assistant' : 'user'; document.getElementById('chatTitle').textContent = username; document.getElementById('chatSubtitle').textContent = userType === 'ai_assistant' ? 'AI Assistant' : 'User'; // Load chat history this.loadChatHistory(username); // Hide welcome screen document.getElementById('welcomeScreen').style.display = 'none'; } loadChatHistory(username) { const messages = this.chatHistory.getMessages(username); const container = document.getElementById('messagesContainer'); container.innerHTML = ''; if (messages.length > 0) { messages.forEach(msg => { this.addMessage(msg.sender, msg.message, msg.isUser); }); } } sendMessage() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (!message || !this.selectedUser) return; // Add user message this.addMessage(this.selectedUser, message, true); // Send via WebSocket this.wsService.sendMessage(this.selectedUser, message); input.value = ''; input.focus(); } updateConnectionStatus() { const statusElement = document.getElementById('connectionStatus'); const button = document.querySelector('.btn-primary'); if (this.isConnected) { statusElement.className = 'connection-status connected'; document.getElementById('connectionText').textContent = `Connected as ${this.currentUser}`; button.textContent = '🔌 Disconnect'; } else { statusElement.className = 'connection-status disconnected'; document.getElementById('connectionText').textContent = 'Disconnected'; button.textContent = '🔌 Connect'; } } escapeHtml(text) { const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }; return text.replace(/[&<>"']/g, m => map[m]); } } // Global app instance let vectorChatApp; // Initialize when page loads document.addEventListener('DOMContentLoaded', function() { vectorChatApp = new VectorChatWebApp(); }); // Export for use in HTML window.VectorChatWebApp = VectorChatWebApp; window.vectorChatApp = vectorChatApp;