- 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
328 lines
12 KiB
JavaScript
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); |