feat(notes): vues structurées tableau/kanban, flashcards et MCP robuste
Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note) avec activation guidée, tableau éditable, kanban et suppression de colonnes. Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN. Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la robustesse du serveur MCP (config, validation, rate-limit, métriques). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Memento MCP Server - Stdio Transport
|
||||
* Memento MCP Server - Stdio Transport (Enhanced)
|
||||
*
|
||||
* Features:
|
||||
* - Structured error handling
|
||||
* - Configuration validation
|
||||
* - Observability metrics
|
||||
* - Input validation
|
||||
* - Better logging
|
||||
*
|
||||
* Environment:
|
||||
* DATABASE_URL Prisma database URL
|
||||
@@ -13,18 +20,53 @@ 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';
|
||||
|
||||
const LOG_LEVEL = process.env.MCP_LOG_LEVEL || 'info';
|
||||
const logLevels = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
const currentLogLevel = logLevels[LOG_LEVEL] ?? 1;
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 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) {
|
||||
console.error(`[${level.toUpperCase()}]`, ...args);
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}] [${level.toUpperCase()}]`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Database Setup
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
const databaseUrl = config.databaseUrl;
|
||||
if (!databaseUrl) {
|
||||
console.error('ERROR: DATABASE_URL is required');
|
||||
process.exit(1);
|
||||
@@ -33,24 +75,84 @@ if (!databaseUrl) {
|
||||
const isPostgres = databaseUrl.startsWith('postgresql://') || databaseUrl.startsWith('postgres://');
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: { url: isPostgres ? `${databaseUrl}${databaseUrl.includes('?') ? '&' : '?'}connection_limit=10&pool_timeout=10` : databaseUrl },
|
||||
},
|
||||
log: LOG_LEVEL === 'debug' ? ['query', 'info', 'warn', 'error'] : ['warn', 'error'],
|
||||
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: {} } },
|
||||
);
|
||||
|
||||
const appBaseUrl = process.env.APP_BASE_URL || 'http://localhost:3000';
|
||||
// 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: process.env.USER_ID || null,
|
||||
userId: config.userId || null,
|
||||
appBaseUrl,
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Main
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
@@ -62,9 +164,23 @@ async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
log('info', `Memento MCP Server v3.2.0 (stdio)`);
|
||||
log('info', `Database: ${isPostgres ? 'PostgreSQL' : 'SQLite'}`);
|
||||
log('info', `User filter: ${process.env.USER_ID || 'none'}`);
|
||||
// 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: 22
|
||||
`);
|
||||
}
|
||||
|
||||
log('info', 'MCP Server ready');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
@@ -72,6 +188,10 @@ main().catch((error) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Shutdown Handler
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async function shutdown() {
|
||||
log('info', 'Shutting down...');
|
||||
await prisma.$disconnect();
|
||||
@@ -80,5 +200,11 @@ async function shutdown() {
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('uncaughtException', (err) => log('error', 'Uncaught:', err.message));
|
||||
process.on('unhandledRejection', (reason) => log('error', 'Unhandled:', reason));
|
||||
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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user