UNPKG

mcp-oracle-server

Version:

MCP (Model Context Protocol) client for Oracle Database with tool discovery - Executable via NPX

888 lines (786 loc) 25.8 kB
#!/usr/bin/env node /** * MCP Oracle Server - Executável NPX * Servidor MCP para Oracle Database compatível com IDEs */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { readFileSync } from 'fs'; // Obter informações do package.json const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packagePath = join(__dirname, '..', 'package.json'); const packageInfo = JSON.parse(readFileSync(packagePath, 'utf8')); // Configuração do servidor const CONFIG = { serverUrl: process.env.MCP_ORACLE_URL || 'http://localhost:8080', timeout: parseInt(process.env.MCP_ORACLE_TIMEOUT) || 30000, maxRetries: 3, retryDelay: 1000 }; class MCPOracleServer { constructor() { this.server = new Server( { name: packageInfo.name, version: packageInfo.version, }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); this.httpClient = axios.create({ baseURL: CONFIG.serverUrl, timeout: CONFIG.timeout, headers: { 'Content-Type': 'application/json', }, }); } setupHandlers() { // Handler para listar ferramentas disponíveis this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'execute_sql', description: 'Executa consulta SQL no Oracle Database', inputSchema: { type: 'object', properties: { sql: { type: 'string', description: 'Consulta SQL para executar (apenas SELECT permitido)', }, params: { type: 'array', description: 'Parâmetros para a consulta SQL (opcional)', items: { type: 'string' } } }, required: ['sql'], }, }, { name: 'get_schemas', description: 'Lista schemas disponíveis no Oracle Database', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_tables', description: 'Lista tabelas de um schema específico', inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'Nome do schema (ex: HR, SCOTT)', } }, required: ['schema'], }, }, { name: 'describe_table', description: 'Descreve estrutura de uma tabela específica', inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'Nome do schema', }, table: { type: 'string', description: 'Nome da tabela', } }, required: ['schema', 'table'], }, }, { name: 'get_triggers', description: 'Lista triggers de um schema específico', inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'Nome do schema (ex: HR, SCOTT)', } }, required: ['schema'], }, }, { name: 'get_packages', description: 'Lista packages de um schema específico', inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'Nome do schema (ex: HR, SCOTT)', } }, required: ['schema'], }, }, { name: 'get_procedures', description: 'Lista procedures de um schema específico', inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'Nome do schema (ex: HR, SCOTT)', } }, required: ['schema'], }, }, { name: 'get_functions', description: 'Lista funções de um schema específico', inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'Nome do schema (ex: HR, SCOTT)', } }, required: ['schema'], }, }, { name: 'get_indexes', description: 'Lista índices de um schema específico', inputSchema: { type: 'object', properties: { schema: { type: 'string', description: 'Nome do schema (ex: HR, SCOTT)', } }, required: ['schema'], }, }, { name: 'health_check', description: 'Verifica status de saúde do servidor Oracle', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_metrics', description: 'Obtém métricas de performance do servidor', inputSchema: { type: 'object', properties: {}, }, } ], }; }); // Handler para executar ferramentas this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'execute_sql': return await this.executeSql(args.sql, args.params); case 'get_schemas': return await this.getSchemas(); case 'get_tables': return await this.getTables(args.schema); case 'describe_table': return await this.describeTable(args.schema, args.table); case 'get_triggers': return await this.getTriggers(args.schema); case 'get_packages': return await this.getPackages(args.schema); case 'get_procedures': return await this.getProcedures(args.schema); case 'get_functions': return await this.getFunctions(args.schema); case 'get_indexes': return await this.getIndexes(args.schema); case 'health_check': return await this.healthCheck(); case 'get_metrics': return await this.getMetrics(); default: throw new McpError( ErrorCode.MethodNotFound, `Ferramenta desconhecida: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Erro ao executar ${name}: ${error.message}` ); } }); } async executeSql(sql, params = []) { try { const response = await this.httpClient.post('/sql', { sql: sql, params: params }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro na consulta SQL:\n${result.error}` } ] }; } // Formata resultado para exibição let output = `✅ Consulta executada com sucesso\n`; output += `⏱️ Tempo de execução: ${result.execution_time.toFixed(3)}s\n`; if (result.cached) { output += `🔄 Resultado obtido do cache\n`; } output += `\n`; if (result.data && result.columns) { // Formata como tabela output += `📊 Resultados (${result.row_count} linhas):\n\n`; // Cabeçalhos const headers = result.columns.join(' | '); output += `| ${headers} |\n`; output += `|${result.columns.map(() => '---').join('|')}|\n`; // Dados (limita a 50 linhas para não sobrecarregar) const displayRows = result.data.slice(0, 50); for (const row of displayRows) { const formattedRow = row.map(cell => cell === null ? 'NULL' : String(cell) ).join(' | '); output += `| ${formattedRow} |\n`; } if (result.data.length > 50) { output += `\n... e mais ${result.data.length - 50} linhas\n`; } } else if (result.rows_affected !== undefined) { output += `📝 Linhas afetadas: ${result.rows_affected}\n`; } return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('execute_sql', error); } } async getSchemas() { try { const response = await this.httpClient.get('/schemas'); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao obter schemas: ${result.error}` } ] }; } const output = `📁 Schemas disponíveis:\n\n${result.schemas.map(schema => `• ${schema}`).join('\n')}`; return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_schemas', error); } } async getTables(schema) { try { const sql = `SELECT table_name FROM all_tables WHERE owner = '${schema.toUpperCase()}' ORDER BY table_name`; const response = await this.httpClient.post('/sql', { sql }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao obter tabelas: ${result.error}` } ] }; } if (!result.data || result.data.length === 0) { return { content: [ { type: 'text', text: `📋 Nenhuma tabela encontrada no schema '${schema}'` } ] }; } const tables = result.data.map(row => row[0]); const output = `📋 Tabelas no schema '${schema}' (${tables.length} encontradas):\n\n${tables.map(table => `• ${table}`).join('\n')}`; return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_tables', error); } } async describeTable(schema, table) { try { const sql = `SELECT column_name, data_type, nullable, data_default FROM all_tab_columns WHERE owner = '${schema.toUpperCase()}' AND table_name = '${table.toUpperCase()}' ORDER BY column_id`; const response = await this.httpClient.post('/sql', { sql }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao descrever tabela: ${result.error}` } ] }; } if (!result.data || result.data.length === 0) { return { content: [ { type: 'text', text: `📋 Tabela '${schema}.${table}' não encontrada` } ] }; } let output = `📋 Estrutura da tabela '${schema}.${table}':\n\n`; output += `| Coluna | Tipo | Nulo? | Padrão |\n`; output += `|--------|------|-------|--------|\n`; for (const row of result.data) { const [columnName, dataType, nullable, defaultValue] = row; const nullableText = nullable === 'Y' ? 'Sim' : 'Não'; const defaultText = defaultValue || '-'; output += `| ${columnName} | ${dataType} | ${nullableText} | ${defaultText} |\n`; } return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('describe_table', error); } } async getTriggers(schema) { try { const sql = `SELECT trigger_name, table_name, triggering_event, status FROM all_triggers WHERE owner = '${schema.toUpperCase()}' ORDER BY trigger_name`; const response = await this.httpClient.post('/sql', { sql }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao listar triggers: ${result.error}` } ] }; } if (!result.data || result.data.length === 0) { return { content: [ { type: 'text', text: `📋 Nenhum trigger encontrado no schema '${schema}'` } ] }; } let output = `📋 Triggers no schema '${schema}' (${result.data.length} encontrados):\n\n`; output += `| Nome | Tabela | Evento | Status |\n`; output += `|------|--------|--------|--------|\n`; for (const row of result.data) { const [triggerName, tableName, event, status] = row; output += `| ${triggerName} | ${tableName} | ${event} | ${status} |\n`; } return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_triggers', error); } } async getPackages(schema) { try { const sql = `SELECT object_name, status, created, last_ddl_time FROM all_objects WHERE owner = '${schema.toUpperCase()}' AND object_type = 'PACKAGE' ORDER BY object_name`; const response = await this.httpClient.post('/sql', { sql }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao listar packages: ${result.error}` } ] }; } if (!result.data || result.data.length === 0) { return { content: [ { type: 'text', text: `📋 Nenhum package encontrado no schema '${schema}'` } ] }; } let output = `📋 Packages no schema '${schema}' (${result.data.length} encontrados):\n\n`; output += `| Nome | Status | Criado | Última Modificação |\n`; output += `|------|--------|--------|-------------------|\n`; for (const row of result.data) { const [name, status, created, lastDdl] = row; const createdDate = new Date(created).toLocaleDateString('pt-BR'); const lastDdlDate = new Date(lastDdl).toLocaleDateString('pt-BR'); output += `| ${name} | ${status} | ${createdDate} | ${lastDdlDate} |\n`; } return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_packages', error); } } async getProcedures(schema) { try { const sql = `SELECT object_name, status, created, last_ddl_time FROM all_objects WHERE owner = '${schema.toUpperCase()}' AND object_type = 'PROCEDURE' ORDER BY object_name`; const response = await this.httpClient.post('/sql', { sql }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao listar procedures: ${result.error}` } ] }; } if (!result.data || result.data.length === 0) { return { content: [ { type: 'text', text: `📋 Nenhuma procedure encontrada no schema '${schema}'` } ] }; } let output = `📋 Procedures no schema '${schema}' (${result.data.length} encontradas):\n\n`; output += `| Nome | Status | Criado | Última Modificação |\n`; output += `|------|--------|--------|-------------------|\n`; for (const row of result.data) { const [name, status, created, lastDdl] = row; const createdDate = new Date(created).toLocaleDateString('pt-BR'); const lastDdlDate = new Date(lastDdl).toLocaleDateString('pt-BR'); output += `| ${name} | ${status} | ${createdDate} | ${lastDdlDate} |\n`; } return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_procedures', error); } } async getFunctions(schema) { try { const sql = `SELECT object_name, status, created, last_ddl_time FROM all_objects WHERE owner = '${schema.toUpperCase()}' AND object_type = 'FUNCTION' ORDER BY object_name`; const response = await this.httpClient.post('/sql', { sql }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao listar funções: ${result.error}` } ] }; } if (!result.data || result.data.length === 0) { return { content: [ { type: 'text', text: `📋 Nenhuma função encontrada no schema '${schema}'` } ] }; } let output = `📋 Funções no schema '${schema}' (${result.data.length} encontradas):\n\n`; output += `| Nome | Status | Criado | Última Modificação |\n`; output += `|------|--------|--------|-------------------|\n`; for (const row of result.data) { const [name, status, created, lastDdl] = row; const createdDate = new Date(created).toLocaleDateString('pt-BR'); const lastDdlDate = new Date(lastDdl).toLocaleDateString('pt-BR'); output += `| ${name} | ${status} | ${createdDate} | ${lastDdlDate} |\n`; } return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_functions', error); } } async getIndexes(schema) { try { const sql = `SELECT index_name, table_name, uniqueness, status FROM all_indexes WHERE owner = '${schema.toUpperCase()}' ORDER BY index_name`; const response = await this.httpClient.post('/sql', { sql }); const result = response.data; if (!result.success) { return { content: [ { type: 'text', text: `❌ Erro ao listar índices: ${result.error}` } ] }; } if (!result.data || result.data.length === 0) { return { content: [ { type: 'text', text: `📋 Nenhum índice encontrado no schema '${schema}'` } ] }; } let output = `📋 Índices no schema '${schema}' (${result.data.length} encontrados):\n\n`; output += `| Nome | Tabela | Único | Status |\n`; output += `|------|--------|-------|--------|\n`; for (const row of result.data) { const [indexName, tableName, uniqueness, status] = row; const uniqueText = uniqueness === 'UNIQUE' ? 'Sim' : 'Não'; output += `| ${indexName} | ${tableName} | ${uniqueText} | ${status} |\n`; } return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_indexes', error); } } async healthCheck() { try { const response = await this.httpClient.get('/health'); const result = response.data; const status = result.status === 'healthy' ? '✅' : '❌'; const uptime = Math.floor(result.uptime_seconds / 60); const output = `${status} Status do servidor Oracle MCP:\n\n` + `• Status: ${result.status}\n` + `• Uptime: ${uptime} minutos\n` + `• Pool de conexões: ${result.pool_status}\n` + `• Timestamp: ${result.timestamp}`; return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('health_check', error); } } async getMetrics() { try { const response = await this.httpClient.get('/metrics'); const result = response.data; const uptime = Math.floor(result.uptime_seconds / 60); const output = `📊 Métricas do servidor Oracle MCP:\n\n` + `• Uptime: ${uptime} minutos\n` + `• Requisições totais: ${result.requests_total}\n` + `• Requisições em cache: ${result.requests_cached}\n` + `• Requisições falhadas: ${result.requests_failed}\n` + `• Taxa de cache hit: ${result.cache_hit_rate.toFixed(1)}%\n` + `• Status do pool: ${result.pool_status}`; return { content: [ { type: 'text', text: output } ] }; } catch (error) { return this.handleError('get_metrics', error); } } handleError(operation, error) { let errorMessage = `❌ Erro na operação '${operation}':\n\n`; if (error.code === 'ECONNREFUSED') { errorMessage += `🔌 Não foi possível conectar ao servidor MCP Oracle em ${CONFIG.serverUrl}\n`; errorMessage += `Verifique se o servidor está rodando e acessível.`; } else if (error.code === 'ETIMEDOUT') { errorMessage += `⏰ Timeout na conexão com o servidor MCP Oracle\n`; errorMessage += `O servidor pode estar sobrecarregado ou indisponível.`; } else if (error.response) { errorMessage += `📡 Erro HTTP ${error.response.status}: ${error.response.statusText}\n`; if (error.response.data && error.response.data.error) { errorMessage += `Detalhes: ${error.response.data.error}`; } } else { errorMessage += `🐛 Erro interno: ${error.message}`; } return { content: [ { type: 'text', text: errorMessage } ] }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); // Log de inicialização (apenas para debug, não vai para o IDE) console.error(`MCP Oracle Server v${packageInfo.version} iniciado via stdio/pipes`); } } // Função principal async function main() { // Verificar argumentos de linha de comando if (process.argv.includes('--version') || process.argv.includes('-v')) { console.log(`${packageInfo.name} v${packageInfo.version}`); process.exit(0); } if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log(` ${packageInfo.name} v${packageInfo.version}`); console.log(packageInfo.description); console.log('\nUso:'); console.log(' npx mcp-oracle-server'); console.log(' mcp-oracle-server'); console.log('\nOpções:'); console.log(' -v, --version Mostra a versão'); console.log(' -h, --help Mostra esta ajuda'); console.log('\nVariáveis de ambiente:'); console.log(' MCP_ORACLE_URL URL do servidor Oracle (padrão: http://localhost:8080)'); console.log(' MCP_ORACLE_TIMEOUT Timeout em ms (padrão: 30000)'); process.exit(0); } const server = new MCPOracleServer(); await server.run(); } // Tratamento de erros process.on('uncaughtException', (error) => { console.error('Erro não capturado:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Promise rejeitada não tratada:', reason); process.exit(1); }); // Inicialização main().catch((error) => { console.error('Erro fatal no servidor MCP Oracle:', error); process.exit(1); });