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
JavaScript
/**
* 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);
});