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

500 lines (437 loc) 18.1 kB
#!/usr/bin/env node import { Command } from 'commander'; import chalk from 'chalk'; import { configManager } from '@/core/config.js'; import { connectionManager } from '@/database/connection.js'; import { schemaCacheManager } from '@/database/schema-cache.js'; import { logger } from '@/core/logger.js'; const program = new Command(); program .name('sql-talk') .description('SQL Talk - Natural Language to SQL with MCP') .version('0.1.6'); // Test database connections program .command('test-connection') .description('Test database connections') .option('-c, --config <path>', 'Configuration file path') .action(async (options) => { try { console.log(chalk.blue('Testing database connections...')); configManager.loadConfig(options.config); await connectionManager.initialize(); console.log(chalk.green('✅ Database connections successful')); } catch (error) { console.error(chalk.red('❌ Database connection failed:'), error); process.exit(1); } finally { await connectionManager.close(); } }); // Refresh schema cache program .command('refresh-schema') .description('Refresh database schema cache') .option('-c, --config <path>', 'Configuration file path') .option('-s, --scope <scope>', 'Refresh scope (all|tables|columns)', 'all') .option('-t, --target <target>', 'Specific table target (schema.table)') .action(async (options) => { try { console.log(chalk.blue(`Refreshing schema cache (scope: ${options.scope})...`)); configManager.loadConfig(options.config); await connectionManager.initialize(); await schemaCacheManager.initialize(); const result = await schemaCacheManager.refresh(options.scope, options.target); console.log(chalk.green('✅ Schema refresh completed')); console.log(chalk.gray(` Added tables: ${result.added_tables.length}`)); console.log(chalk.gray(` Removed tables: ${result.removed_tables.length}`)); console.log(chalk.gray(` Updated columns: ${result.updated_columns}`)); if (result.added_tables.length > 0) { console.log(chalk.green(' Added:'), result.added_tables.join(', ')); } if (result.removed_tables.length > 0) { console.log(chalk.red(' Removed:'), result.removed_tables.join(', ')); } } catch (error) { console.error(chalk.red('❌ Schema refresh failed:'), error); process.exit(1); } finally { await connectionManager.close(); } }); // Show schema cache info program .command('show-schema') .description('Show cached schema information') .option('-c, --config <path>', 'Configuration file path') .option('-f, --filter <filter>', 'Filter tables (JSON format)') .action(async (options) => { try { configManager.loadConfig(options.config); await connectionManager.initialize(); await schemaCacheManager.initialize(); let filter; if (options.filter) { try { filter = JSON.parse(options.filter); } catch (error) { console.error(chalk.red('❌ Invalid filter JSON:'), error); process.exit(1); } } const tables = schemaCacheManager.filterTables(filter); const cache = schemaCacheManager.getCache(); // Import table formatter const { TableFormatter } = await import('@/utils/table-formatter.js'); console.log(chalk.blue('📊 Schema Cache Information / スキーマキャッシュ情報')); console.log(chalk.gray(`Engine: ${cache.engine}`)); console.log(chalk.gray(`Database: ${cache.db}`)); console.log(chalk.gray(`Last Updated: ${cache.last_updated}`)); console.log(); // テーブル形式でスキーマ情報を表示 const tableData = tables.map(table => ({ name: `${table.schema}.${table.table}`, columns: table.columns, comment: table.table_comment })); console.log(TableFormatter.formatSchemaInfo(tableData)); } catch (error) { console.error(chalk.red('❌ Failed to show schema:'), error); process.exit(1); } finally { await connectionManager.close(); } }); // Show table details program .command('describe-table') .description('Show detailed table information / テーブル詳細情報を表示') .option('-c, --config <path>', 'Configuration file path / 設定ファイルパス') .argument('<table>', 'Table name (schema.table or just table) / テーブル名') .action(async (tableName, options) => { try { configManager.loadConfig(options.config); await connectionManager.initialize(); await schemaCacheManager.initialize(); const { TableFormatter } = await import('@/utils/table-formatter.js'); // テーブルを検索 const tables = schemaCacheManager.filterTables(); const foundTable = tables.find(t => `${t.schema}.${t.table}` === tableName || t.table === tableName ); if (!foundTable) { console.log(TableFormatter.formatError( `Table "${tableName}" not found / テーブル "${tableName}" が見つかりません`, 'Available tables: ' + tables.map(t => `${t.schema}.${t.table}`).join(', ') )); process.exit(1); } // テーブル情報を表示 console.log(TableFormatter.formatColumnInfo( `${foundTable.schema}.${foundTable.table}`, foundTable.columns.map(col => ({ column_name: col.name, data_type: col.type, is_nullable: col.nullable, column_default: col.default, comment: col.comment })) )); // テーブルコメントがある場合は表示 if (foundTable.table_comment) { console.log(`\n${chalk.cyan('📝 テーブルコメント / Table Comment')}:`); console.log(chalk.gray(foundTable.table_comment)); } } catch (error) { const { TableFormatter } = await import('@/utils/table-formatter.js'); console.log(TableFormatter.formatError(String(error))); process.exit(1); } finally { await connectionManager.close(); } }); // Execute SQL with table output program .command('execute-sql') .description('Execute SQL query with table output / SQL実行(テーブル形式出力)') .option('-c, --config <path>', 'Configuration file path / 設定ファイルパス') .option('-f, --format <format>', 'Output format: json, table, both / 出力形式', 'table') .option('-l, --limit <limit>', 'Row limit / 行数制限', '100') .argument('<sql>', 'SQL query to execute / 実行するSQLクエリ') .action(async (sql, options) => { try { console.log(chalk.blue('📊 Executing SQL query / SQLクエリを実行中...')); console.log(chalk.gray(`Query: ${sql.substring(0, 100)}${sql.length > 100 ? '...' : ''}`)); console.log(); configManager.loadConfig(options.config); await connectionManager.initialize(); await schemaCacheManager.initialize(); // Import handler and formatter const { handleExecuteReadonly } = await import('@/tools/handlers.js'); const { TableFormatter } = await import('@/utils/table-formatter.js'); const startTime = Date.now(); // 疑似的なMCPリクエストを作成 const request = { method: 'tools/call' as const, params: { name: 'execute_readonly', arguments: { sql: sql, limit_override: parseInt(options.limit), output_format: options.format as 'json' | 'table' | 'both' } } }; const result = await handleExecuteReadonly(request); // 結果を表示 if (result.content && result.content[0]) { console.log(result.content[0].text); } } catch (error) { const { TableFormatter } = await import('@/utils/table-formatter.js'); console.log(TableFormatter.formatError(String(error), `SQL: ${sql}`)); process.exit(1); } finally { await connectionManager.close(); } }); // Start MCP server program .command('serve') .description('Start MCP server') .option('-c, --config <path>', 'Configuration file path') .action(async (options) => { try { // Set config path environment variable if provided if (options.config) { process.env.CONFIG_PATH = options.config; } // Import and start server const { MCPNLToSQLServer } = await import('./index.js'); const server = new MCPNLToSQLServer(); await server.start(); } catch (error) { console.error(chalk.red('❌ Failed to start server:'), error); process.exit(1); } }); // Generate example config program .command('init-config') .description('Generate example configuration file') .option('-o, --output <path>', 'Output path', 'mcp-db.config.yaml') .action(async (options) => { try { const { readFileSync, writeFileSync } = await import('fs'); const { resolve } = await import('path'); const examplePath = resolve(__dirname, '../../config/mcp-db.config.example.yaml'); const content = readFileSync(examplePath, 'utf-8'); writeFileSync(options.output, content, 'utf-8'); console.log(chalk.green(`✅ Configuration template created: ${options.output}`)); console.log(chalk.yellow('⚠️ Please edit the file and set your database credentials')); } catch (error) { console.error(chalk.red('❌ Failed to create config:'), error); process.exit(1); } }); // If no command is provided, start the MCP server if (!process.argv.slice(2).length) { // Set MCP mode to prevent logger from outputting to stdout process.env.MCP_MODE = 'true'; // Import and start the MCP server import('./index.js').then(async (module) => { // MCPサーバー起動時はログ出力しない(stdioモードのため) // logger.info('🚀 Starting SQL Talk MCP Server...'); // MCPサーバーを直接起動 const server = new module.MCPNLToSQLServer(); // Handle graceful shutdown const shutdown = async (signal: string): Promise<void> => { console.error(`シャットダウン / Shutting down (${signal})...`); await server.shutdown(); process.exit(0); }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); try { console.error('MCPサーバー開始 / MCP server starting...'); 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) => { 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; } let response; switch (request.method) { case 'initialize': console.error('MCPリクエスト受信 / MCP request received:', request.method, '(id:', request.id + ')'); 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': // Import the actual tools from the module const { allTools } = await import('./tools/mcp-tools.js'); response = { jsonrpc: '2.0', id: request.id, result: { tools: allTools } }; break; case 'prompts/list': response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } }; break; case 'resources/list': response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } }; break; case 'tools/call': try { // Import the tool handlers const { handleDescribeSchema, handleRefreshSchema, handleProposeSQL, handleLintSQL, handleExplainSQL, handleExecuteReadonly, handleProposeMissingComments, handleRenderCommentSQL, handleDryRunCommentSQL, handleApplyCommentSQL, handleUpdateConfig } = await import('./tools/handlers.js'); const toolName = request.params?.name; console.error('ツール呼び出し / Tool call:', toolName); // Ensure configuration is loaded before handling tool calls await server.ensureConfiguration(); let result; switch (toolName) { case 'describe_schema': result = await handleDescribeSchema(request); break; case 'refresh_schema': result = await handleRefreshSchema(request); break; case 'propose_sql': result = await handleProposeSQL(request); break; case 'lint_sql': result = await handleLintSQL(request); break; case 'explain_sql': result = await handleExplainSQL(request); break; case 'execute_readonly': result = await handleExecuteReadonly(request); break; case 'propose_missing_comments': result = await handleProposeMissingComments(request); break; case 'render_comment_sql': result = await handleRenderCommentSQL(request); break; case 'dry_run_comment_sql': result = await handleDryRunCommentSQL(request); break; case 'apply_comment_sql': result = await handleApplyCommentSQL(request); break; case 'update_config': result = await handleUpdateConfig(request); break; default: throw new Error(`Unknown tool: ${toolName}`); } response = { jsonrpc: '2.0', id: request.id, result }; } catch (error) { console.error('ツール実行エラー / Tool execution error:', error); response = { jsonrpc: '2.0', id: request.id, error: { code: -32603, message: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}` } }; } 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)); } }); // プロセスを継続するための無限待機 console.error('無限待機開始 / Starting infinite wait...'); await new Promise(() => {}); // 永続的に待機 } catch (error) { console.error('Failed to start server:', error); process.exit(1); } }).catch((error) => { // エラーはstderrに出力 console.error('❌ Failed to start MCP server:', error); process.exit(1); }); } else { // Parse command line arguments only if commands are provided program.parse(); } export default program;