Skip to main content
Glama
cli.ts16 kB
#!/usr/bin/env node /* istanbul ignore file -- Point d'entrée CLI difficile à tester automatiquement */ import './utils/logger'; import express, { type NextFunction, type Request, type Response } from 'express'; import { randomUUID } from 'crypto'; import { constants as fsConstants, promises as fs } from 'fs'; import path from 'path'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { EnhancedStdioServerTransport } from './utils/platform-stdio'; import { PathUtils } from './utils/path-utils'; import { PlatformConfig } from './config'; import { createSmartThinkingServer } from './server/smart-thinking-server'; interface CliOptions { transport: 'stdio' | 'http' | 'sse' | 'stream'; port: number; host: string; allowOrigins: string[]; allowHosts: string[]; enableSse: boolean; enableStream: boolean; mode: 'full' | 'connector'; } type SessionTransport = StreamableHTTPServerTransport | SSEServerTransport; interface SessionState { transport: SessionTransport; serverClose: () => Promise<void>; type: 'stream' | 'sse'; } (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: string[]): CliOptions { const envMode = (process.env.SMART_THINKING_MODE ?? '').toLowerCase(); const defaultMode: 'full' | 'connector' = envMode === 'connector' ? 'connector' : 'full'; const options: CliOptions = { transport: 'stdio', port: process.env.PORT ? Number(process.env.PORT) : 3000, host: process.env.HOST ?? '0.0.0.0', allowOrigins: [], allowHosts: [], 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 = (): string | undefined => { 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; } return options; } async function startStdIoServer(options: CliOptions): Promise<void> { configureStdoutFiltering(); if (options.mode === 'connector') { console.error('Smart-Thinking: mode connecteur actif (outils search & fetch uniquement)'); } const { server } = createSmartThinkingServer(undefined, { includeSmartThinkingTool: options.mode !== 'connector' }); const transport = new EnhancedStdioServerTransport(); try { await server.connect(transport); } catch (error) { console.error('Smart-Thinking: échec de la connexion STDIO', error); throw error; } } async function startHttpServer(options: CliOptions): Promise<void> { const app = express(); app.use(express.json({ limit: '4mb' })); app.use(createCorsMiddleware(options)); if (options.mode === 'connector') { console.error('Smart-Thinking: mode connecteur actif (outils search & fetch uniquement)'); } const sessions = new Map<string, SessionState>(); const dnsProtectionEnabled = options.allowHosts.length > 0 || options.allowOrigins.length > 0; 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 as StreamableHTTPServerTransport).handleRequest(req, res, req.body); return; } if (req.method !== 'POST' || !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 } = createSmartThinkingServer(undefined, { includeSmartThinkingTool: options.mode !== 'connector' }); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => 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 } = createSmartThinkingServer(undefined, { includeSmartThinkingTool: options.mode !== 'connector' }); const transport = new 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): value is string => 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 as SSEServerTransport).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: unknown, _req: Request, res: Response, _next: NextFunction) => { if (!res.headersSent) { res.status(500).json({ error: 'Erreur interne du serveur MCP' }); } console.error('Smart-Thinking: middleware erreur', error); }); await new Promise<void>((resolve, reject) => { const httpServer = app.listen(options.port, options.host, () => { console.error(`Smart-Thinking: serveur MCP HTTP lancé sur http://${options.host}:${options.port}`); if (options.enableStream) { console.error(' • Endpoint streamable HTTP : /mcp'); } if (options.enableSse) { console.error(' • Endpoint SSE (legacy) : /sse + /messages'); } }); httpServer.on('error', reject); const shutdown = async () => { console.error('Smart-Thinking: arrêt du serveur HTTP en cours...'); for (const [sessionId] of sessions) { await cleanupSession(sessions, sessionId); } await new Promise<void>((resolveClose) => { httpServer.close(() => resolveClose()); }); resolve(); }; process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown); }); } function createCorsMiddleware(options: CliOptions) { return (req: Request, res: Response, next: NextFunction): void => { const originHeader = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin; res.header('Vary', 'Origin'); if (options.allowOrigins.length > 0) { if (originHeader && options.allowOrigins.includes(originHeader)) { res.header('Access-Control-Allow-Origin', originHeader); } } else if (originHeader) { res.header('Access-Control-Allow-Origin', originHeader); } else { res.header('Access-Control-Allow-Origin', '*'); } 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(); }; } async function cleanupSession( sessions: Map<string, SessionState>, sessionId?: string, skipTransportClose: boolean = false ): Promise<void> { 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(): void { const originalStdoutWrite = process.stdout.write.bind(process.stdout); function isValidJSON(str: string): boolean { 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: unknown, encoding?: BufferEncoding, cb?: (error?: Error | null) => void) { 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 }] } }) + (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 as any, encoding, cb); } as typeof process.stdout.write; } async function ensureDataDirExists(): Promise<void> { const dataDir = PathUtils.getDataDirectory(); try { await fs.mkdir(dataDir, { recursive: true }); if (PlatformConfig.IS_WINDOWS) { try { await fs.access(dataDir, fsConstants.W_OK); } catch { console.warn('Smart-Thinking: permissions limitées sur le répertoire data, utilisation possible d\'un fallback.'); } } } catch { if (PlatformConfig.IS_WINDOWS) { const altDataDir = path.join(process.env.USERPROFILE || '', 'Documents', 'Smart-Thinking', 'data'); try { await fs.mkdir(altDataDir, { recursive: true }); } catch { // Ignorer l'erreur, le gestionnaire de mémoire basculera en mémoire vive uniquement } } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Leghis/Smart-Thinking'

If you have feedback or need assistance with the MCP directory API, please join our Discord server