Zademy

lu-query: el servidor MCP para Oracle que por fin terminé

Herramientas
lu-query; Oracle; MCP; TypeScript
words palabras

Este repo se llama lu-query y ya está publicado en npm. Va en la 2.18.0, con 11 releases, 156 commits y 8 publicaciones. Todo eso pasó en unas 28 horas, entre el 17 y el 18 de octubre de 2026.

La idea, en cambio, la tenía desde julio. El primer intento se llama mcp-oracle-db, está escrito en Java, y sigue en mi cuenta con fecha del 8 de julio de 2026, completamente vacío. Ni una línea de código, ni un README con contenido. Solo el nombre y la fecha, que es la forma más incómoda que tiene un proyecto abandonado de seguir recordándote que existió.

Lo interesante es que la segunda versión no tardó 28 horas en escribirse. Tardó 28 horas en terminarse. Y la diferencia entre las dos no fue el lenguaje, aunque automáticamente asumo que sí. Fue que la primera vez no tenía claro qué estaba construyendo.

Por qué el intento en Java murió en julio

Nunca lo dejé por escrito, así que esto es lectura mía y puede estar equivocado. Pero MCP es un ecosistema que vive en TypeScript: los clientes, los SDK, los ejemplos que te vas a encontrar están casi todos en Node. Escribirlo en Java significaba implementar el protocolo a mano, y encima hacerlo sobre el driver clásico de Oracle, que exige Oracle Instant Client, Docker, LD_LIBRARY_PATH y un sqlnet.ora bien puesto. Ese es el peaje típico de Oracle, y pega justo en la laptop de alguien que no es un servidor Linux con todo preinstalado.

O sea: antes de escribir la primera tool ya estaba peleándome con el entorno. Así se mueren los proyectos. No por el código, sino por la primera tarde.

La segunda vez lo escribí en Node con el driver oracledb 26 en modo thin. Sin Instant Client, sin Docker, sin LD_LIBRARY_PATH, sin sqlnet.ora. Node 22 o superior, TypeScript, empaquetado con tsup, licencia MIT. Y la diferencia de productividad fue tan brutal que parece otro proyecto, cuando en realidad es el mismo.

La versión dos, en números

Expone 75 tools, 4 resources y 9 prompts. Las 75 tools se reparten así: 1 de sistema, 2 de query, 3 de DML, 7 de DDL, 8 de metadata base, 21 de catálogo, 7 de estadísticas del optimizador, 2 de PL/SQL, 4 de explain y tuning, 7 de grafos de dependencias y 13 de performance sobre vistas V$.

Detrás hay 7,009 líneas de TypeScript en 35 archivos, más 235 archivos de test. De esos, 219 tests de integración corren contra un Oracle real. El CI es una matriz de 3 sistemas operativos por Node 22, 24 y 26.

Los números que no están son los que más me gustaría tener. No he medido el antes y el después de un query lento con mis propias manos, así que no voy a inventar una tabla con milisegundos. Cuando hablo de rendimiento hablo de lo que el servidor le deja hacer al agente: medir antes y después con DBMS_XPLAN, comparar, y ver qué plan cambió. Nada más.

Todo el SQL pasa por el mismo lugar

Hay una clase entera de herramientas donde el SQL se ejecuta, y si cada una arma su propia conexión y su propio manejo de errores terminas con veinte caminos distintos para el mismo problema. Yo lo resolví con un chokepoint.

DataAccess, en src/db/data-access.ts, es el único lugar por donde pasa una sentencia. Y ahí se aplican siempre las mismas cosas: el timeout, el tope de 1000 filas por lectura, la auditoría y el commit explícito.

Los valores por defecto son conservadores a propósito: pool de 4 conexiones, timeout de statement de 120 segundos, socket de 30 segundos y un watchdog de MCP a 150 segundos. Si el statement se pasa del tiempo, se corta y se registra. El tope de 1000 filas es server-side, no un slice en mi memoria después de haber traído la tabla entera, y la paginación va por run_query(limit, offset).

Nada de eso es rocket science. Es lo aburrido, y lo aburrido es exactamente lo que conviene que esté en un solo sitio.

El agente ya no adivina el esquema

Esta es la parte por la que realmente lo uso, y no tiene nada de romanticismo. Antes de esto, cuando le preguntabas a un agente algo sobre una base Oracle, lo que hacías era inventarte tablas. Te da un SELECT que suena muy bien y que revienta en runtime porque la columna se llama CUSTOMER_ID y no customerId.

Con 21 tools de catálogo eso se acaba. El agente puede preguntar por columnas, índices, constraints, dependencias reales, y solo después escribir la consulta.

Y luego están las 13 tools de performance sobre vistas V$. list_blocked_sessions, get_wait_events, list_top_sql. No es que el agente se vuelva un DBA, es que deja de adivinar también en la capa de diagnóstico. Si algo está bloqueado, ahora hay una tool que lo dice con nombre y apellido en lugar de un SELECT improvisado contra V$.

A eso se suma el audit trail por statement. No lo hice por auditoría: hay 42 archivos reales de logs en mcp-audit-logs/, en Markdown, uno por sesión, grepeable por un humano. Elegí Markdown y no JSON a propósito. JSON es mejor para máquinas y peor para el caso real, que es alguien intentando entender hace dos horas qué carajos ejecutó el agente.

Los dos bugs que casi me hacen botarlo

El primero me gusta contarlo porque fue el más tonto y el más instructivo.

Todas las tools de catálogo fallaban. El driver de Oracle devuelve el nombre del schema actual en MAYÚSCULAS, mi caché por conexión nunca se llenaba porque comparaba contra minúsculas, y todo lo que dependía de saber "quién soy yo" se caía.

Lo bueno: encontré el bug. Lo terrible: los tests estaban en verde. Porque los fakes que usaba en la suite devolvían minúsculas, o sea, los tests no simulaban el driver, simulaban lo que yo creía que hacía el driver. Una suite entera en verde escondía un fallo total. Arreglé el bug y arreglé el fake. En la 2.16.3.

El segundo fue CLOB, NCLOB y BLOB. Los LOB venían arrastrando una referencia a la conexión, y JSON.stringify reventaba con "Converting circular structure to JSON". Cualquier tabla con un CLOB quedaba inutilizable. También arreglado en la 2.16.3.

Ninguno de los dos es un bug raro. El primero es una suposición mía sobre el driver que mis propios tests me ayudaron a defender. Del segundo me queda la lección más importante: si tu capa de serialización toca objetos que vienen de un driver nativo, no confíes.

Lo que aprendí documentando huecos en vez de fingir

Varias cosas no tienen equivalente limpio en TypeScript, y ahí es donde más fácil me habría engañado a mí mismo.

node-sql-parser no tiene dialecto Oracle. Parsea como si fuera PostgreSQL y un MERGE te lo reporta como UNKNOWN. Podría haber escrito un parser "compatible" y habría mentido.

El driver thin no hace autocommit, y no tiene stmtTimeout. El timeout se simula con un Promise.race más breakExecution. Funciona, pero es una simulación, y está etiquetada como tal en el código.

EXPLAIN PLAN necesita bloques PRAGMA AUTONOMOUS_TRANSACTION para no contaminar la sesión. Eso no lo inventé, lo aprendí porque la primera versión me dejó la sesión en un estado raro.

Y el patrón de pruebas sí me gustó: en vez de mockear módulos, inyecto un FakeDataAccess programable en el servidor MCP montado sobre InMemoryTransport. Lo que se prueba es el contrato que ve el cliente, tools/list, los resources, isError. Con el bug de CURRENT_SCHEMA descubrí que eso también sirve para lo contrario.

Un detalle más, porque me divertí: la skill de benchmark no agregó ni una sola tool nueva. La revisé pieza por pieza contra la base real y ya era composable con las que existían. Preferí documentarlo y verificarlo antes que documentarlo y asumirlo.

Lo que me gusta

  • Que abra cero puertos. Es stdio, la confianza está en el proceso y punto. No hay superficie HTTP, ni IAM, ni cuenta en la nube.
  • El allow-list de identificadores. ^[A-Za-z][A-Za-z0-9_$#]{0,127}$ más literales escapados. Y run_query solo acepta SELECT: el DML y el DDL van por tools dedicadas con preview, rollback y execute. -- Esto no es teórico. El servidor MCP de Postgres de referencia de Anthropic se archivó el 10 de julio de 2025 después de una inyección SQL que evadía el modo read-only. Hay uno de executeautomation con un bypass sin parche, y un estudio de abril de 2026 bypaseó 17 de 17 servidores MCP SQL "read-only". Diecisiete de diecisiete. Eso es exactamente el bug que el allow-list y la separación de tools previenen, y prefiero pensarlo así y no como un accidente.
  • El modo thin. Es lo que hizo posible el proyecto entero. El peaje de Oracle dejó de ser el primer problema.
  • Las 10 skills. explore-schema, query-data, audit-data-quality, plan-data-changes, manage-schema, tune-sql, benchmark-sql, audit-report, triage-incident, use-plsql. El agente no adivina qué hacer, hay una instrucción para cada intención. -- Publicar sin token. OIDC trusted publishing, sin OTP. Es un detalle pequeño que me quitó de encima una tanda de fricción.

Lo que me preocupa

  • No tiene autenticación. stdio y punto significa que la frontera es tu usuario de base de datos. El servidor no es un firewall, y las tools hacen exactamente lo que ese usuario puede hacer.
  • Los logs de auditoría son pasivo. Guardan el SQL literal. Es lo que los hace útiles y también lo que los hace peligrosos. -- Solo Oracle. Nada de Postgres, nada de SQL Server. Para mí está bien porque nunca he tenido la necesidad, pero entiendo que es una limitación real para casi todo el mundo. -- Las 2 estrellas y 725 descargas del primer día. Traducido: básicamente nadie más lo está usando. Y 0 contribuciones externas. Publicar en npm no es que alguien lo pruebe, es que el paquete existe. -- Oracle ya tiene su MCP server oficial. Database Tools, con OAuth 2.0, OCI IAM, Streamable HTTP y NL2SQL; Autonomous Database expone endpoint MCP. Lo mío es local y sin cuenta cloud, pero no tiene auth, no es multi-tenant y no tiene NL2SQL. Frente a la offering oficial estoy en desventaja en casi todo lo que ellos hicieron bien, y gano en lo local. Eso es un posicionamiento, no una victoria.
  • Cosas que dejé sucias. El CHANGELOG.md se quedó en 2.16.3 mientras package.json ya va en 2.18.0. Y sobran topics de sqlite en un proyecto que no habla con SQLite en ninguna parte. Ninguna de las dos rompe nada, pero son la clase de basura que se acumula cuando trabajas veinte horas seguidas.

Dónde lo dejaría hoy

Mi veredicto, con todas sus aristas: si trabajas con Oracle y quieres que un agente toque tu base sin sacarlo de tu máquina, esto te sirve hoy. Si no trabajas con Oracle, no hay nada aquí para ti, y no tengo intención de arreglarlo.

Lo que más me gusta del proyecto es otro, y no es técnico. Tardé meses en dejarlo y 28 horas en terminarlo. Lo que cambió no fue el lenguaje, fue que dejé de tener miedo a la primera tool y empecé a escribir. El repo vacío de julio sigue ahí y cada vez que lo veo me recuerda el precio de empezar por el entorno en vez de empezar por el problema.

Salí con más dudas que al principio. Sigo sin saber si esto es un proyecto o un ejercicio largo. Las 2 estrellas y cero contribuciones externas me sugieren lo segundo, y voy a tardar en saber cuál de las dos cosas es.

Por cierto, dejé un promo corto de 33 segundos renderizado con Remotion dentro del repo, en docs/lu-query-promo.mp4. Ahí explico qué es esto y cómo se conecta, en 33 segundos.

Nos vemos en el siguiente video.

Recursos