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
text/typescript
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;