const express = require('express'); const cors = require('cors'); const axios = require('axios'); const fs = require('fs'); const path = require('path'); class MemoryMultiTenantWrapper { constructor() { this.app = express(); this.clients = this.loadClients(); this.ORIGINAL_SERVICE_URL = process.env.SUPABASE_URL || 'https://descomplicar-supabase.qibspu.easypanel.host'; // Direct Supabase connection this.setupMiddleware(); this.setupRoutes(); } loadClients() { try { const clientsPath = '/opt/mcp-servers/shared/clients.json'; if (fs.existsSync(clientsPath)) { return JSON.parse(fs.readFileSync(clientsPath, 'utf8')); } } catch (error) { console.error('Error loading clients:', error); } return { 'default-client': { name: 'Default Client', token: 'default-token-2025', created: new Date().toISOString(), disabled: false, config: {} } }; } setupMiddleware() { this.app.use(cors()); this.app.use(express.json({ limit: '10mb' })); // Logging middleware this.app.use((req, res, next) => { // console.log(`${new Date().toISOString()} ${req.method} ${req.path} - Client: ${req.headers['x-client-id'] || 'none'}`); next(); }); } setupRoutes() { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'healthy', service: 'memory-mcp-multitenant', mode: 'multi-tenant', version: '1.0.0', timestamp: new Date().toISOString(), clients: Object.keys(this.clients).length }); }); // Client info endpoint this.app.get('/client/info', this.authenticateClient.bind(this), (req, res) => { const { clientId } = req; res.json({ clientId, service: 'memory-mcp-multitenant', allowedTools: 'memory_search,memory_save,memory_get,memory_delete', rateLimit: { perMinute: '60', perHour: '1000', perDay: '10000' } }); }); // Memory operations with client isolation this.app.post('/client/memory/save', this.authenticateClient.bind(this), async (req, res) => { try { const { clientId } = req; const { content, metadata = {} } = req.body; // Add client isolation to metadata const clientMetadata = { ...metadata, clientId, tenant: clientId, created: new Date().toISOString() }; // Forward to original memory service with client context const response = await this.forwardToMemoryService('POST', '/memory/save', { content, metadata: clientMetadata }); res.json({ success: true, data: response.data }); } catch (error) { console.error('Memory save error:', error); res.status(500).json({ error: error.message }); } }); this.app.post('/client/memory/search', this.authenticateClient.bind(this), async (req, res) => { try { const { clientId } = req; const { query, limit = 10 } = req.body; // Search only within client's memories const response = await this.forwardToMemoryService('POST', '/memory/search', { query, limit, filter: { clientId } // Client isolation filter }); res.json({ success: true, data: response.data }); } catch (error) { console.error('Memory search error:', error); res.status(500).json({ error: error.message }); } }); this.app.get('/client/memory/:id', this.authenticateClient.bind(this), async (req, res) => { try { const { clientId } = req; const { id } = req.params; const response = await this.forwardToMemoryService('GET', `/memory/${id}`, null, { clientId // Verify ownership }); if (response.data && response.data.metadata?.clientId === clientId) { res.json({ success: true, data: response.data }); } else { res.status(404).json({ error: 'Memory not found or access denied' }); } } catch (error) { console.error('Memory get error:', error); res.status(500).json({ error: error.message }); } }); this.app.delete('/client/memory/:id', this.authenticateClient.bind(this), async (req, res) => { try { const { clientId } = req; const { id } = req.params; // Verify ownership before deletion const getResponse = await this.forwardToMemoryService('GET', `/memory/${id}`); if (getResponse.data?.metadata?.clientId !== clientId) { return res.status(403).json({ error: 'Access denied' }); } const response = await this.forwardToMemoryService('DELETE', `/memory/${id}`); res.json({ success: true, data: response.data }); } catch (error) { console.error('Memory delete error:', error); res.status(500).json({ error: error.message }); } }); this.app.get('/client/memory', this.authenticateClient.bind(this), async (req, res) => { try { const { clientId } = req; const { limit = 50, offset = 0 } = req.query; const response = await this.forwardToMemoryService('GET', '/memory', null, { limit: parseInt(limit), offset: parseInt(offset), filter: { clientId } // Client isolation }); res.json({ success: true, data: response.data }); } catch (error) { console.error('Memory list error:', error); res.status(500).json({ error: error.message }); } }); // SSE endpoint for MCP communication this.app.get('/sse', this.authenticateClient.bind(this), (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); const { clientId } = req; res.write(`data: ${JSON.stringify({ type: 'connected', clientId, service: 'memory-mcp-multitenant', timestamp: new Date().toISOString() })}\n\n`); // Keep connection alive const keepAlive = setInterval(() => { res.write(`data: ${JSON.stringify({ type: 'ping', timestamp: new Date().toISOString() })}\n\n`); }, 30000); req.on('close', () => { clearInterval(keepAlive); // console.log(`Client ${clientId} disconnected from memory SSE`); }); }); // Admin endpoints this.app.get('/admin/clients', this.authenticateAdmin.bind(this), (req, res) => { const clientStats = Object.entries(this.clients).map(([id, client]) => ({ id, name: client.name, created: client.created, disabled: client.disabled, memoryCount: 0 // TODO: Get actual count from memory service })); res.json({ clients: clientStats }); }); this.app.post('/admin/clients', this.authenticateAdmin.bind(this), (req, res) => { const { clientId, clientName, email } = req.body; if (!clientId || !clientName) { return res.status(400).json({ error: 'Missing required fields: clientId, clientName' }); } const apiKey = `sk-${clientId}-${crypto.randomBytes(32).toString('hex')}`; this.clients[clientId] = { name: clientName, email: email || '', token: apiKey, created: new Date().toISOString(), disabled: false, config: {} }; this.saveClients(); res.json({ success: true, client: { clientId, apiKey, clientName, email }, endpoints: { sse: `https://mcp-hub.descomplicar.pt/mcp/memory/sse`, info: `https://mcp-hub.descomplicar.pt/mcp/memory/client/info` } }); }); } async forwardToMemoryService(method, path, data = null, params = null) { const config = { method: method.toLowerCase(), url: `${this.ORIGINAL_SERVICE_URL}${path}`, headers: { 'Content-Type': 'application/json' }, timeout: 10000 }; if (data) { config.data = data; } if (params) { config.params = params; } return await axios(config); } authenticateClient(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); const clientId = req.headers['x-client-id']; if (!token || !clientId) { return res.status(401).json({ error: 'Missing authentication headers' }); } const client = this.clients[clientId]; if (!client || client.token !== token || client.disabled) { return res.status(401).json({ error: 'Invalid authentication' }); } req.clientId = clientId; req.client = client; next(); } authenticateAdmin(req, res, next) { const adminKey = req.headers['x-admin-key']; if (adminKey !== (process.env.ADMIN_KEY || 'admin-key-2025')) { return res.status(401).json({ error: 'Invalid admin credentials' }); } next(); } saveClients() { try { const clientsPath = '/opt/mcp-servers/shared/clients.json'; const dir = path.dirname(clientsPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(clientsPath, JSON.stringify(this.clients, null, 2)); } catch (error) { console.error('Error saving clients:', error); } } start(port = 3002) { this.app.listen(port, '0.0.0.0', () => { // console.log(`🧠 Memory MCP Multi-Tenant Wrapper running on port ${port}`); // console.log(`📋 Service: memory-mcp-multitenant`); // console.log(`🔗 Health: http://mcp-hub.descomplicar.pt:${port}/health`); // console.log(`🔐 SSE: http://mcp-hub.descomplicar.pt:${port}/sse (requires auth)`); // console.log(`⚙️ Admin: http://mcp-hub.descomplicar.pt:${port}/admin/clients`); // console.log(`🎯 Proxying to: ${this.ORIGINAL_SERVICE_URL}`); }); } } // Start the wrapper const wrapper = new MemoryMultiTenantWrapper(); wrapper.start(process.env.PORT || 3002);