UNPKG

sql-talk

Version:

SQL Talk - 自然言語をSQLに変換するMCPサーバー(安全性保護・SSHトンネル対応) / SQL Talk - MCP Server for Natural Language to SQL conversion with safety guards and SSH tunnel support

342 lines (289 loc) 13.2 kB
#!/usr/bin/env node import { logger } from '@/core/logger.js'; import { configManager } from '@/core/config.js'; import { connectionManager } from '@/database/connection.js'; import { schemaCacheManager } from '@/database/schema-cache.js'; import { auditLogger } from '@/core/audit.js'; import { allTools } from '@/tools/mcp-tools.js'; import { handleDescribeSchema, handleRefreshSchema, handleProposeSQL, handleLintSQL, handleExplainSQL, handleExecuteReadonly, handleProposeMissingComments, handleRenderCommentSQL, handleDryRunCommentSQL, handleApplyCommentSQL, handleUpdateConfig, } from '@/tools/handlers.js'; /** * MCP Server for Natural Language to SQL conversion */ class MCPNLToSQLServer { constructor() { // ContextOptimizerの実装を参考に、MCP SDKを使わずに直接実装 } async initialize(): Promise<void> { try { // MCPサーバー初期化時のログ出力は無効化(stdioモードのため) // logger.info('Initializing MCP NL-to-SQL Server...'); // Initialize audit logging first (doesn't require config) auditLogger.initialize(); // logger.info('Audit logging initialized'); // サーバー立ち上がりきったら設定ファイルを読み込む console.error('設定ファイルを読み込み中... / Loading configuration file...'); await this.ensureConfiguration(); console.error('設定ファイル読み込み完了 / Configuration loading complete'); // logger.info('MCP NL-to-SQL Server initialization complete'); } catch (error) { // エラーログをstderrに出力 console.error('Server initialization failed:', error); throw error; } } public async ensureConfiguration(): Promise<void> { // 設定が既に読み込まれている場合はスキップ try { configManager.getConfig(); return; // 設定が既に読み込まれている } catch (error) { // 設定が読み込まれていない場合は読み込みを試行 } try { // Load configuration (with improved workspace detection) configManager.loadConfig(); console.error('✅ 設定ファイル読み込み成功 / Configuration file loaded successfully'); } catch (error) { // 設定ファイルが見つからない場合はデフォルト設定を使用 console.error('❌ 設定ファイル読み込み失敗、デフォルト設定を使用 / Using default configuration due to config file error:', error instanceof Error ? error.message : String(error)); throw error; } // Initialize database connections const config = configManager.getConfig(); console.error('📋 設定内容確認 / Configuration details:'); console.error(' - engine:', config.engine); console.error(' - database:', config.connections.read_only.database); try { console.error('データベース接続を試行中... / Attempting database connection...'); await connectionManager.initialize(); console.error('✅ データベース接続成功 / Database connection successful'); // Initialize schema cache console.error('スキーマキャッシュを初期化中... / Initializing schema cache...'); await schemaCacheManager.initialize(); console.error('✅ スキーマキャッシュ初期化成功 / Schema cache initialization successful'); } catch (error) { console.error('❌ データベース接続失敗 / Database connection failed'); console.error('💡 設定を確認してください / Please check your configuration:'); console.error(' - ホスト・ポート設定 / Host & Port settings'); console.error(' - 認証情報 / Authentication credentials'); console.error(' - データベース存在確認 / Database existence'); console.error(' - 権限設定 / Permission settings'); console.error('🔧 設定ファイル / Config file: sql-talk.config.yaml'); console.error(''); console.error('詳細エラー / Error details:', error instanceof Error ? error.message : String(error)); throw new Error(`Database connection failed. Please check your configuration in sql-talk.config.yaml`); } } async start(): Promise<void> { await this.initialize(); // MCPサーバー起動時のログ出力は無効化(stdioモードのため) // logger.info('MCP NL-to-SQL Server started and ready for connections'); } async shutdown(): Promise<void> { try { // MCPサーバーはstdioモードで動作するため、ログ出力は無効化 // logger.info('Shutting down MCP NL-to-SQL Server...'); // Close database connections await connectionManager.close(); // MCPサーバーはstdioモードで動作するため、ログ出力は無効化 // logger.info('Server shutdown complete'); } catch (error) { // MCPサーバーはstdioモードで動作するため、ログ出力は無効化 // logger.error('Error during server shutdown:', error); } } } /** * Main entry point */ async function main(): Promise<void> { // Set MCP mode to prevent logger from outputting to stdout process.env.MCP_MODE = 'true'; console.error('MCPサーバー開始 / MCP server starting...'); const server = new MCPNLToSQLServer(); // Handle graceful shutdown const shutdown = async (signal: string): Promise<void> => { // MCPサーバーはstdioモードで動作するため、ログ出力は無効化 // logger.info(`Received ${signal}, shutting down gracefully...`); console.error(`シャットダウン / Shutting down (${signal})...`); await server.shutdown(); process.exit(0); }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); try { console.error('初期化開始 / Initializing...'); await server.start(); console.error('初期化完了 / Initialization complete'); // ContextOptimizerの実装を参考に、直接JSON-RPCメッセージを処理 console.error('stdin監視開始 / Starting stdin monitoring...'); process.stdin.on('data', async (data) => { console.error('データ受信 / Data received:', data.toString().trim()); try { const lines = data.toString().trim().split('\n'); for (const line of lines) { if (!line.trim()) continue; let request; try { request = JSON.parse(line); } catch (e) { console.error('JSON解析エラー / JSON parse error:', e instanceof Error ? e.message : String(e)); continue; } console.error('MCPリクエスト受信 / MCP request received:', request.method, `(id: ${request.id})`); let response; switch (request.method) { case 'initialize': response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: true }, resources: { subscribe: false, listChanged: false }, prompts: { listChanged: false } }, serverInfo: { name: 'sql-talk', version: '0.1.33', description: 'SQL Talk - Natural Language to SQL MCP Server' } } }; break; case 'notifications/initialized': console.error('MCP初期化完了 / MCP initialization completed'); continue; case 'tools/list': response = { jsonrpc: '2.0', id: request.id, result: { tools: allTools } }; break; case 'tools/call': console.error('ツール呼び出し / Tool call:', request.params.name, request.params.arguments); // ツール起動ログ const toolStartTime = Date.now(); console.error(`[TOOL] ${request.params.name} started`); try { // 初回ツール呼び出し時に設定ファイルを読み込む await server.ensureConfiguration(); // ツールハンドラーを呼び出し switch (request.params.name) { case 'describe_schema': response = await handleDescribeSchema(request); break; case 'refresh_schema': response = await handleRefreshSchema(request); break; case 'propose_sql': response = await handleProposeSQL(request); break; case 'lint_sql': response = await handleLintSQL(request); break; case 'explain_sql': response = await handleExplainSQL(request); break; case 'execute_readonly': response = await handleExecuteReadonly(request); break; case 'propose_missing_comments': response = await handleProposeMissingComments(request); break; case 'render_comment_sql': response = await handleRenderCommentSQL(request); break; case 'dry_run_comment_sql': response = await handleDryRunCommentSQL(request); break; case 'apply_comment_sql': response = await handleApplyCommentSQL(request); break; case 'update_config': response = await handleUpdateConfig(request); break; default: response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Unknown tool: ${request.params.name}` } }; } } catch (error) { response = { jsonrpc: '2.0', id: request.id, error: { code: -32603, message: `Internal error: ${error instanceof Error ? error.message : String(error)}` } }; } // ツール成果ログ const toolEndTime = Date.now(); const executionTime = toolEndTime - toolStartTime; if (response && typeof response === 'object' && 'result' in response) { console.error(`[TOOL] ${request.params.name} Results: Tool executed successfully (${executionTime}ms)`); } else if (response && typeof response === 'object' && 'error' in response) { const error = (response as any).error; if (error && typeof error === 'object' && 'message' in error) { console.error(`[TOOL] ${request.params.name} Error: ${error.message} (${executionTime}ms)`); } } break; default: response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Unknown method: ${request.method}` } }; } console.error('MCPレスポンス送信 / MCP response sent'); console.log(JSON.stringify(response)); } } catch (error) { console.error('エラー / Error:', error instanceof Error ? error.message : String(error)); } }); // プロセスを継続するための無限待機 // stdin.on('data')だけでは不十分な場合があるため console.error('無限待機開始 / Starting infinite wait...'); await new Promise(() => {}); // 永続的に待機 } catch (error) { // エラーログをstderrに出力 console.error('Failed to start server:', error); process.exit(1); } } // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { // エラーログをstderrに出力 console.error('Unhandled error:', error); process.exit(1); }); } export { MCPNLToSQLServer };