Files
vscode-ia/.vscode/mcp-oracle-davinti/server.mjs
T
2026-05-14 09:54:24 -03:00

1195 lines
31 KiB
JavaScript

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 || '<sem nome>') + '.');
}
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);
});