Skip to main content
Glama

Smart Code Search MCP Server

media-history-server.js33.7 kB
/** * Media History Server * Enhanced voice assistant server with media storage and MCP browser tools integration */ const express = require('express'); const WebSocket = require('ws'); const { Client } = require('@modelcontextprotocol/sdk/client/index.js'); const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); const multer = require('multer'); const sqlite3 = require('sqlite3').verbose(); const { open } = require('sqlite'); const path = require('path'); const fs = require('fs').promises; const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path; const cors = require('cors'); const dotenv = require('dotenv'); // Load environment variables dotenv.config(); // Set ffmpeg path ffmpeg.setFfmpegPath(ffmpegPath); const app = express(); app.use(cors()); app.use(express.json()); app.use(express.static('public')); // Configuration const CONFIG = { port: process.env.PORT || 3000, wsPort: process.env.WS_PORT || 3001, mediaDir: process.env.MEDIA_STORAGE_PATH || './media', dbPath: './media-history.db', thumbnailSize: { width: 300, height: 200 }, maxStorageGB: parseInt(process.env.MAX_STORAGE_GB) || 5, projectRoot: process.env.PROJECT_ROOT || process.cwd() }; // Initialize storage const storage = multer.diskStorage({ destination: async (req, file, cb) => { const type = file.mimetype.startsWith('video') ? 'recordings' : 'screenshots'; const dir = path.join(CONFIG.mediaDir, type); await fs.mkdir(dir, { recursive: true }); cb(null, dir); }, filename: (req, file, cb) => { const timestamp = Date.now(); const ext = path.extname(file.originalname); cb(null, `${timestamp}${ext}`); } }); const upload = multer({ storage, limits: { fileSize: 100 * 1024 * 1024 } // 100MB limit }); // Database setup let db; async function initDatabase() { db = await open({ filename: CONFIG.dbPath, driver: sqlite3.Database }); await db.exec(` CREATE TABLE IF NOT EXISTS media_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, filename TEXT NOT NULL, thumbnail TEXT, url TEXT, duration INTEGER, size INTEGER, width INTEGER, height INTEGER, project TEXT, context TEXT, annotations TEXT, tags TEXT, created_at INTEGER NOT NULL, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_time INTEGER NOT NULL, end_time INTEGER, project TEXT, media_count INTEGER DEFAULT 0, total_duration INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS annotations ( id INTEGER PRIMARY KEY AUTOINCREMENT, media_id INTEGER, type TEXT, data TEXT, created_at INTEGER, FOREIGN KEY (media_id) REFERENCES media_history(id) ); CREATE INDEX IF NOT EXISTS idx_media_created ON media_history(created_at); CREATE INDEX IF NOT EXISTS idx_media_type ON media_history(type); CREATE INDEX IF NOT EXISTS idx_media_project ON media_history(project); `); } // MCP Client Management const mcpClients = new Map(); let scsMcpClient = null; let elevenLabsClient = null; let browserMcpClient = null; async function initializeMCPClients() { try { // Initialize SCS-MCP client console.log('Initializing SCS-MCP client...'); const scsTransport = new StdioClientTransport({ command: 'node', args: [path.join(__dirname, '../src/index.js')], env: { ...process.env, PROJECT_ROOT: CONFIG.projectRoot } }); scsMcpClient = new Client({ name: 'media-assistant-scs', version: '1.0.0' }, { capabilities: {} }); await scsMcpClient.connect(scsTransport); mcpClients.set('scsMcp', scsMcpClient); console.log('✅ SCS-MCP connected'); // Initialize ElevenLabs MCP client (if API key available) if (process.env.ELEVEN_LABS_API_KEY) { console.log('Initializing ElevenLabs MCP client...'); const elevenLabsTransport = new StdioClientTransport({ command: 'npx', args: ['-y', '@modelcontextprotocol/server-elevenlabs'], env: { ...process.env } }); elevenLabsClient = new Client({ name: 'media-assistant-elevenlabs', version: '1.0.0' }, { capabilities: {} }); await elevenLabsClient.connect(elevenLabsTransport); mcpClients.set('elevenLabs', elevenLabsClient); console.log('✅ ElevenLabs MCP connected'); } // Initialize Browser MCP client (Playwright) if (process.env.ENABLE_BROWSER_MCP !== 'false') { console.log('Initializing Browser MCP client...'); const browserTransport = new StdioClientTransport({ command: 'npx', args: ['-y', '@modelcontextprotocol/server-playwright'], env: { ...process.env } }); browserMcpClient = new Client({ name: 'media-assistant-browser', version: '1.0.0' }, { capabilities: {} }); await browserMcpClient.connect(browserTransport); mcpClients.set('browser', browserMcpClient); console.log('✅ Browser MCP connected'); } } catch (error) { console.error('Failed to initialize MCP clients:', error); // Continue without MCP for development/testing } } // WebSocket Server const wss = new WebSocket.Server({ port: CONFIG.wsPort }); const connections = new Map(); let currentEditorContext = null; wss.on('connection', (ws, req) => { const id = Date.now().toString(); const clientType = req.url === '/vscode' ? 'vscode' : 'web'; connections.set(id, { ws, type: clientType, connected: new Date() }); console.log(`Client connected: ${clientType} (${id})`); // Send initial status ws.send(JSON.stringify({ type: 'status', data: { connected: true, scsMcp: scsMcpClient !== null, elevenLabs: elevenLabsClient !== null, browser: browserMcpClient !== null, clientId: id } })); ws.on('message', async (message) => { try { const data = JSON.parse(message.toString()); await handleWebSocketMessage(ws, id, data); } catch (error) { console.error('WebSocket message error:', error); ws.send(JSON.stringify({ type: 'error', error: error.message })); } }); ws.on('close', () => { connections.delete(id); console.log(`Connection closed: ${clientType} (${id})`); }); }); // WebSocket Message Handler async function handleWebSocketMessage(ws, clientId, data) { const client = connections.get(clientId); if (!client) return; switch (data.type) { // Voice commands case 'voice': await handleVoiceInput(clientId, data.text, data.context); break; case 'context': if (client.type === 'vscode') { currentEditorContext = data.data; broadcastToWebClients({ type: 'context_update', data: currentEditorContext }); } break; case 'command': await handleCommand(clientId, data.command, data.context); break; case 'tts': await handleTextToSpeech(clientId, data.text, data.voice); break; // Media commands case 'screenshot': await handleScreenshot(ws, data); break; case 'browser_capture': await handleBrowserCapture(ws, data); break; case 'start_recording': await handleStartRecording(ws, data); break; case 'stop_recording': await handleStopRecording(ws, data); break; case 'get_history': await sendMediaHistory(ws, data.filters); break; case 'annotate': await handleAnnotation(ws, data); break; case 'mcp_tool': await handleMCPTool(ws, data); break; default: console.log('Unknown message type:', data.type); } } // Voice Input Handler (from original server) async function handleVoiceInput(clientId, text, additionalContext) { console.log(`Voice input from ${clientId}: "${text}"`); const context = { ...currentEditorContext, ...additionalContext, voiceInput: text }; // Check for media-related commands const lowerText = text.toLowerCase(); if (lowerText.includes('screenshot') || lowerText.includes('capture')) { await handleVoiceScreenshot(clientId, text, context); return; } if (lowerText.includes('start recording') || lowerText.includes('record')) { await handleVoiceStartRecording(clientId, context); return; } if (lowerText.includes('stop recording')) { await handleVoiceStopRecording(clientId); return; } if (lowerText.includes('show media') || lowerText.includes('media history')) { await handleVoiceShowMedia(clientId); return; } // Otherwise handle as normal SCS-MCP command const intent = await determineIntent(text, context); const result = await executeScsTool(intent.tool, intent.arguments); const response = await generateResponse(result, intent); sendToClient(clientId, { type: 'response', data: { text: response.text, code: response.code, action: intent.tool, context: context } }); // Generate audio if ElevenLabs available if (elevenLabsClient) { const audio = await generateSpeech(response.text); if (audio) { sendToClient(clientId, { type: 'audio', data: audio }); } } } // Voice Media Commands async function handleVoiceScreenshot(clientId, text, context) { const ws = connections.get(clientId)?.ws; if (!ws) return; await handleScreenshot(ws, { fullPage: text.includes('full') || text.includes('entire'), element: context.selectedCode ? 'selection' : null, project: context.currentFile ? path.basename(path.dirname(context.currentFile)) : 'default', context: context }); sendToClient(clientId, { type: 'response', data: { text: 'Screenshot captured successfully', action: 'screenshot' } }); } async function handleVoiceStartRecording(clientId, context) { const ws = connections.get(clientId)?.ws; if (!ws) return; await handleStartRecording(ws, { project: context.currentFile ? path.basename(path.dirname(context.currentFile)) : 'default', context: context }); sendToClient(clientId, { type: 'response', data: { text: 'Recording started. Say "stop recording" when finished.', action: 'start_recording' } }); } async function handleVoiceStopRecording(clientId) { const ws = connections.get(clientId)?.ws; if (!ws) return; // Find active recording for this client const recordingId = Array.from(activeRecordings.keys()).find( id => activeRecordings.get(id).ws === ws ); if (recordingId) { await handleStopRecording(ws, { id: recordingId }); sendToClient(clientId, { type: 'response', data: { text: 'Recording saved successfully', action: 'stop_recording' } }); } else { sendToClient(clientId, { type: 'response', data: { text: 'No active recording found', action: 'stop_recording' } }); } } async function handleVoiceShowMedia(clientId) { const ws = connections.get(clientId)?.ws; if (!ws) return; await sendMediaHistory(ws, {}); sendToClient(clientId, { type: 'response', data: { text: 'Opening media history viewer', action: 'show_media' } }); } // Screenshot Handler async function handleScreenshot(ws, data) { try { let screenshotData = null; // Try to use browser MCP if available if (browserMcpClient) { try { const result = await browserMcpClient.callTool('browser_take_screenshot', { fullPage: data.fullPage || false, element: data.element, ref: data.ref }); screenshotData = result.content[0]?.data; } catch (error) { console.log('Browser MCP screenshot failed, using fallback'); } } // Fallback: create a placeholder or use system screenshot if (!screenshotData) { // Create a placeholder image for testing const placeholder = await sharp({ create: { width: 1920, height: 1080, channels: 4, background: { r: 102, g: 126, b: 234, alpha: 1 } } }) .png() .toBuffer(); screenshotData = placeholder.toString('base64'); } // Save screenshot const filename = `screenshot-${Date.now()}.png`; const filepath = path.join(CONFIG.mediaDir, 'screenshots', filename); const buffer = Buffer.from(screenshotData, 'base64'); await fs.mkdir(path.dirname(filepath), { recursive: true }); await fs.writeFile(filepath, buffer); // Generate thumbnail const thumbnailPath = await generateThumbnail(filepath, 'screenshot'); // Get image dimensions const metadata = await sharp(filepath).metadata(); // Save to database const mediaEntry = await db.run(` INSERT INTO media_history ( type, filename, thumbnail, url, size, width, height, project, context, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ 'screenshot', filename, path.basename(thumbnailPath), `/media/screenshots/${filename}`, buffer.length, metadata.width, metadata.height, data.project || 'default', JSON.stringify(data.context || {}), Date.now() ]); ws.send(JSON.stringify({ type: 'screenshot_saved', data: { id: mediaEntry.lastID, url: `/media/screenshots/${filename}`, thumbnail: `/media/thumbnails/${path.basename(thumbnailPath)}`, metadata } })); broadcastUpdate('new_media', { type: 'screenshot', id: mediaEntry.lastID }); } catch (error) { console.error('Screenshot error:', error); ws.send(JSON.stringify({ type: 'error', error: 'Failed to capture screenshot' })); } } // Browser Capture Handler async function handleBrowserCapture(ws, data) { try { if (!browserMcpClient) { throw new Error('Browser MCP not available'); } // Navigate to URL if provided if (data.url) { await browserMcpClient.callTool('browser_navigate', { url: data.url }); await browserMcpClient.callTool('browser_wait_for', { time: 2 }); } // Take snapshot const snapshot = await browserMcpClient.callTool('browser_snapshot', {}); // Take screenshot const screenshot = await browserMcpClient.callTool('browser_take_screenshot', { fullPage: true }); // Save files const timestamp = Date.now(); const screenshotFile = `browser-${timestamp}.png`; const snapshotFile = `browser-${timestamp}.json`; const screenshotPath = path.join(CONFIG.mediaDir, 'screenshots', screenshotFile); const snapshotPath = path.join(CONFIG.mediaDir, 'snapshots', snapshotFile); await fs.mkdir(path.join(CONFIG.mediaDir, 'snapshots'), { recursive: true }); const buffer = Buffer.from(screenshot.content[0]?.data || '', 'base64'); await fs.writeFile(screenshotPath, buffer); await fs.writeFile(snapshotPath, JSON.stringify(snapshot, null, 2)); // Generate thumbnail const thumbnailPath = await generateThumbnail(screenshotPath, 'browser'); // Save to database const mediaEntry = await db.run(` INSERT INTO media_history ( type, filename, thumbnail, url, context, created_at ) VALUES (?, ?, ?, ?, ?, ?) `, [ 'browser', screenshotFile, path.basename(thumbnailPath), data.url || '', JSON.stringify({ snapshot: snapshotFile, url: data.url, ...data.context }), Date.now() ]); ws.send(JSON.stringify({ type: 'browser_captured', data: { id: mediaEntry.lastID, screenshot: `/media/screenshots/${screenshotFile}`, snapshot: `/media/snapshots/${snapshotFile}`, thumbnail: `/media/thumbnails/${path.basename(thumbnailPath)}` } })); broadcastUpdate('new_media', { type: 'browser', id: mediaEntry.lastID }); } catch (error) { console.error('Browser capture error:', error); ws.send(JSON.stringify({ type: 'error', error: 'Browser capture not available. Install @modelcontextprotocol/server-playwright' })); } } // Recording Handlers const activeRecordings = new Map(); async function handleStartRecording(ws, data) { try { const recordingId = Date.now().toString(); const filename = `recording-${recordingId}.webm`; const filepath = path.join(CONFIG.mediaDir, 'recordings', filename); await fs.mkdir(path.dirname(filepath), { recursive: true }); // Store recording session activeRecordings.set(recordingId, { ws, filename, filepath, startTime: Date.now(), chunks: [] }); // Create session in database const session = await db.run(` INSERT INTO sessions (start_time, project) VALUES (?, ?) `, [Date.now(), data.project || 'default']); ws.send(JSON.stringify({ type: 'recording_started', data: { id: recordingId, sessionId: session.lastID } })); } catch (error) { console.error('Start recording error:', error); ws.send(JSON.stringify({ type: 'error', error: 'Failed to start recording' })); } } async function handleStopRecording(ws, data) { try { const recording = activeRecordings.get(data.id); if (!recording) throw new Error('Recording not found'); // For now, create a placeholder video file // In production, you'd receive video chunks from the client const placeholderVideo = Buffer.from('placeholder video data'); await fs.writeFile(recording.filepath, placeholderVideo); // Generate thumbnail (placeholder for now) const thumbnailPath = await generateVideoThumbnail(recording.filepath); // Mock metadata const metadata = { duration: Math.floor((Date.now() - recording.startTime) / 1000), size: placeholderVideo.length, width: 1920, height: 1080 }; // Save to database const mediaEntry = await db.run(` INSERT INTO media_history ( type, filename, thumbnail, url, duration, size, project, context, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ 'recording', recording.filename, path.basename(thumbnailPath), `/media/recordings/${recording.filename}`, metadata.duration, metadata.size, data.project || 'default', JSON.stringify(data.context || {}), Date.now() ]); activeRecordings.delete(data.id); ws.send(JSON.stringify({ type: 'recording_saved', data: { id: mediaEntry.lastID, url: `/media/recordings/${recording.filename}`, thumbnail: `/media/thumbnails/${path.basename(thumbnailPath)}`, duration: metadata.duration } })); broadcastUpdate('new_media', { type: 'recording', id: mediaEntry.lastID }); } catch (error) { console.error('Stop recording error:', error); ws.send(JSON.stringify({ type: 'error', error: 'Failed to save recording' })); } } // Thumbnail Generation async function generateThumbnail(imagePath, type) { const thumbnailDir = path.join(CONFIG.mediaDir, 'thumbnails'); await fs.mkdir(thumbnailDir, { recursive: true }); const filename = path.basename(imagePath, path.extname(imagePath)); const thumbnailPath = path.join(thumbnailDir, `${filename}-thumb.png`); try { await sharp(imagePath) .resize(CONFIG.thumbnailSize.width, CONFIG.thumbnailSize.height, { fit: 'cover', position: 'center' }) .toFile(thumbnailPath); } catch (error) { // Create placeholder thumbnail on error await sharp({ create: { width: CONFIG.thumbnailSize.width, height: CONFIG.thumbnailSize.height, channels: 4, background: { r: 128, g: 128, b: 128, alpha: 1 } } }) .png() .toFile(thumbnailPath); } return thumbnailPath; } async function generateVideoThumbnail(videoPath) { const thumbnailDir = path.join(CONFIG.mediaDir, 'thumbnails'); await fs.mkdir(thumbnailDir, { recursive: true }); const filename = path.basename(videoPath, path.extname(videoPath)); const thumbnailPath = path.join(thumbnailDir, `${filename}-thumb.png`); // For now, create a placeholder thumbnail // In production, you'd extract a frame from the video await sharp({ create: { width: CONFIG.thumbnailSize.width, height: CONFIG.thumbnailSize.height, channels: 4, background: { r: 100, g: 100, b: 200, alpha: 1 } } }) .png() .toFile(thumbnailPath); return thumbnailPath; } // Annotation Handler async function handleAnnotation(ws, data) { try { const result = await db.run(` INSERT INTO annotations (media_id, type, data, created_at) VALUES (?, ?, ?, ?) `, [data.mediaId, data.type, JSON.stringify(data.data), Date.now()]); await db.run(` UPDATE media_history SET annotations = ?, updated_at = ? WHERE id = ? `, [JSON.stringify(data.data), Date.now(), data.mediaId]); ws.send(JSON.stringify({ type: 'annotation_saved', data: { id: result.lastID } })); broadcastUpdate('annotation_added', { mediaId: data.mediaId, annotationId: result.lastID }); } catch (error) { console.error('Annotation error:', error); ws.send(JSON.stringify({ type: 'error', error: 'Failed to save annotation' })); } } // Send Media History async function sendMediaHistory(ws, filters = {}) { try { let query = 'SELECT * FROM media_history WHERE 1=1'; const params = []; if (filters.type && filters.type !== 'all') { query += ' AND type = ?'; params.push(filters.type); } if (filters.project && filters.project !== 'all') { query += ' AND project = ?'; params.push(filters.project); } if (filters.dateFrom) { query += ' AND created_at >= ?'; params.push(new Date(filters.dateFrom).getTime()); } if (filters.dateTo) { query += ' AND created_at <= ?'; params.push(new Date(filters.dateTo).getTime() + 86400000); } if (filters.search) { query += ' AND (filename LIKE ? OR context LIKE ? OR tags LIKE ?)'; const searchTerm = `%${filters.search}%`; params.push(searchTerm, searchTerm, searchTerm); } query += ' ORDER BY created_at DESC LIMIT 100'; const media = await db.all(query, params); ws.send(JSON.stringify({ type: 'media_history', data: media })); } catch (error) { console.error('Get history error:', error); ws.send(JSON.stringify({ type: 'error', error: 'Failed to get media history' })); } } // Helper functions from original voice server async function determineIntent(text, context) { const lowerText = text.toLowerCase(); const intents = [ { patterns: ['review', 'check', 'analyze', 'look at'], tool: 'instant_review', extractArgs: (text, ctx) => ({ code: ctx.selectedCode || ctx.currentFunction, file_path: ctx.currentFile }) }, { patterns: ['find similar', 'similar code', 'patterns like'], tool: 'find_similar', extractArgs: (text, ctx) => ({ code: ctx.selectedCode || ctx.currentFunction, limit: 5 }) }, { patterns: ['explain', 'what does', 'how does', 'tell me about'], tool: 'analyze_symbol', extractArgs: (text, ctx) => ({ symbol_name: ctx.currentSymbol || extractSymbolFromText(text), include_usages: true }) }, { patterns: ['model', 'which model', 'capabilities', 'can you'], tool: 'get_current_model_status', extractArgs: () => ({}) } ]; for (const intent of intents) { if (intent.patterns.some(pattern => lowerText.includes(pattern))) { return { tool: intent.tool, arguments: intent.extractArgs(text, context) }; } } return { tool: 'search', arguments: { query: text, limit: 10 } }; } async function executeScsTool(toolName, args) { if (!scsMcpClient) { return { error: 'SCS-MCP not connected', fallback: 'I would help you with that request.' }; } try { const result = await scsMcpClient.callTool(toolName, args); return result; } catch (error) { return { error: error.message, fallback: 'I encountered an error processing your request.' }; } } async function generateResponse(result, intent) { if (result.error) { return { text: `I encountered an error: ${result.error}. ${result.fallback || ''}`, code: null }; } const content = result.content?.[0]?.text || JSON.stringify(result); return { text: content.split('\n').slice(0, 3).join(' '), code: content }; } async function generateSpeech(text, voice = 'rachel') { if (!elevenLabsClient) return null; try { const result = await elevenLabsClient.callTool('text_to_speech', { text: text, voice_name: voice, model_id: 'eleven_turbo_v2', output_format: 'mp3_44100_128' }); return result.content?.[0]?.data; } catch (error) { console.error('Speech generation failed:', error); return null; } } async function handleCommand(clientId, command, context) { await handleVoiceInput(clientId, command, context); } async function handleTextToSpeech(clientId, text, voice) { const audio = await generateSpeech(text, voice); if (audio) { sendToClient(clientId, { type: 'audio', data: audio }); } } async function handleMCPTool(ws, data) { try { const client = mcpClients.get(data.server || 'browser'); if (!client) throw new Error('MCP server not connected'); const result = await client.callTool(data.tool, data.params); ws.send(JSON.stringify({ type: 'mcp_result', data: result })); } catch (error) { console.error('MCP tool error:', error); ws.send(JSON.stringify({ type: 'error', error: `MCP tool failed: ${error.message}` })); } } function extractSymbolFromText(text) { const match = text.match(/(?:the|this)\s+(\w+)\s+(?:function|method|class|variable)/); return match ? match[1] : null; } function sendToClient(clientId, message) { const client = connections.get(clientId); if (client && client.ws.readyState === WebSocket.OPEN) { client.ws.send(JSON.stringify(message)); } } function broadcastToWebClients(message) { connections.forEach((client) => { if (client.type === 'web' && client.ws.readyState === WebSocket.OPEN) { client.ws.send(JSON.stringify(message)); } }); } function broadcastUpdate(type, data) { const message = JSON.stringify({ type, data }); connections.forEach((client) => { if (client.ws.readyState === WebSocket.OPEN) { client.ws.send(message); } }); } // REST API Endpoints // Get media history app.get('/api/media', async (req, res) => { try { const { type, project, from, to, search, limit = 100, offset = 0 } = req.query; let query = 'SELECT * FROM media_history WHERE 1=1'; const params = []; if (type) { query += ' AND type = ?'; params.push(type); } if (project) { query += ' AND project = ?'; params.push(project); } if (from) { query += ' AND created_at >= ?'; params.push(parseInt(from)); } if (to) { query += ' AND created_at <= ?'; params.push(parseInt(to)); } if (search) { query += ' AND (filename LIKE ? OR context LIKE ?)'; params.push(`%${search}%`, `%${search}%`); } query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; params.push(parseInt(limit), parseInt(offset)); const media = await db.all(query, params); res.json(media); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get sessions app.get('/api/sessions', async (req, res) => { try { const sessions = await db.all(` SELECT * FROM sessions ORDER BY start_time DESC LIMIT 50 `); res.json(sessions); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get analytics app.get('/api/analytics', async (req, res) => { try { const stats = await db.get(` SELECT COUNT(*) as total_media, COUNT(CASE WHEN type = 'screenshot' THEN 1 END) as screenshots, COUNT(CASE WHEN type = 'recording' THEN 1 END) as recordings, COUNT(CASE WHEN type = 'browser' THEN 1 END) as browser_captures, SUM(size) as total_size FROM media_history WHERE created_at > ? `, [Date.now() - 7 * 24 * 60 * 60 * 1000]); res.json({ stats, storageLimit: CONFIG.maxStorageGB * 1024 * 1024 * 1024 }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Upload recording app.post('/api/recordings', upload.single('recording'), async (req, res) => { try { const { context } = req.body; const file = req.file; // Generate thumbnail const thumbnailPath = await generateVideoThumbnail(file.path); // Mock metadata const metadata = { duration: 60, size: file.size, width: 1920, height: 1080 }; // Save to database const result = await db.run(` INSERT INTO media_history ( type, filename, thumbnail, url, duration, size, context, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ 'recording', file.filename, path.basename(thumbnailPath), `/media/recordings/${file.filename}`, metadata.duration, metadata.size, context, Date.now() ]); res.json({ id: result.lastID, url: `/media/recordings/${file.filename}`, thumbnail: `/media/thumbnails/${path.basename(thumbnailPath)}` }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete media app.delete('/api/media/:id', async (req, res) => { try { const { id } = req.params; const media = await db.get('SELECT * FROM media_history WHERE id = ?', [id]); if (!media) { return res.status(404).json({ error: 'Media not found' }); } // Delete files const mediaPath = path.join(CONFIG.mediaDir, media.type + 's', media.filename); const thumbnailPath = path.join(CONFIG.mediaDir, 'thumbnails', media.thumbnail); await fs.unlink(mediaPath).catch(() => {}); await fs.unlink(thumbnailPath).catch(() => {}); // Delete from database await db.run('DELETE FROM media_history WHERE id = ?', [id]); await db.run('DELETE FROM annotations WHERE media_id = ?', [id]); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Serve media files app.use('/media', express.static(CONFIG.mediaDir)); // Storage cleanup async function cleanupOldMedia() { const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; const oldMedia = await db.all( 'SELECT * FROM media_history WHERE created_at < ?', [cutoff] ); for (const media of oldMedia) { const mediaPath = path.join(CONFIG.mediaDir, media.type + 's', media.filename); const thumbnailPath = path.join(CONFIG.mediaDir, 'thumbnails', media.thumbnail); await fs.unlink(mediaPath).catch(() => {}); await fs.unlink(thumbnailPath).catch(() => {}); } await db.run('DELETE FROM media_history WHERE created_at < ?', [cutoff]); console.log(`Cleaned up ${oldMedia.length} old media files`); } // Start Server async function start() { // Initialize database await initDatabase(); // Initialize MCP clients await initializeMCPClients(); // Create media directories await fs.mkdir(CONFIG.mediaDir, { recursive: true }); await fs.mkdir(path.join(CONFIG.mediaDir, 'screenshots'), { recursive: true }); await fs.mkdir(path.join(CONFIG.mediaDir, 'recordings'), { recursive: true }); await fs.mkdir(path.join(CONFIG.mediaDir, 'thumbnails'), { recursive: true }); // Start cleanup scheduler setInterval(cleanupOldMedia, 24 * 60 * 60 * 1000); // Daily // Start HTTP server app.listen(CONFIG.port, () => { console.log(`✅ Media History Server running on port ${CONFIG.port}`); console.log(`✅ WebSocket server running on port ${CONFIG.wsPort}`); console.log(`📁 Media storage: ${CONFIG.mediaDir}`); console.log(`🌐 Open http://localhost:${CONFIG.port} to access the UI`); }); } start().catch(console.error); // Graceful shutdown process.on('SIGINT', async () => { console.log('\nShutting down...'); // Close WebSocket connections connections.forEach((client) => client.ws.close()); // Disconnect MCP clients for (const client of mcpClients.values()) { try { await client.close(); } catch (e) { // Ignore errors during shutdown } } // Close database if (db) await db.close(); process.exit(0); });

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/stevenjjobson/scs-mcp'

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