@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
JavaScript
/**
* 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
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;