Files
memory-multitenant/server.js
Emanuel Almeida 701e1518a2 Initial commit: Multi-tenant memory MCP server
- server.js: servidor MCP com suporte multi-tenant
- package.json + package-lock.json: Node.js dependencies (vulnerabilidades corrigidas)
- .gitignore: Node.js standard (node_modules, .env, logs)
- README.txt: documentacao basica
2026-03-04 19:59:13 +00:00

328 lines
12 KiB
JavaScript

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);