Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée. Co-authored-by: Cursor <cursoragent@cursor.com>
211 lines
7.9 KiB
JavaScript
211 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Memento MCP Server - Stdio Transport (Enhanced)
|
|
*
|
|
* Features:
|
|
* - Structured error handling
|
|
* - Configuration validation
|
|
* - Observability metrics
|
|
* - Input validation
|
|
* - Better logging
|
|
*
|
|
* Environment:
|
|
* DATABASE_URL Prisma database URL
|
|
* USER_ID Optional user ID filter
|
|
* APP_BASE_URL Next.js app URL (default: http://localhost:3000)
|
|
* MCP_LOG_LEVEL debug, info, warn, error (default: info)
|
|
*/
|
|
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import { registerTools } from './tools.js';
|
|
import config, { validateConfig, printConfig } from './config.js';
|
|
import {
|
|
mcpError,
|
|
mcpErrorContent,
|
|
McpErrors,
|
|
getErrorCategory,
|
|
withErrorHandling,
|
|
logError,
|
|
} from './errors.js';
|
|
import { recordRequest, recordError, recordDbQuery, getMetricsSummary } from './metrics.js';
|
|
import { validateAndSanitize } from './validation.js';
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Configuration Validation
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
const configErrors = validateConfig();
|
|
if (configErrors.some((e) => e.critical)) {
|
|
console.error('❌ CRITICAL CONFIGURATION ERRORS:');
|
|
configErrors.forEach((e) => console.error(` ${e.key}: ${e.message}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (configErrors.length > 0) {
|
|
console.warn('⚠️ Configuration warnings:');
|
|
configErrors.forEach((e) => console.warn(` ${e.key}: ${e.message}`));
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Logging
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
const logLevels = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
|
|
const currentLogLevel = logLevels[config.logLevel] ?? 1;
|
|
|
|
function log(level, ...args) {
|
|
if (logLevels[level] >= currentLogLevel) {
|
|
const timestamp = new Date().toISOString();
|
|
console.error(`[${timestamp}] [${level.toUpperCase()}]`, ...args);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Database Setup
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
const databaseUrl = config.databaseUrl;
|
|
if (!databaseUrl) {
|
|
console.error('ERROR: DATABASE_URL is required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const isPostgres = databaseUrl.startsWith('postgresql://') || databaseUrl.startsWith('postgres://');
|
|
|
|
const prisma = new PrismaClient({
|
|
datasources: { db: { url: databaseUrl } },
|
|
...(isPostgres
|
|
? {
|
|
datasources: {
|
|
db: {
|
|
url: `${databaseUrl}${databaseUrl.includes('?') ? '&' : '?'}connection_limit=${config.connectionLimit}&pool_timeout=${config.poolTimeout}`,
|
|
},
|
|
},
|
|
}
|
|
: {}),
|
|
log: config.logLevel === 'debug' ? ['query', 'info', 'warn', 'error'] : ['warn', 'error'],
|
|
});
|
|
|
|
// Wrap Prisma for metrics
|
|
const originalQuery = prisma.$queryRaw.bind(prisma);
|
|
prisma.$queryRaw = async (...args) => {
|
|
const start = Date.now();
|
|
try {
|
|
const result = await originalQuery(...args);
|
|
recordDbQuery(true, Date.now() - start);
|
|
return result;
|
|
} catch (error) {
|
|
recordDbQuery(false, Date.now() - start);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const appBaseUrl = config.appBaseUrl;
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// MCP Server Setup with Input Validation
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
const server = new Server(
|
|
{ name: 'memento-mcp-server', version: '3.2.0' },
|
|
{ capabilities: { tools: {} } },
|
|
);
|
|
|
|
// Wrap tool calls with validation
|
|
const originalCallTool = server.callTool.bind(server);
|
|
server.callTool = async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
|
|
// Validate input
|
|
const validation = validateAndSanitize(name, args || {});
|
|
if (!validation.success) {
|
|
log('warn', `Validation failed for ${name}:`, validation.errors);
|
|
return mcpErrorContent(McpErrors.INVALID_PARAMS.code, {
|
|
detail: 'Input validation failed',
|
|
field: validation.errors[0]?.field,
|
|
context: { toolName: name, errors: validation.errors },
|
|
});
|
|
}
|
|
|
|
// Record tool execution
|
|
const start = Date.now();
|
|
try {
|
|
const result = await originalCallTool(request);
|
|
recordRequest(name, 'success', 'stdio', Date.now() - start);
|
|
return result;
|
|
} catch (error) {
|
|
recordRequest(name, 'error', 'stdio', Date.now() - start);
|
|
recordError(getErrorCategory(error.code || McpErrors.INTERNAL_ERROR.code), error.code || 'unknown', {
|
|
tool: name,
|
|
});
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
registerTools(server, prisma, {
|
|
userId: config.userId || null,
|
|
appBaseUrl,
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Main
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
async function main() {
|
|
try {
|
|
await prisma.$queryRaw`SELECT 1`;
|
|
} catch (error) {
|
|
console.error('FATAL: Database connection failed:', error.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
|
|
// Print configuration to stderr (won't interfere with stdio protocol)
|
|
if (config.logLevel !== 'silent') {
|
|
console.error(`
|
|
╔═══════════════════════════════════════════════════════╗
|
|
║ Memento MCP Server v3.2.0 (Enhanced) ║
|
|
║ Stdio Transport ║
|
|
╚═══════════════════════════════════════════════════════╝
|
|
|
|
Mode: stdio
|
|
Database: ${isPostgres ? 'PostgreSQL' : 'SQLite'}
|
|
User: ${config.userId || 'all'}
|
|
Log Level: ${config.logLevel}
|
|
Tools: 29
|
|
`);
|
|
}
|
|
|
|
log('info', 'MCP Server ready');
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error('Server error:', error);
|
|
process.exit(1);
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Shutdown Handler
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
async function shutdown() {
|
|
log('info', 'Shutting down...');
|
|
await prisma.$disconnect();
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on('SIGINT', shutdown);
|
|
process.on('SIGTERM', shutdown);
|
|
process.on('uncaughtException', (err) => {
|
|
logError(log, err);
|
|
process.exit(1);
|
|
});
|
|
process.on('unhandledRejection', (reason) => {
|
|
log('error', 'Unhandled rejection:', reason);
|
|
recordError(getErrorCategory(McpErrors.INTERNAL_ERROR.code), 'unhandled_rejection');
|
|
});
|