import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import oracledb from 'oracledb'; import * as z from 'zod/v4'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ENV_FILE_PATH = resolveEnvFilePath(); loadEnvFile(ENV_FILE_PATH); const FETCH_ARRAY_SIZE = parsePositiveInt(process.env.ORACLE_FETCH_ARRAY_SIZE, 100); const DEFAULT_MAX_ROWS = parsePositiveInt(process.env.ORACLE_DEFAULT_MAX_ROWS, 200); const HARD_MAX_ROWS = parsePositiveInt(process.env.ORACLE_HARD_MAX_ROWS, 1000); const CALL_TIMEOUT_MS = parsePositiveInt(process.env.ORACLE_CALL_TIMEOUT_MS, 15000); const POOL_MIN = parseNonNegativeInt(process.env.ORACLE_POOL_MIN, 0); const POOL_MAX = parsePositiveInt(process.env.ORACLE_POOL_MAX, 4); const POOL_INCREMENT = parsePositiveInt(process.env.ORACLE_POOL_INCREMENT, 1); const POOL_TIMEOUT = parsePositiveInt(process.env.ORACLE_POOL_TIMEOUT, 60); const QUEUE_TIMEOUT_MS = parsePositiveInt(process.env.ORACLE_QUEUE_TIMEOUT_MS, 5000); const STMT_CACHE_SIZE = parsePositiveInt(process.env.ORACLE_STMT_CACHE_SIZE, 30); const ORACLE_BACKEND = normalizeBackendMode(process.env.ORACLE_BACKEND || 'auto'); const ORACLE_SQLCL_TIMEOUT_MS = parsePositiveInt(process.env.ORACLE_SQLCL_TIMEOUT_MS, CALL_TIMEOUT_MS + 5000); const ORACLE_SQLCL_PATH = resolveSqlclPath(process.env.ORACLE_SQLCL_PATH); oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT; oracledb.fetchAsString = [oracledb.CLOB]; oracledb.fetchArraySize = FETCH_ARRAY_SIZE; oracledb.prefetchRows = FETCH_ARRAY_SIZE; const runtimeConfig = buildRuntimeConfig(); const poolCache = new Map(); function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) { return; } const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split(/\r?\n/); for (let index = 0; index < lines.length; index += 1) { const rawLine = lines[index]; const line = rawLine.trim(); if (!line || line.charAt(0) == '#') { continue; } const separatorIndex = rawLine.indexOf('='); if (separatorIndex <= 0) { continue; } const key = rawLine.slice(0, separatorIndex).trim(); let value = rawLine.slice(separatorIndex + 1).trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } value = value.replace(/\\n/g, '\n'); if (process.env[key] === undefined) { process.env[key] = value; } } } function resolveEnvFilePath() { const explicitPath = String(process.env.MCP_ORACLE_ENV_FILE || process.env.ORACLE_ENV_FILE || '').trim(); const candidates = []; const visitedDirs = new Set(); if (explicitPath) { candidates.push(path.resolve(process.cwd(), explicitPath)); } collectSearchDirectories(process.cwd(), visitedDirs).forEach((searchDir) => { candidates.push(path.join(searchDir, '.env')); candidates.push(path.join(searchDir, 'mcp-oracle-custom', '.env')); }); candidates.push(path.join(__dirname, '.env')); for (let index = 0; index < candidates.length; index += 1) { const candidate = String(candidates[index] || '').trim(); if (candidate && fs.existsSync(candidate)) { return candidate; } } return explicitPath ? path.resolve(process.cwd(), explicitPath) : path.join(__dirname, '.env'); } function collectSearchDirectories(startDir, visitedDirs) { const directories = []; let currentDir = path.resolve(startDir || process.cwd()); while (currentDir && !visitedDirs.has(currentDir)) { visitedDirs.add(currentDir); directories.push(currentDir); const parentDir = path.dirname(currentDir); if (parentDir == currentDir) { break; } currentDir = parentDir; } return directories; } function parsePositiveInt(value, fallbackValue) { const parsed = Number.parseInt(String(value || ''), 10); if (!Number.isFinite(parsed) || parsed <= 0) { return fallbackValue; } return parsed; } function parseNonNegativeInt(value, fallbackValue) { const parsed = Number.parseInt(String(value || ''), 10); if (!Number.isFinite(parsed) || parsed < 0) { return fallbackValue; } return parsed; } function normalizeBackendMode(value) { const normalized = String(value || 'auto').trim().toLowerCase(); if (normalized == 'oracledb' || normalized == 'sqlcl') { return normalized; } return 'auto'; } function resolveSqlclPath(explicitPath) { const candidates = []; const normalizedExplicitPath = String(explicitPath || '').trim(); if (normalizedExplicitPath) { candidates.push(normalizedExplicitPath); } collectVsCodeExtensionDirs().forEach((extensionsDir) => { if (!fs.existsSync(extensionsDir)) { return; } const extensionNames = fs.readdirSync(extensionsDir) .filter((entry) => /^oracle\.sql-developer-.*-(linux-x64|win32-x64|darwin-(x64|arm64))$/.test(entry)) .sort() .reverse(); extensionNames.forEach((entry) => { candidates.push(path.join(extensionsDir, entry, 'dbtools', 'sqlcl', 'bin', process.platform == 'win32' ? 'sql.exe' : 'sql')); }); }); const defaultCandidates = [ '/usr/local/bin/sql', '/usr/bin/sql', path.join(os.homedir(), '.local', 'bin', 'sql') ]; defaultCandidates.forEach((candidate) => { candidates.push(candidate); }); for (let index = 0; index < candidates.length; index += 1) { const candidate = String(candidates[index] || '').trim(); if (candidate && fs.existsSync(candidate)) { return candidate; } } return normalizedExplicitPath; } function collectVsCodeExtensionDirs() { const homeDir = os.homedir(); const baseDirs = []; const uniqueDirs = new Set(); if (process.env.VSCODE_AGENT_FOLDER) { baseDirs.push(process.env.VSCODE_AGENT_FOLDER); } if (homeDir) { baseDirs.push( path.join(homeDir, '.vscode-server'), path.join(homeDir, '.vscode-server-insiders'), path.join(homeDir, '.vscode'), path.join(homeDir, '.vscode-insiders') ); } return baseDirs.reduce((directories, baseDir) => { const normalizedBaseDir = String(baseDir || '').trim(); if (!normalizedBaseDir) { return directories; } const extensionsDir = path.join(normalizedBaseDir, 'extensions'); if (!uniqueDirs.has(extensionsDir)) { uniqueDirs.add(extensionsDir); directories.push(extensionsDir); } return directories; }, []); } function isSqlclAvailable() { return Boolean(ORACLE_SQLCL_PATH) && fs.existsSync(ORACLE_SQLCL_PATH); } function buildRuntimeConfig() { const connections = {}; let defaultConnection = String(process.env.ORACLE_DEFAULT_CONNECTION || '').trim(); if (process.env.ORACLE_CONNECTIONS_JSON) { let jsonConfig; try { jsonConfig = JSON.parse(process.env.ORACLE_CONNECTIONS_JSON); } catch (error) { throw new Error('ORACLE_CONNECTIONS_JSON invalido: ' + String(error)); } Object.keys(jsonConfig || {}).forEach((connectionName) => { const row = jsonConfig[connectionName] || {}; const normalized = normalizeConnectionConfig(connectionName, row); connections[normalized.name] = normalized; }); } if (process.env.ORACLE_USER && process.env.ORACLE_PASSWORD && process.env.ORACLE_CONNECT_STRING) { const singleName = String(process.env.ORACLE_CONNECTION_NAME || defaultConnection || 'default').trim(); const normalized = normalizeConnectionConfig(singleName, { user: process.env.ORACLE_USER, password: process.env.ORACLE_PASSWORD, connectString: process.env.ORACLE_CONNECT_STRING }); connections[normalized.name] = normalized; if (!defaultConnection) { defaultConnection = normalized.name; } } const connectionNames = Object.keys(connections).sort(); if (!defaultConnection && connectionNames.length > 0) { defaultConnection = connectionNames[0]; } return { connections, defaultConnection, connectionNames }; } function normalizeConnectionConfig(connectionName, config) { const normalizedName = String(connectionName || '').trim(); const user = String(config.user || '').trim(); const password = String(config.password || '').trim(); const connectString = String(config.connectString || config.connect_string || '').trim(); if (!normalizedName || !user || !password || !connectString) { throw new Error('Configuracao de conexao invalida para ' + String(connectionName || '') + '.'); } return { name: normalizedName, user, password, connectString }; } function getConnectionConfig(connectionName) { const requested = String(connectionName || runtimeConfig.defaultConnection || '').trim(); if (!requested) { throw new Error('Nenhuma conexao configurada. Preencha o arquivo .env do MCP Oracle.'); } const config = runtimeConfig.connections[requested]; if (!config) { throw new Error('Conexao nao encontrada: ' + requested + '. Conexoes disponiveis: ' + runtimeConfig.connectionNames.join(', ')); } return config; } async function getPool(connectionName) { const config = getConnectionConfig(connectionName); if (poolCache.has(config.name)) { return poolCache.get(config.name); } const pool = await oracledb.createPool({ user: config.user, password: config.password, connectString: config.connectString, poolMin: POOL_MIN, poolMax: POOL_MAX, poolIncrement: POOL_INCREMENT, poolTimeout: POOL_TIMEOUT, queueTimeout: QUEUE_TIMEOUT_MS, stmtCacheSize: STMT_CACHE_SIZE, homogeneous: true }); poolCache.set(config.name, pool); return pool; } async function withConnection(connectionName, callback) { const config = getConnectionConfig(connectionName); const pool = await getPool(config.name); let connection; try { connection = await pool.getConnection(); connection.callTimeout = CALL_TIMEOUT_MS; return await callback(connection, config); } finally { if (connection) { try { await connection.close(); } catch (error) { console.error('Falha ao fechar conexao Oracle:', error); } } } } function stripLeadingComments(sql) { let cleaned = String(sql || ''); let changed = true; while (changed) { changed = false; cleaned = cleaned.replace(/^\s+/, ''); if (cleaned.startsWith('--')) { const newLineIndex = cleaned.indexOf('\n'); cleaned = newLineIndex >= 0 ? cleaned.slice(newLineIndex + 1) : ''; changed = true; continue; } if (cleaned.startsWith('/*')) { const endIndex = cleaned.indexOf('*/'); cleaned = endIndex >= 0 ? cleaned.slice(endIndex + 2) : ''; changed = true; } } return cleaned.trim(); } function validateReadOnlySql(sql) { const original = String(sql || '').trim(); if (!original) { throw new Error('Informe um SQL para executar.'); } if (original.indexOf(';') >= 0) { throw new Error('Envie apenas uma consulta por vez e sem ponto e virgula.'); } const cleaned = stripLeadingComments(original); if (!/^(select|with)\b/i.test(cleaned)) { throw new Error('Este MCP aceita apenas consultas de leitura iniciando com SELECT ou WITH.'); } const withoutStrings = cleaned.replace(/'([^']|'')*'/g, "''"); if (/\b(insert|update|delete|merge|alter|drop|truncate|grant|revoke|commit|rollback|begin|declare|execute|call)\b/i.test(withoutStrings)) { throw new Error('Foi detectado um comando nao permitido para este MCP de leitura.'); } return cleaned; } function parseBindsJson(bindsJson) { if (bindsJson === undefined || bindsJson === null || String(bindsJson).trim() == '') { return {}; } let parsed; try { parsed = JSON.parse(bindsJson); } catch (error) { throw new Error('bindsJson invalido: ' + String(error)); } if (Array.isArray(parsed)) { return parsed; } if (parsed && typeof parsed == 'object') { return parsed; } throw new Error('bindsJson deve ser um objeto JSON ou um array JSON.'); } function serializeValue(value) { if (value === null || value === undefined) { return null; } if (value instanceof Date) { return value.toISOString(); } if (Buffer.isBuffer(value)) { return value.toString('base64'); } if (Array.isArray(value)) { return value.map((item) => serializeValue(item)); } if (typeof value == 'bigint') { return String(value); } if (typeof value == 'object') { const serialized = {}; Object.keys(value).forEach((key) => { serialized[key] = serializeValue(value[key]); }); return serialized; } return value; } function stripAnsi(text) { return String(text || '').replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, ''); } function extractJsonPayload(text) { const source = stripAnsi(text); let startIndex = -1; let openingChar = ''; for (let index = 0; index < source.length; index += 1) { const current = source.charAt(index); if (current == '{' || current == '[') { startIndex = index; openingChar = current; break; } } if (startIndex < 0) { throw new Error('Nao foi possivel localizar o payload JSON retornado pelo SQLcl.'); } const closingChar = openingChar == '{' ? '}' : ']'; let depth = 0; let inString = false; let escaped = false; for (let index = startIndex; index < source.length; index += 1) { const current = source.charAt(index); if (inString) { if (escaped) { escaped = false; continue; } if (current == '\\') { escaped = true; continue; } if (current == '"') { inString = false; } continue; } if (current == '"') { inString = true; continue; } if (current == openingChar) { depth += 1; continue; } if (current == closingChar) { depth -= 1; if (depth == 0) { return source.slice(startIndex, index + 1); } } } throw new Error('O payload JSON retornado pelo SQLcl parece incompleto.'); } function parseJsonStrictOrLenient(payloadText) { const payload = String(payloadText || '').trim(); try { return JSON.parse(payload); } catch (strictError) { // SQLcl pode retornar JSON quase valido com chaves sem aspas e/ou virgula final. const withoutTrailingCommas = payload.replace(/,\s*([}\]])/g, '$1'); const withQuotedKeys = withoutTrailingCommas.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_$]*)(\s*:)/g, '$1"$2"$3'); const withNormalizedNumbers = normalizeJsonLocaleNumbers(withQuotedKeys); try { return JSON.parse(withNormalizedNumbers); } catch (lenientError) { throw strictError; } } } function isDigitCharacter(value) { const code = String(value || '').charCodeAt(0); return code >= 48 && code <= 57; } function isLooseJsonNumberStart(text, index) { const current = text.charAt(index); if (isDigitCharacter(current)) { return true; } return (current == '-' || current == '+') && isDigitCharacter(text.charAt(index + 1)); } function readLooseJsonNumberToken(text, startIndex) { let token = ''; let index = startIndex; while (index < text.length) { const current = text.charAt(index); const next = text.charAt(index + 1); const afterNext = text.charAt(index + 2); if (isDigitCharacter(current)) { token += current; index += 1; continue; } if ((current == '+' || current == '-') && token && /[eE]$/.test(token)) { token += current; index += 1; continue; } if ((current == 'e' || current == 'E') && token && (isDigitCharacter(next) || ((next == '+' || next == '-') && isDigitCharacter(afterNext)))) { token += current; index += 1; continue; } if ((current == '.' || current == ',') && isDigitCharacter(next)) { token += current; index += 1; continue; } break; } return { token, endIndex: index }; } function normalizeLooseJsonNumberToken(token) { let normalized = String(token || ''); if (normalized.indexOf(',') < 0) { return normalized; } let exponent = ''; const exponentMatch = normalized.match(/[eE][+-]?\d+$/); if (exponentMatch) { exponent = exponentMatch[0]; normalized = normalized.slice(0, -exponent.length); } let sign = ''; if (normalized.charAt(0) == '-' || normalized.charAt(0) == '+') { sign = normalized.charAt(0); normalized = normalized.slice(1); } if (normalized.indexOf('.') >= 0) { normalized = normalized.replace(/\./g, ''); } const lastCommaIndex = normalized.lastIndexOf(','); normalized = normalized.slice(0, lastCommaIndex).replace(/,/g, '') + '.' + normalized.slice(lastCommaIndex + 1); return sign + normalized + exponent; } function normalizeJsonLocaleNumbers(text) { let normalized = ''; let inString = false; let escaped = false; for (let index = 0; index < text.length; index += 1) { const current = text.charAt(index); if (inString) { normalized += current; if (escaped) { escaped = false; continue; } if (current == '\\') { escaped = true; continue; } if (current == '"') { inString = false; } continue; } if (current == '"') { inString = true; normalized += current; continue; } if (isLooseJsonNumberStart(text, index)) { const parsed = readLooseJsonNumberToken(text, index); normalized += normalizeLooseJsonNumberToken(parsed.token); index = parsed.endIndex - 1; continue; } normalized += current; } return normalized; } function ensureSqlclAvailable() { if (!isSqlclAvailable()) { throw new Error('Fallback SQLcl indisponivel. Ajuste ORACLE_SQLCL_PATH para um executavel sql valido.'); } } function escapeSqlclCredential(value) { const normalized = String(value || ''); if (normalized.indexOf('"') >= 0) { throw new Error('O fallback SQLcl nao suporta aspas duplas nas credenciais Oracle.'); } return normalized; } function parseSqlclJsonResult(stdout) { const payload = parseJsonStrictOrLenient(extractJsonPayload(stdout)); const firstResult = Array.isArray(payload.results) && payload.results.length > 0 ? payload.results[0] : {}; const columns = Array.isArray(firstResult.columns) ? firstResult.columns : []; const items = Array.isArray(firstResult.items) ? firstResult.items : []; return { columns, items }; } function normalizeSqlclRow(row, columns) { const normalized = {}; const source = row && typeof row == 'object' ? row : {}; columns.forEach((column) => { const columnName = String(column.name || '').trim(); const lowerCaseName = columnName.toLowerCase(); if (Object.prototype.hasOwnProperty.call(source, columnName)) { normalized[columnName] = serializeValue(source[columnName]); return; } if (Object.prototype.hasOwnProperty.call(source, lowerCaseName)) { normalized[columnName] = serializeValue(source[lowerCaseName]); return; } normalized[columnName] = null; }); return normalized; } function shouldUseSqlclFallback(error) { const message = String(error && error.message ? error.message : error || ''); return /NJS-116/i.test(message); } function runSqlclScript(connectionName, scriptLines) { const config = getConnectionConfig(connectionName); ensureSqlclAvailable(); const sqlLines = [ 'WHENEVER OSERROR EXIT 9', 'WHENEVER SQLERROR EXIT SQL.SQLCODE', 'connect ' + config.user + '/"' + escapeSqlclCredential(config.password) + '"@' + config.connectString, 'set pagesize 50000', 'set feedback off', 'set verify off', 'set echo off', 'set heading off' ].concat(scriptLines || [], ['exit', '']); return new Promise((resolve, reject) => { const child = spawn(ORACLE_SQLCL_PATH, ['-s', '/nolog'], { cwd: __dirname, stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; let finished = false; const timeoutHandle = setTimeout(() => { if (!finished) { finished = true; child.kill('SIGKILL'); reject(new Error('Tempo limite excedido ao executar SQLcl (' + ORACLE_SQLCL_TIMEOUT_MS + ' ms).')); } }, ORACLE_SQLCL_TIMEOUT_MS); child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8'); child.stdout.on('data', (chunk) => { stdout += chunk; }); child.stderr.on('data', (chunk) => { stderr += chunk; }); child.on('error', (error) => { if (finished) { return; } finished = true; clearTimeout(timeoutHandle); reject(error); }); child.on('close', (code) => { if (finished) { return; } finished = true; clearTimeout(timeoutHandle); const stdoutText = stripAnsi(stdout).trim(); const stderrText = stripAnsi(stderr).trim(); if (code !== 0) { reject(new Error(stderrText || stdoutText || ('SQLcl finalizou com codigo ' + String(code)))); return; } resolve({ config, stdout: stdoutText, stderr: stderrText }); }); child.stdin.end(sqlLines.join('\n')); }); } async function runReadOnlyQueryWithOracleDb({ connectionName, sql, bindsJson, maxRows }) { const binds = parseBindsJson(bindsJson); return withConnection(connectionName, async (connection, config) => { const result = await connection.execute(sql, binds, { outFormat: oracledb.OUT_FORMAT_OBJECT, resultSet: true, fetchArraySize: Math.min(FETCH_ARRAY_SIZE, maxRows), prefetchRows: Math.min(FETCH_ARRAY_SIZE, maxRows) }); const columns = (result.metaData || []).map((column) => ({ name: column.name, dbTypeName: column.dbTypeName || null, nullable: column.nullable, precision: column.precision, scale: column.scale })); const rows = []; let truncated = false; if (result.resultSet) { try { while (rows.length < maxRows) { const batchSize = Math.min(FETCH_ARRAY_SIZE, maxRows - rows.length); const batch = await result.resultSet.getRows(batchSize); if (!batch || batch.length == 0) { break; } rows.push.apply(rows, batch.map((row) => serializeValue(row))); } const overflowCheck = await result.resultSet.getRows(1); truncated = Array.isArray(overflowCheck) && overflowCheck.length > 0; } finally { await result.resultSet.close(); } } return { backend: 'oracledb', connection: config.name, connectString: config.connectString, rowsReturned: rows.length, maxRowsApplied: maxRows, truncated, columns, rows }; }); } async function runReadOnlyQueryWithSqlcl({ connectionName, sql, bindsJson, maxRows }) { if (bindsJson !== undefined && bindsJson !== null && String(bindsJson).trim() != '') { throw new Error('O fallback SQLcl nao suporta bindsJson. Ajuste ORACLE_BACKEND=oracledb ou remova os binds.'); } const limitedSql = 'SELECT * FROM (\n' + sql + '\n) WHERE ROWNUM <= ' + String(maxRows + 1); const sqlclResult = await runSqlclScript(connectionName, [ 'set sqlformat json', limitedSql + ';' ]); const parsed = parseSqlclJsonResult(sqlclResult.stdout); const normalizedColumns = parsed.columns.map((column) => ({ name: column.name, dbTypeName: column.type || null, nullable: null, precision: null, scale: null })); const normalizedRows = parsed.items.map((row) => normalizeSqlclRow(row, normalizedColumns)); const truncated = normalizedRows.length > maxRows; return { backend: 'sqlcl', connection: sqlclResult.config.name, connectString: sqlclResult.config.connectString, rowsReturned: Math.min(normalizedRows.length, maxRows), maxRowsApplied: maxRows, truncated, columns: normalizedColumns, rows: normalizedRows.slice(0, maxRows) }; } async function testConnectionWithOracleDb(connectionName) { return withConnection(connectionName, async (connection, config) => { const result = await connection.execute( "SELECT USER AS USUARIO, SYS_CONTEXT('USERENV', 'DB_NAME') AS BANCO, SYS_CONTEXT('USERENV', 'INSTANCE_NAME') AS INSTANCIA, TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD HH24:MI:SS') AS DATA_SERVIDOR FROM DUAL", {}, { outFormat: oracledb.OUT_FORMAT_OBJECT } ); return { backend: 'oracledb', connection: config.name, connectString: config.connectString, data: serializeValue((result.rows || [])[0] || {}) }; }); } async function testConnectionWithSqlcl(connectionName) { const sqlclResult = await runSqlclScript(connectionName, [ 'set sqlformat json', "SELECT USER AS USUARIO, SYS_CONTEXT('USERENV', 'DB_NAME') AS BANCO, SYS_CONTEXT('USERENV', 'INSTANCE_NAME') AS INSTANCIA, TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD HH24:MI:SS') AS DATA_SERVIDOR FROM DUAL;" ]); const parsed = parseSqlclJsonResult(sqlclResult.stdout); const normalizedColumns = parsed.columns.map((column) => ({ name: column.name, dbTypeName: column.type || null, nullable: null, precision: null, scale: null })); const firstRow = normalizeSqlclRow(parsed.items[0] || {}, normalizedColumns); return { backend: 'sqlcl', connection: sqlclResult.config.name, connectString: sqlclResult.config.connectString, data: firstRow }; } async function runReadOnlyQuery({ connectionName, sql, bindsJson, maxRows }) { const validatedSql = validateReadOnlySql(sql); const effectiveMaxRows = Math.min(parsePositiveInt(maxRows, DEFAULT_MAX_ROWS), HARD_MAX_ROWS); if (ORACLE_BACKEND == 'sqlcl') { return runReadOnlyQueryWithSqlcl({ connectionName, sql: validatedSql, bindsJson, maxRows: effectiveMaxRows }); } if (ORACLE_BACKEND == 'oracledb') { return runReadOnlyQueryWithOracleDb({ connectionName, sql: validatedSql, bindsJson, maxRows: effectiveMaxRows }); } try { return await runReadOnlyQueryWithOracleDb({ connectionName, sql: validatedSql, bindsJson, maxRows: effectiveMaxRows }); } catch (error) { if (!shouldUseSqlclFallback(error)) { throw error; } return runReadOnlyQueryWithSqlcl({ connectionName, sql: validatedSql, bindsJson, maxRows: effectiveMaxRows }); } } async function testConnection(connectionName) { if (ORACLE_BACKEND == 'sqlcl') { return testConnectionWithSqlcl(connectionName); } if (ORACLE_BACKEND == 'oracledb') { return testConnectionWithOracleDb(connectionName); } try { return await testConnectionWithOracleDb(connectionName); } catch (error) { if (!shouldUseSqlclFallback(error)) { throw error; } return testConnectionWithSqlcl(connectionName); } } async function resetPools() { const names = Array.from(poolCache.keys()); for (let index = 0; index < names.length; index += 1) { const name = names[index]; const pool = poolCache.get(name); if (pool) { await pool.close(0); } poolCache.delete(name); } return { closedPools: names.length, connections: names }; } function formatTextResult(payload) { return { content: [ { type: 'text', text: JSON.stringify(payload, null, 2) } ] }; } function extractOracleErrorSummary(message) { const normalized = String(message || '').replace(/\r\n?/g, '\n').trim(); const oracleMatch = normalized.match(/(?:Erro de SQL:\s*)?(ORA-\d{5}:[^\n]+)/i); if (!oracleMatch) { return ''; } const summary = [oracleMatch[1].trim()]; const commandLineMatch = normalized.match(/Erro na Linha de Comandos\s*:\s*([^\n]+)/i); if (commandLineMatch) { summary.push('Linha/coluna: ' + commandLineMatch[1].trim()); } return summary.join('\n'); } function normalizeErrorMessage(error) { const message = String(error && error.message ? error.message : error || '').replace(/\r\n?/g, '\n').trim(); if (!message) { return 'Erro nao identificado.'; } const oracleSummary = extractOracleErrorSummary(message); if (oracleSummary) { return oracleSummary; } return message; } function formatErrorResult(error) { return { content: [ { type: 'text', text: normalizeErrorMessage(error) } ], isError: true }; } const server = new McpServer({ name: 'oracle-davinti', version: '1.0.0' }); server.registerTool( 'list_connections', { title: 'List Oracle Connections', description: 'Lista as conexoes Oracle configuradas no arquivo .env do MCP.' }, async () => { try { return formatTextResult({ defaultConnection: runtimeConfig.defaultConnection || null, connections: runtimeConfig.connectionNames, configured: runtimeConfig.connectionNames.length > 0, backendMode: ORACLE_BACKEND, sqlclAvailable: isSqlclAvailable(), sqlclPath: isSqlclAvailable() ? ORACLE_SQLCL_PATH : null, hint: runtimeConfig.connectionNames.length > 0 ? null : 'Preencha mcp-oracle-custom/.env antes de usar.' }); } catch (error) { return formatErrorResult(error); } } ); server.registerTool( 'test_connection', { title: 'Test Oracle Connection', description: 'Testa a conexao com o banco e retorna usuario, banco e horario do servidor.', inputSchema: { connectionName: z.string().optional().describe('Nome da conexao configurada no .env. Opcional se houver uma conexao padrao.') } }, async ({ connectionName }) => { try { return formatTextResult(await testConnection(connectionName)); } catch (error) { return formatErrorResult(error); } } ); server.registerTool( 'run_query', { title: 'Run Read-only Oracle Query', description: 'Executa consultas Oracle de leitura com timeout, pool e limite de linhas.', inputSchema: { sql: z.string().min(1).describe('Consulta Oracle iniciando com SELECT ou WITH.'), connectionName: z.string().optional().describe('Nome da conexao configurada no .env. Opcional se houver uma conexao padrao.'), bindsJson: z.string().optional().describe('JSON com binds nomeados ou posicionais. Exemplo: {"ParLoja":1219472}'), maxRows: z.number().int().positive().optional().describe('Quantidade maxima de linhas retornadas. Respeita o limite rigido configurado no servidor.') } }, async ({ sql, connectionName, bindsJson, maxRows }) => { try { return formatTextResult(await runReadOnlyQuery({ sql, connectionName, bindsJson, maxRows })); } catch (error) { return formatErrorResult(error); } } ); server.registerTool( 'reset_pools', { title: 'Reset Oracle Pools', description: 'Fecha todos os pools Oracle abertos por este servidor MCP.' }, async () => { try { return formatTextResult(await resetPools()); } catch (error) { return formatErrorResult(error); } } ); async function shutdown() { try { await resetPools(); } catch (error) { console.error('Falha ao fechar pools Oracle:', error); } } process.on('SIGINT', async () => { await shutdown(); process.exit(0); }); process.on('SIGTERM', async () => { await shutdown(); process.exit(0); }); const transport = new StdioServerTransport(); server.connect(transport).catch(async (error) => { console.error('Falha ao iniciar o MCP Oracle:', error); await shutdown(); process.exit(1); });