UNPKG

git-memory-mcp-server

Version:

MCP Server for Git repository management with memory and AI capabilities

812 lines (716 loc) 33.4 kB
#!/usr/bin/env node /** * Git Memory Coordinator - ตัวประมวลแชร์ข้อมูลสำหรับระบบ 1000 MCP Servers * รองรับการแชร์ข้อมูลแบบ real-time ผ่าน Git-based memory system */ const fs = require('fs'); const path = require('path'); const { spawn, exec } = require('child_process'); const http = require('http'); class GitMemoryCoordinator { constructor() { this.servers = []; this.memoryStore = new Map(); this.gitMemoryPath = path.join(__dirname, '.git-memory'); this.coordinatorPort = 9000; this.healthCheckInterval = 30000; // 30 seconds this.webhookSubscriptions = new Map(); // key -> Set of webhook URLs // สร้างโฟลเดอร์ git-memory หากไม่มี if (!fs.existsSync(this.gitMemoryPath)) { fs.mkdirSync(this.gitMemoryPath, { recursive: true }); } this.initializeGitMemory(); this.startCoordinatorServer(); this.discoverServers(); this.startHealthCheck(); } /** * เข้ารหัสข้อมูลสำหรับข้อมูลส่วนตัว */ encryptData(data, password) { const crypto = require('crypto'); const algorithm = 'aes-256-cbc'; const key = crypto.scryptSync(password, 'salt', 32); const iv = crypto.randomBytes(16); const cipher = crypto.createCipher(algorithm, key); let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex'); encrypted += cipher.final('hex'); return { encrypted, iv: iv.toString('hex'), algorithm }; } /** * ถอดรหัสข้อมูลส่วนตัว */ decryptData(encryptedData, password) { const crypto = require('crypto'); const key = crypto.scryptSync(password, 'salt', 32); const decipher = crypto.createDecipher(encryptedData.algorithm, key); let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } /** * ส่ง webhook notifications */ async sendWebhookNotifications(key, data) { try { // ตรวจสอบ direct subscriptions if (this.webhookSubscriptions.has(key)) { const webhooks = this.webhookSubscriptions.get(key); for (const webhookUrl of webhooks) { try { const http = require('http'); const url = new URL(webhookUrl); const postData = JSON.stringify({ event: 'data_update', key, value: data.private ? '[ENCRYPTED]' : data.value, timestamp: data.timestamp, persist: data.persist, private: data.private }); const options = { hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Git-Memory-Event': 'data_update', 'Content-Length': Buffer.byteLength(postData) } }; const req = http.request(options); req.write(postData); req.end(); } catch (webhookError) { console.log(`⚠️ Webhook error for ${webhookUrl}: ${webhookError.message}`); } } } } catch (error) { console.log(`⚠️ Error sending webhook notifications: ${error.message}`); } } /** * เริ่มต้น Git Memory Repository */ async initializeGitMemory() { try { const gitDir = path.join(this.gitMemoryPath, '.git'); if (!fs.existsSync(gitDir)) { console.log('🔧 กำลังเริ่มต้น Git Memory Repository...'); await this.execCommand('git init', this.gitMemoryPath); await this.execCommand('git config user.name "Git Memory Coordinator"', this.gitMemoryPath); await this.execCommand('git config user.email "coordinator@git-memory.local"', this.gitMemoryPath); // สร้างไฟล์ README const readmePath = path.join(this.gitMemoryPath, 'README.md'); fs.writeFileSync(readmePath, '# Git Memory Shared Storage\n\nระบบแชร์ข้อมูลสำหรับ 1000 MCP Servers\n'); await this.execCommand('git add .', this.gitMemoryPath); await this.execCommand('git commit -m "Initial commit: Git Memory System"', this.gitMemoryPath); console.log('✅ Git Memory Repository เริ่มต้นเสร็จสิ้น'); } } catch (error) { console.error('❌ เกิดข้อผิดพลาดในการเริ่มต้น Git Memory:', error.message); } } /** * เริ่มต้น Coordinator Server */ startCoordinatorServer() { const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'application/json'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const url = new URL(req.url, `http://localhost:${this.coordinatorPort}`); const pathname = url.pathname; if (pathname === '/status') { this.handleStatusRequest(req, res); } else if (pathname === '/memory/set') { this.handleMemorySet(req, res); } else if (pathname === '/memory/get') { this.handleMemoryGet(req, res); } else if (pathname === '/memory/sync') { this.handleMemorySync(req, res); } else if (pathname === '/memory/webhook') { this.handleWebhookRequest(req, res); } else if (pathname === '/memory/broadcast') { this.handleBroadcastRequest(req, res); } else if (pathname === '/memory/notifications') { this.handleNotificationsRequest(req, res); } else if (pathname === '/servers') { this.handleServersRequest(req, res); } else { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); } }); server.listen(this.coordinatorPort, () => { console.log(`🚀 Git Memory Coordinator เริ่มทำงานที่พอร์ต ${this.coordinatorPort}`); console.log(`📊 Dashboard: http://localhost:${this.coordinatorPort}/status`); }); } /** * ค้นหา MCP Servers ที่กำลังทำงาน */ async discoverServers() { console.log('🔍 กำลังค้นหา MCP Servers...'); // เริ่มต้นใหม่ทุกครั้งเพื่อไม่ให้ซ้ำจากการเรียกซ้ำ this.servers = []; const collected = new Map(); // key: `${category}:${mcpPort}` หรือ id const addServer = (info) => { const key = info.category && info.mcpPort ? `${info.category}:${info.mcpPort}` : info.id; if (!collected.has(key)) collected.set(key, info); }; // 1) ค้นหาจากโฟลเดอร์ servers const serversDir = path.join(__dirname, 'servers'); if (fs.existsSync(serversDir)) { let discovered = []; // 1.1) รูปแบบเดิม: servers/gen-001, gen-002, ... try { const topLevel = fs.readdirSync(serversDir, { withFileTypes: true }) .filter(e => e.isDirectory() && /^gen-\d+$/.test(e.name)) .map(e => ({ id: e.name, path: path.join(serversDir, e.name) })); discovered.push(...topLevel); } catch (e) { console.warn('⚠️ อ่านโฟลเดอร์รูปแบบเดิมล้มเหลว:', e.message); } // 1.2) รูปแบบใหม่: servers/gen/001, 002, ... try { const genDir = path.join(serversDir, 'gen'); if (fs.existsSync(genDir)) { const children = fs.readdirSync(genDir, { withFileTypes: true }) .filter(e => e.isDirectory() && /^\d+$/.test(e.name)) .map(e => { const num = e.name.padStart(3, '0'); return { id: `gen-${num}`, path: path.join(genDir, e.name) }; }); discovered.push(...children); } } catch (e) { console.warn('⚠️ อ่านโฟลเดอร์รูปแบบใหม่ล้มเหลว:', e.message); } // เรียงตาม id เพื่อให้มีลำดับคงที่ discovered = discovered .filter((v, i, arr) => arr.findIndex(x => x.id === v.id) === i) .sort((a, b) => { const na = parseInt(a.id.replace('gen-', ''), 10); const nb = parseInt(b.id.replace('gen-', ''), 10); return na - nb; }); for (const item of discovered) { const { id, path: serverPath } = item; const serverNumber = parseInt(id.replace('gen-', ''), 10); const mcpPort = 3099 + serverNumber; // mcp: 3100+ const healthPort = mcpPort + 1000; // health: 4100+ addServer({ id, name: id, category: 'gen', healthPort, mcpPort, path: serverPath, status: 'unknown', lastSeen: null }); } } // 2) ค้นหาไฟล์สคริปต์เดี่ยวที่รากโปรเจค: mcp-server-<category>-<port>.js try { const rootFiles = fs.readdirSync(__dirname, { withFileTypes: true }) .filter(e => e.isFile() && /^mcp-server-[a-z0-9-]+-\d+\.js$/i.test(e.name)); for (const f of rootFiles) { const base = f.name.replace(/\.js$/i, ''); // ตัวอย่าง: mcp-server-api-3204 => ["mcp","server","api","3204"] const parts = base.split('-'); const portStr = parts.pop(); const category = parts.slice(2).join('-'); const mcpPort = parseInt(portStr, 10); if (!Number.isFinite(mcpPort)) continue; const healthPort = mcpPort + 1000; addServer({ id: `${category}-${mcpPort}`, name: base, category, healthPort, mcpPort, path: path.join(__dirname, f.name), status: 'unknown', lastSeen: null }); } } catch (e) { console.warn('⚠️ อ่านไฟล์สคริปต์รากโปรเจคล้มเหลว:', e.message); } // 3) เสริมข้อมูลจากไฟล์คอนฟิก (ถ้ามี): mcp-coordinator-config.json try { const cfgPath = path.join(__dirname, 'mcp-coordinator-config.json'); if (fs.existsSync(cfgPath)) { const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')); const list = Array.isArray(cfg.mcpServers) ? cfg.mcpServers : []; for (const s of list) { const category = s.category || 'uncategorized'; const mcpPort = s.port; if (!Number.isFinite(mcpPort)) continue; const healthPort = (s.healthPort && Number.isFinite(s.healthPort)) ? s.healthPort : (mcpPort + 1000); const id = s.id || `${category}-${mcpPort}`; const scriptPath = s.scriptPath || s.path; const entry = { id, name: s.name || id, category, healthPort, mcpPort, path: scriptPath || path.join(__dirname, `mcp-server-${category}-${mcpPort}.js`), status: 'unknown', lastSeen: null }; const key = `${category}:${mcpPort}`; if (!collected.has(key)) { collected.set(key, entry); } else { // ถ้ามีอยู่แล้ว อัปเดต path/name หากใน config ชัดเจนกว่า const curr = collected.get(key); if (scriptPath && (!curr.path || !fs.existsSync(curr.path))) curr.path = scriptPath; if (s.name && (!curr.name || curr.name === curr.id)) curr.name = s.name; } } } } catch (e) { console.warn('⚠️ อ่านคอนฟิกล้มเหลว:', e.message); } // รวมผลลัพธ์ this.servers = Array.from(collected.values()); // ตรวจสอบสถานะ server let activeServers = 0; for (const serverInfo of this.servers) { try { await this.checkServerHealth(serverInfo); if (serverInfo.status === 'active') activeServers++; } catch (_err) { serverInfo.status = 'inactive'; } } console.log(`✅ พบ ${this.servers.length} servers, ${activeServers} servers กำลังทำงาน`); } /** * ตรวจสอบสุขภาพของ server */ async checkServerHealth(serverInfo) { return new Promise((resolve, reject) => { const req = http.get(`http://localhost:${serverInfo.healthPort}/health`, (res) => { if (res.statusCode === 200) { serverInfo.status = 'active'; serverInfo.lastSeen = new Date(); resolve(true); } else { serverInfo.status = 'inactive'; reject(new Error(`Health check failed: ${res.statusCode}`)); } }); req.on('error', (error) => { serverInfo.status = 'inactive'; reject(error); }); req.setTimeout(5000, () => { req.destroy(); serverInfo.status = 'inactive'; reject(new Error('Health check timeout')); }); }); } /** * เริ่มต้นการตรวจสอบสุขภาพแบบ periodic */ startHealthCheck() { setInterval(async () => { console.log('🔍 กำลังตรวจสอบสุขภาพ servers...'); let activeCount = 0; for (const server of this.servers) { try { await this.checkServerHealth(server); if (server.status === 'active') { activeCount++; } } catch (error) { // Server is inactive } } console.log(`📊 Servers สถานะ: ${activeCount}/${this.servers.length} กำลังทำงาน`); }, this.healthCheckInterval); } /** * จัดการคำขอสถานะ */ handleStatusRequest(req, res) { const activeServers = this.servers.filter(s => s.status === 'active'); const inactiveServers = this.servers.filter(s => s.status === 'inactive'); const status = { coordinator: { status: 'running', port: this.coordinatorPort, uptime: process.uptime(), memory: process.memoryUsage() }, servers: { total: this.servers.length, active: activeServers.length, inactive: inactiveServers.length, list: this.servers.map(s => ({ id: s.id, name: s.name, status: s.status, healthPort: s.healthPort, mcpPort: s.mcpPort, lastSeen: s.lastSeen })) }, memory: { storeSize: this.memoryStore.size, gitPath: this.gitMemoryPath } }; res.writeHead(200); res.end(JSON.stringify(status, null, 2)); } /** * จัดการการตั้งค่าข้อมูลในหน่วยความจำ */ async handleMemorySet(req, res) { let body = ''; req.on('data', chunk => body += chunk); req.on('end', async () => { try { const { key, value, persist = false, private: isPrivate = false, password } = JSON.parse(body); if (!key) { res.writeHead(400); res.end(JSON.stringify({ error: 'Key is required' })); return; } if (isPrivate && !password) { res.writeHead(400); res.end(JSON.stringify({ error: 'Password is required for private data' })); return; } const dataToStore = { value: isPrivate ? this.encryptData(value, password) : value, timestamp: new Date(), persist, private: isPrivate, encrypted: isPrivate }; // เก็บในหน่วยความจำ this.memoryStore.set(key, dataToStore); // บันทึกลง Git หากต้องการ persist if (persist) { await this.persistToGit(key, dataToStore.value, isPrivate); } // ส่ง webhook notifications ถ้ามี subscribers await this.sendWebhookNotifications(key, dataToStore); res.writeHead(200); res.end(JSON.stringify({ success: true, key, persisted: persist, private: isPrivate })); console.log(`💾 บันทึกข้อมูล: ${key} ${persist ? '(persistent)' : '(memory only)'} ${isPrivate ? '(private)' : ''}`); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } }); } /** * จัดการการดึงข้อมูลจากหน่วยความจำ */ handleMemoryGet(req, res) { const url = new URL(req.url, `http://localhost:${this.coordinatorPort}`); const key = url.searchParams.get('key'); const password = url.searchParams.get('password'); if (!key) { res.writeHead(400); res.end(JSON.stringify({ error: 'Key parameter is required' })); return; } const data = this.memoryStore.get(key); if (data) { if (data.private && data.encrypted) { if (!password) { res.writeHead(401); res.end(JSON.stringify({ error: 'Password required for private data' })); return; } try { const decryptedValue = this.decryptData(data.value, password); res.writeHead(200); res.end(JSON.stringify({ success: true, key, value: decryptedValue, timestamp: data.timestamp, persist: data.persist, private: true })); return; } catch (error) { res.writeHead(401); res.end(JSON.stringify({ error: 'Invalid password' })); return; } } res.writeHead(200); res.end(JSON.stringify({ success: true, key, value: data.value, timestamp: data.timestamp, persist: data.persist, private: data.private || false })); } else { res.writeHead(404); res.end(JSON.stringify({ success: false, error: 'Key not found' })); } } /** * จัดการการซิงค์ข้อมูลจาก Git */ async handleMemorySync(req, res) { try { await this.syncFromGit(); res.writeHead(200); res.end(JSON.stringify({ success: true, message: 'Memory synced from Git', storeSize: this.memoryStore.size })); } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } } /** * จัดการคำขอรายการ servers */ handleServersRequest(req, res) { res.writeHead(200); res.end(JSON.stringify({ servers: this.servers, summary: { total: this.servers.length, active: this.servers.filter(s => s.status === 'active').length, inactive: this.servers.filter(s => s.status === 'inactive').length } }, null, 2)); } /** * จัดการ webhook subscriptions */ handleWebhookRequest(req, res) { if (req.method === 'POST') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { try { const { key, webhookUrl, action = 'subscribe' } = JSON.parse(body); if (action === 'subscribe') { if (!this.webhookSubscriptions.has(key)) { this.webhookSubscriptions.set(key, new Set()); } this.webhookSubscriptions.get(key).add(webhookUrl); res.writeHead(200); res.end(JSON.stringify({ success: true, message: `Subscribed to ${key}`, webhookUrl })); } else if (action === 'unsubscribe') { if (this.webhookSubscriptions.has(key)) { this.webhookSubscriptions.get(key).delete(webhookUrl); if (this.webhookSubscriptions.get(key).size === 0) { this.webhookSubscriptions.delete(key); } } res.writeHead(200); res.end(JSON.stringify({ success: true, message: `Unsubscribed from ${key}`, webhookUrl })); } } catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } }); } else { res.writeHead(405); res.end(JSON.stringify({ error: 'Method not allowed' })); } } /** * จัดการ broadcast messages */ handleBroadcastRequest(req, res) { if (req.method === 'GET') { const url = new URL(req.url, `http://localhost:${this.coordinatorPort}`); const since = url.searchParams.get('since'); const limit = parseInt(url.searchParams.get('limit') || '50'); const broadcasts = []; for (const [key, data] of this.memoryStore.entries()) { if (key.startsWith('broadcast_') && data.value && data.value.type === 'broadcast') { if (!since || new Date(data.timestamp) > new Date(since)) { broadcasts.push({ key, ...data.value, timestamp: data.timestamp }); } } } broadcasts.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); res.writeHead(200); res.end(JSON.stringify({ success: true, broadcasts: broadcasts.slice(0, limit), count: broadcasts.length })); } else { res.writeHead(405); res.end(JSON.stringify({ error: 'Method not allowed' })); } } /** * จัดการ notifications */ handleNotificationsRequest(req, res) { const url = new URL(req.url, `http://localhost:${this.coordinatorPort}`); const target = url.searchParams.get('target'); const since = url.searchParams.get('since'); const limit = parseInt(url.searchParams.get('limit') || '50'); const notifications = []; for (const [key, data] of this.memoryStore.entries()) { if (key.startsWith('notification_') && data.value && data.value.type === 'notification') { const notification = data.value; if (!target || notification.targets.includes('all') || notification.targets.includes(target)) { if (!since || new Date(data.timestamp) > new Date(since)) { notifications.push({ key, ...notification, timestamp: data.timestamp }); } } } } notifications.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); res.writeHead(200); res.end(JSON.stringify({ success: true, notifications: notifications.slice(0, limit), count: notifications.length })); } /** * บันทึกข้อมูลลง Git */ async persistToGit(key, value, isPrivate = false) { try { const dataPath = path.join(this.gitMemoryPath, 'data'); if (!fs.existsSync(dataPath)) { fs.mkdirSync(dataPath, { recursive: true }); } const filePath = path.join(dataPath, `${key}.json`); const data = { key, value, timestamp: new Date().toISOString(), coordinator: 'git-memory-coordinator', private: isPrivate, encrypted: isPrivate }; fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); await this.execCommand('git add .', this.gitMemoryPath); await this.execCommand(`git commit -m "Update: ${key}${isPrivate ? ' (private)' : ''}"`, this.gitMemoryPath); console.log(`📝 บันทึกข้อมูล ${key} ลง Git เรียบร้อย${isPrivate ? ' (private)' : ''}`); } catch (error) { console.error(`❌ เกิดข้อผิดพลาดในการบันทึก ${key}:`, error.message); throw error; } } /** * ซิงค์ข้อมูลจาก Git */ async syncFromGit() { try { const dataPath = path.join(this.gitMemoryPath, 'data'); if (!fs.existsSync(dataPath)) { return; } const files = fs.readdirSync(dataPath) .filter(file => file.endsWith('.json')); let syncedCount = 0; for (const file of files) { const filePath = path.join(dataPath, file); const content = fs.readFileSync(filePath, 'utf8'); const data = JSON.parse(content); this.memoryStore.set(data.key, { value: data.value, timestamp: new Date(data.timestamp), persist: true, private: data.private || false, encrypted: data.encrypted || false }); syncedCount++; } console.log(`🔄 ซิงค์ข้อมูล ${syncedCount} รายการจาก Git เรียบร้อย`); } catch (error) { console.error('❌ เกิดข้อผิดพลาดในการซิงค์จาก Git:', error.message); throw error; } } /** * รันคำสั่ง shell */ execCommand(command, cwd = process.cwd()) { return new Promise((resolve, reject) => { exec(command, { cwd }, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve(stdout.trim()); } }); }); } } // เริ่มต้น Git Memory Coordinator if (require.main === module) { console.log('🚀 เริ่มต้น Git Memory Coordinator...'); console.log('📊 ระบบแชร์ข้อมูลสำหรับ 1000 MCP Servers'); console.log('=' .repeat(50)); const coordinator = new GitMemoryCoordinator(); // จัดการการปิดโปรแกรม process.on('SIGINT', () => { console.log('\n🛑 กำลังปิด Git Memory Coordinator...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('\n🛑 กำลังปิด Git Memory Coordinator...'); process.exit(0); }); } module.exports = GitMemoryCoordinator;