UNPKG

smart-thinking-mcp

Version:

Un serveur MCP avancé pour le raisonnement multi-dimensionnel, adaptatif et collaboratif

650 lines 25.5 kB
#!/usr/bin/env node "use strict"; /* istanbul ignore file -- Point d'entrée CLI difficile à tester automatiquement */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); require("./utils/logger"); const express_1 = __importDefault(require("express")); const crypto_1 = require("crypto"); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js"); const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const platform_stdio_1 = require("./utils/platform-stdio"); const path_utils_1 = require("./utils/path-utils"); const config_1 = require("./config"); const smart_thinking_server_1 = require("./server/smart-thinking-server"); const PROJECT_HOMEPAGE = 'https://github.com/Leghis/Smart-Thinking'; const PROJECT_ICON = 'https://raw.githubusercontent.com/Leghis/Smart-Thinking/main/logoSmart-thinking.png'; function parseCsvEnv(name) { const raw = process.env[name]; if (!raw) { return []; } return raw.split(',').map(item => item.trim()).filter(Boolean); } function resolveDefaultAllowedHosts(host, port) { const hosts = new Set([ 'localhost', `localhost:${port}`, '127.0.0.1', `127.0.0.1:${port}` ]); if (host && host !== '0.0.0.0' && host !== '::') { hosts.add(host); hosts.add(`${host}:${port}`); } return Array.from(hosts); } function resolveDefaultAllowedOrigins(host, port) { const origins = new Set([ `http://localhost:${port}`, `http://127.0.0.1:${port}`, `https://localhost:${port}`, `https://127.0.0.1:${port}` ]); if (host && host !== '0.0.0.0' && host !== '::') { origins.add(`http://${host}:${port}`); origins.add(`https://${host}:${port}`); } return Array.from(origins); } function loadPackageVersion() { try { const packageJsonPath = path_1.default.join(__dirname, '..', 'package.json'); const raw = (0, fs_1.readFileSync)(packageJsonPath, 'utf8'); const parsed = JSON.parse(raw); return parsed.version ?? '0.0.0'; } catch { return '0.0.0'; } } function buildServerCard(options) { const version = loadPackageVersion(); const tools = [ { name: 'search', description: 'Recherche semantique dans les memoires locales Smart-Thinking.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Requete de recherche.' }, limit: { type: 'integer', minimum: 1, maximum: 20, default: 5, description: 'Nombre maximal de resultats.' }, sessionId: { type: 'string', description: 'Session cible optionnelle.' }, }, required: ['query'], }, }, { name: 'fetch', description: 'Recupere une memoire complete par identifiant.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Identifiant de la memoire.' }, sessionId: { type: 'string', description: 'Session cible optionnelle.' }, }, required: ['id'], }, }, ]; if (options.mode !== 'connector') { tools.unshift({ name: 'smartthinking', description: 'Pipeline de raisonnement graphe local, deterministe et persistant.', inputSchema: { type: 'object', properties: { thought: { type: 'string', description: 'Pensee a analyser.' }, thoughtType: { type: 'string', enum: ['regular', 'revision', 'meta', 'hypothesis', 'conclusion'], default: 'regular', description: 'Type de pensee.', }, sessionId: { type: 'string', description: 'Identifiant de session optionnel.' }, requestSuggestions: { type: 'boolean', default: false, description: 'Demande des suggestions d amelioration.' }, requestVerification: { type: 'boolean', default: false, description: 'Active la verification explicite.' }, containsCalculations: { type: 'boolean', default: false, description: 'Indique la presence de calculs.' }, generateVisualization: { type: 'boolean', default: false, description: 'Active la visualisation.' }, help: { type: 'boolean', default: false, description: 'Retourne le guide d utilisation.' }, }, }, }); } return { serverInfo: { name: 'smart-thinking-mcp', title: 'Smart-Thinking', version, websiteUrl: PROJECT_HOMEPAGE, icons: [ { src: PROJECT_ICON, mimeType: 'image/png', sizes: ['512x512'], }, ], }, tools, prompts: [ { name: 'smartthinking-reasoning-plan', description: 'Construit un plan de raisonnement testable avant execution.', arguments: [ { name: 'objective', required: true, description: 'Objectif principal.' }, { name: 'constraints', required: false, description: 'Contraintes.' }, { name: 'depth', required: false, description: 'Niveau de profondeur (fast|balanced|deep).' }, ], }, { name: 'smartthinking-verify-claim', description: 'Genere une checklist de verification factuelle.', arguments: [ { name: 'claim', required: true, description: 'Affirmation a verifier.' }, ], }, ], resources: [ { uri: 'smart-thinking://docs/about', name: 'smartthinking-about', description: 'Documentation du serveur.', }, { uri: 'smart-thinking://runtime/status', name: 'smartthinking-runtime-status', description: 'Etat runtime du serveur.', }, { uriTemplate: 'smart-thinking://sessions/{sessionId}/recent', name: 'smartthinking-session-recent', description: 'Memoires recentes par session.', }, { uriTemplate: 'smart-thinking://memories/{memoryId}', name: 'smartthinking-memory-by-id', description: 'Memoire complete par identifiant.', }, ], }; } (async () => { const options = parseArgs(process.argv.slice(2)); try { await ensureDataDirExists(); if (options.transport === 'stdio') { await startStdIoServer(options); } else { await startHttpServer(options); } } catch (error) { console.error('Smart-Thinking: échec du démarrage du serveur', error); process.exit(1); } })(); function parseArgs(argv) { const envMode = (process.env.SMART_THINKING_MODE ?? '').toLowerCase(); const defaultMode = envMode === 'connector' ? 'connector' : 'full'; const options = { transport: 'stdio', port: process.env.PORT ? Number(process.env.PORT) : 3000, host: process.env.HOST ?? '127.0.0.1', allowOrigins: parseCsvEnv('SMART_THINKING_ALLOWED_ORIGINS'), allowHosts: parseCsvEnv('SMART_THINKING_ALLOWED_HOSTS'), enableSse: true, enableStream: true, mode: defaultMode }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; const [flag, inlineValue] = arg.includes('=') ? arg.split('=', 2) : [arg, undefined]; const readValue = () => { if (inlineValue !== undefined) { return inlineValue; } const nextArg = argv[i + 1]; if (nextArg && !nextArg.startsWith('--')) { i += 1; return nextArg; } return undefined; }; switch (flag) { case '--transport': { const value = readValue(); if (value === 'stdio' || value === 'http' || value === 'sse' || value === 'stream') { options.transport = value; } break; } case '--port': { const value = readValue(); const port = value ? Number(value) : NaN; if (!Number.isNaN(port)) { options.port = port; } break; } case '--host': { const value = readValue(); if (value) { options.host = value; } break; } case '--allow-origin': { const value = readValue(); if (value) { options.allowOrigins.push(value); } break; } case '--allow-host': { const value = readValue(); if (value) { options.allowHosts.push(value); } break; } case '--disable-sse': options.enableSse = false; break; case '--disable-stream': options.enableStream = false; break; case '--mode': { const value = readValue(); if (value === 'full' || value === 'connector') { options.mode = value; } break; } default: break; } } if (options.transport === 'sse') { options.enableSse = true; options.enableStream = false; } else if (options.transport === 'stream') { options.enableSse = false; options.enableStream = true; } else if (options.transport === 'http') { options.enableSse = options.enableSse !== false; options.enableStream = options.enableStream !== false; } if (options.transport !== 'stdio' && !options.enableSse && !options.enableStream) { options.enableStream = true; } if (options.allowHosts.length === 0) { options.allowHosts = resolveDefaultAllowedHosts(options.host, options.port); } if (options.allowOrigins.length === 0) { options.allowOrigins = resolveDefaultAllowedOrigins(options.host, options.port); } return options; } async function startStdIoServer(options) { configureStdoutFiltering(); if (options.mode === 'connector') { console.info('Smart-Thinking: mode connecteur actif (outils search & fetch uniquement)'); } const { server } = (0, smart_thinking_server_1.createSmartThinkingServer)(undefined, { includeSmartThinkingTool: options.mode !== 'connector' }); const transport = new platform_stdio_1.EnhancedStdioServerTransport(); try { await server.connect(transport); } catch (error) { console.error('Smart-Thinking: échec de la connexion STDIO', error); throw error; } } async function startHttpServer(options) { const app = (0, express_1.default)(); app.use(express_1.default.json({ limit: '4mb' })); app.use(createHostValidationMiddleware(options)); app.use(createCorsMiddleware(options)); app.get('/.well-known/mcp/server-card.json', (_req, res) => { res.json(buildServerCard(options)); }); if (options.mode === 'connector') { console.info('Smart-Thinking: mode connecteur actif (outils search & fetch uniquement)'); } const sessions = new Map(); const dnsProtectionEnabled = true; if (options.enableStream) { app.all('/mcp', async (req, res) => { try { const sessionHeader = req.headers['mcp-session-id']; const sessionId = Array.isArray(sessionHeader) ? sessionHeader[0] : sessionHeader; if (sessionId) { const state = sessions.get(sessionId); if (!state || state.type !== 'stream') { res.status(404).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Session inconnue pour le transport streamable HTTP' }, id: null }); return; } await state.transport.handleRequest(req, res, req.body); return; } if (req.method !== 'POST' || !(0, types_js_1.isInitializeRequest)(req.body)) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Initialisation manquante ou requête invalide' }, id: null }); return; } const { server } = (0, smart_thinking_server_1.createSmartThinkingServer)(undefined, { includeSmartThinkingTool: options.mode !== 'connector' }); const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: () => (0, crypto_1.randomUUID)(), enableDnsRebindingProtection: dnsProtectionEnabled, allowedHosts: options.allowHosts.length ? options.allowHosts : undefined, allowedOrigins: options.allowOrigins.length ? options.allowOrigins : undefined, onsessioninitialized: (id) => { sessions.set(id, { transport, serverClose: () => server.close().catch(() => undefined), type: 'stream' }); }, onsessionclosed: (id) => { void cleanupSession(sessions, id, true); } }); transport.onerror = (error) => { console.error('Smart-Thinking: erreur transport streamable', error); }; transport.onclose = () => { void cleanupSession(sessions, transport.sessionId, true); }; await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Erreur interne du serveur' }, id: null }); } console.error('Smart-Thinking: erreur lors du traitement streamable HTTP', error); } }); } else { app.all('/mcp', (_req, res) => { res.status(404).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Transport streamable HTTP désactivé sur ce serveur' }, id: null }); }); } if (options.enableSse) { app.get('/sse', async (req, res) => { try { const { server } = (0, smart_thinking_server_1.createSmartThinkingServer)(undefined, { includeSmartThinkingTool: options.mode !== 'connector' }); const transport = new sse_js_1.SSEServerTransport('/messages', res, { enableDnsRebindingProtection: dnsProtectionEnabled, allowedHosts: options.allowHosts.length ? options.allowHosts : undefined, allowedOrigins: options.allowOrigins.length ? options.allowOrigins : undefined }); const sessionId = transport.sessionId; sessions.set(sessionId, { transport, serverClose: () => server.close().catch(() => undefined), type: 'sse' }); transport.onclose = () => { void cleanupSession(sessions, sessionId, true); }; transport.onerror = (error) => { console.error('Smart-Thinking: erreur transport SSE', error); }; await server.connect(transport); } catch (error) { if (!res.headersSent) { res.status(500).send('Erreur lors de l\'établissement du flux SSE'); } console.error('Smart-Thinking: échec d\'initialisation SSE', error); } }); app.post('/messages', async (req, res) => { try { const queryParam = req.query.sessionId; const sessionId = typeof queryParam === 'string' ? queryParam : Array.isArray(queryParam) ? queryParam.find((value) => typeof value === 'string') : undefined; if (!sessionId) { res.status(400).send('Paramètre sessionId manquant'); return; } const state = sessions.get(sessionId); if (!state || state.type !== 'sse') { res.status(404).send('Session SSE introuvable'); return; } await state.transport.handlePostMessage(req, res, req.body); } catch (error) { if (!res.headersSent) { res.status(500).send('Erreur traitement message SSE'); } console.error('Smart-Thinking: erreur sur /messages', error); } }); } else { app.get('/sse', (_req, res) => { res.status(404).send('Transport SSE désactivé'); }); app.post('/messages', (_req, res) => { res.status(404).send('Transport SSE désactivé'); }); } app.use((error, _req, res, _next) => { if (!res.headersSent) { res.status(500).json({ error: 'Erreur interne du serveur MCP' }); } console.error('Smart-Thinking: middleware erreur', error); }); await new Promise((resolve, reject) => { const httpServer = app.listen(options.port, options.host, () => { console.info(`Smart-Thinking: serveur MCP HTTP lance sur http://${options.host}:${options.port}`); if (options.enableStream) { console.info(' • Endpoint streamable HTTP : /mcp'); } if (options.enableSse) { console.info(' • Endpoint SSE (legacy) : /sse + /messages'); } }); httpServer.on('error', reject); const shutdown = async () => { console.info('Smart-Thinking: arret du serveur HTTP en cours...'); for (const [sessionId] of sessions) { await cleanupSession(sessions, sessionId); } await new Promise((resolveClose) => { httpServer.close(() => resolveClose()); }); resolve(); }; process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown); }); } function createCorsMiddleware(options) { return (req, res, next) => { const originHeader = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin; res.header('Vary', 'Origin'); if (originHeader) { if (!options.allowOrigins.includes(originHeader)) { res.status(403).json({ error: 'Origin non autorisee' }); return; } res.header('Access-Control-Allow-Origin', originHeader); } res.header('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-Id'); res.header('Access-Control-Expose-Headers', 'Mcp-Session-Id'); res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS'); if (req.method === 'OPTIONS') { res.status(204).end(); return; } next(); }; } function createHostValidationMiddleware(options) { const allowedHosts = new Set(options.allowHosts.map(host => host.toLowerCase())); return (req, res, next) => { const hostHeader = Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host; if (!hostHeader) { next(); return; } const normalizedHost = hostHeader.toLowerCase(); const hostOnly = normalizedHost.split(':')[0]; if (!allowedHosts.has(normalizedHost) && !allowedHosts.has(hostOnly)) { res.status(403).json({ error: 'Host non autorise' }); return; } next(); }; } async function cleanupSession(sessions, sessionId, skipTransportClose = false) { if (!sessionId) { return; } const state = sessions.get(sessionId); if (!state) { return; } sessions.delete(sessionId); try { if (!skipTransportClose) { await state.transport.close(); } } catch (error) { console.error(`Smart-Thinking: erreur fermeture transport ${sessionId}`, error); } try { await state.serverClose(); } catch (error) { console.error(`Smart-Thinking: erreur fermeture serveur ${sessionId}`, error); } } function configureStdoutFiltering() { const originalStdoutWrite = process.stdout.write.bind(process.stdout); function isValidJSON(str) { if (typeof str !== 'string') return false; const trimmed = str.trim(); if (!trimmed) return false; if (!(trimmed.startsWith('{') && trimmed.endsWith('}')) && !(trimmed.startsWith('[') && trimmed.endsWith(']'))) { return false; } try { JSON.parse(trimmed); return true; } catch { return false; } } process.stdout.write = function stdoutFilter(chunk, encoding, cb) { if (typeof chunk === 'string') { const trimmed = chunk.trim(); if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && !isValidJSON(trimmed)) { console.error('[ERREUR] JSON invalide détecté:', chunk); try { const safeMessage = JSON.stringify({ jsonrpc: '2.0', result: { content: [{ type: 'text', text: chunk }] } }) + (config_1.PlatformConfig.IS_WINDOWS ? '\n' : ''); return originalStdoutWrite(safeMessage, encoding, cb); } catch (error) { console.error('[ERREUR] Impossible de corriger le JSON:', error); process.stderr.write(chunk, encoding); if (cb) cb(); return true; } } if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) { process.stderr.write(chunk, encoding); if (cb) cb(); return true; } } return originalStdoutWrite(chunk, encoding, cb); }; } async function ensureDataDirExists() { const dataDir = path_utils_1.PathUtils.getDataDirectory(); try { await fs_1.promises.mkdir(dataDir, { recursive: true }); if (config_1.PlatformConfig.IS_WINDOWS) { try { await fs_1.promises.access(dataDir, fs_1.constants.W_OK); } catch { console.warn('Smart-Thinking: permissions limitées sur le répertoire data, utilisation possible d\'un fallback.'); } } } catch { if (config_1.PlatformConfig.IS_WINDOWS) { const altDataDir = path_1.default.join(process.env.USERPROFILE || '', 'Documents', 'Smart-Thinking', 'data'); try { await fs_1.promises.mkdir(altDataDir, { recursive: true }); } catch { // Ignorer l'erreur, le gestionnaire de mémoire basculera en mémoire vive uniquement } } } } //# sourceMappingURL=cli.js.map