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