jdbc-mcp-server

Acceso de solo lectura a PostgreSQL, Oracle y SQL Server para agentes de IA: descubrimiento de esquemas, validación de consultas, planes de ejecución, evaluación comparativa y análisis de índices/estadísticas. Controladores JDBC incluidos.

Documentación

Servidor MCP JDBC

CI Release License Java 21 MCP

Un servidor MCP local para acceso de solo lectura a bases de datos PostgreSQL, Oracle y Microsoft SQL Server. Permite que agentes de IA como Claude Code, Cursor, VS Code Copilot y otros escriban consultas SQL, inspeccionen planes de ejecución y exploren la estructura de la base de datos: tablas, columnas, índices, claves foráneas, vistas, funciones y secuencias.

Los controladores JDBC de PostgreSQL, Oracle y Microsoft SQL Server están incluidos en el archivo fat jar, por lo que no se requiere instalación adicional de controladores.

El servidor expone 49 herramientas MCP y puede exponer opcionalmente recursos MCP calificados por catálogo para metadatos de tablas y columnas. Las herramientas pueden actualizar el catálogo SQLite local, pero nunca escriben en la base de datos inspeccionada de PostgreSQL, Oracle o SQL Server.

Un proceso del servidor puede atender varias bases de datos: nómbrelas en connections.json y pase connection a cualquier herramienta. El manifiesto de herramientas se mantiene como un único conjunto sin importar cuántas bases de datos estén configuradas, y los grupos de conexiones se abren solo para las bases de datos realmente utilizadas.

Inicio rápido

1. Obtenga el jar — descargue jdbc-mcp-server.jar desde la última versión (se requiere JDK 21+; todos los controladores JDBC están incluidos), o compílelo usted mismo:

./gradlew bootJar   # → build/libs/jdbc-mcp-server.jar

2. Describa sus bases de datos en ~/.jdbc-mcp-server/connections.json:

{
  "connections": {
    "myapp": {
      "url": "jdbc:postgresql://db.example.com:5432/myapp",
      "username": "ai_readonly",
      "password": "secret",
      "description": "Application database — customers, orders, shipments"
    }
  }
}

Use un usuario de base de datos de solo lectura; cinco minutos allí superan cualquier otra protección en este servidor.

3. Registre el servidor con su cliente MCP — sin configuraciones de base de datos en la configuración del cliente:

{
  "command": "java",
  "args": ["-jar", "<absolute-path>/jdbc-mcp-server.jar"],
  "env": {}
}

Para Claude Code, eso es un solo comando:

claude mcp add --scope user jdbc java -jar /path/to/jdbc-mcp-server.jar

4. Pida al agente listConnections. Él responde con las bases de datos que este servidor atiende; cada otra herramienta toma ese nombre como su primer argumento:

{"connection": "myapp", "sql": "SELECT count(*) FROM orders"}

Detalles completos: Bases de datos y credenciales, Conexión de un cliente de IA, Atender varias bases de datos desde un solo servidor.

Bases de datos y credenciales

Cada base de datos que este servidor atiende se describe en un único archivo JSON. Nada sobre una base de datos — URL, credenciales, esquema, tiempos de espera, límites — proviene del entorno.

El archivo de conexiones

Ruta predeterminada ~/.jdbc-mcp-server/connections.json (<data-dir>/connections.json), anulada con JDBC_MCP_CONNECTIONS_FILE. Cuando el archivo falta o no define ninguna conexión, el servidor igualmente se inicia (para que un cliente MCP pueda listar sus herramientas), registra una advertencia y listConnections devuelve una lista vacía; cada otra herramienta entonces informa que no hay conexión disponible. Un archivo presente pero malformado es un error de inicio.

{
  "connections": {
    "orders": {
      "url": "jdbc:postgresql://db.example.com:5432/orders",
      "username": "ai_readonly",
      "password": "secret",
      "defaultSchema": "public",
      "description": "Order service — customers, orders, shipments",
      "structureSnapshotSchemas": ["public", "nsi"]
    },
    "billing": {
      "url": "jdbc:oracle:thin:@//oracle.example.com:1521/BILLING",
      "username": "AI_READONLY",
      "password": "${BILLING_DB_PASSWORD}",
      "description": "Legacy billing (Oracle)"
    }
  }
}

La clave del objeto es el nombre de la conexión. También es el nombre del directorio del catálogo local de la conexión (<data-dir>/<name>/) y aparece en los URI de recursos MCP, por lo que debe coincidir con [A-Za-z0-9._-]+(@[A-Za-z0-9._-]+)?, tener como máximo 64 caracteres y no ser . ni ...

El @ opcional existe para nombrar los dos ejes por separado: <service>@<stand>, como en ssj@dev, nsi@dev, ssj@tst. Un guion no puede hacer ese trabajo, porque los guiones ya aparecen dentro de los nombres de servicio (ssj-ws, ssj-ek-export, ais-ui), por lo que ssj-ws-dev es ambiguo tanto para un humano como para un modelo. @ nunca aparece en un nombre de servicio y se lee como "qué, dónde" de la misma manera que user@host. Se codifica en porcentaje a %40 en los URI de recursos; nada más sobre el nombre cambia — el directorio en disco es el nombre tal como está escrito.

url es el único campo obligatorio; el motor se detecta por su prefijo (jdbc:postgresql:, jdbc:oracle:, jdbc:sqlserver:). description es texto libre devuelto por listConnections, por lo que un agente puede elegir una base de datos por significado en lugar de por nombre — vale la pena completarlo.

Cualquier valor de cadena puede hacer referencia a una variable de entorno como ${VAR}. Una variable referenciada que no esté configurada falla al inicio con un mensaje que nombra la variable y el campo; nunca se convierte en una contraseña vacía. Lea la siguiente sección antes de usarla.

Por qué las credenciales viven en un archivo, no en variables de entorno

El propósito de este servidor es que el agente llegue a la base de datos solo a través de él: cada declaración pasa por la protección de solo lectura, cada resultado está limitado por maxRows, y nada más que SELECT / WITH / EXPLAIN pasa.

Las credenciales en variables de entorno socavan exactamente eso. Se establecen en el proceso del servidor por el cliente MCP, lo que significa que también residen en la propia configuración del cliente — un archivo que los agentes leen y editan como parte de su rutina — y en el entorno de cualquier shell que lo haya lanzado. Un agente que ha visto una URL, un usuario y una contraseña ya no necesita las herramientas: psql, sqlplus, sqlcmd o tres líneas de Python se conectan directamente a la base de datos, sin protección, sin límite de filas y sin rastro en el registro de este servidor.

Por lo tanto, el servidor no acepta credenciales de base de datos del entorno en absoluto — no hay variables JDBC_URL / JDBC_USERNAME / JDBC_PASSWORD. Viven en connections.json, que solo el servidor lee.

Sea claro sobre qué compra y qué no compra:

  • Elimina el camino fácil. Las credenciales dejan de ser parte del material que un agente maneja rutinariamente: configuraciones de cliente MCP, entorno de shell, volcados de env en registros e informes de errores.
  • No es una zona de pruebas. Un agente con acceso al shell ejecutándose como usted puede leer el archivo; chmod 600 mantiene fuera a otros usuarios, no a un proceso que se ejecuta como su usuario.
  • La garantía que sobrevive a todo es un usuario de base de datos de solo lectura. El archivo reduce la superficie de ataque; los permisos propios de la base de datos la cierran.

Por la misma razón, prefiera una contraseña literal en el archivo sobre una referencia ${VAR} cuya variable se establecería en el bloque env del cliente MCP — eso coloca el secreto directamente de vuelta donde el agente mira. ${VAR} gana su lugar cuando el valor se inyecta desde fuera del alcance del agente (una unidad systemd, un script contenedor, un administrador de secretos), o cuando el archivo mismo se comparte o se confirma y el secreto no debe estar.

Campos de conexión

Todo excepto url es opcional; un campo omitido recurre al valor predeterminado integrado:

CampoPredeterminadoSignificado
urlobligatorioURL JDBC; también selecciona el motor
username, passwordningunoCredenciales de la base de datos
descriptionningunoTexto libre devuelto por listConnections
defaultSchemael esquema de la sesiónEsquema utilizado cuando una llamada de herramienta de metadatos omite uno
queryTimeoutSeconds30Tiempo de espera por consulta; 0 lo desactiva
maxRows1000Límite de filas para una respuesta; truncated: true cuando se alcanza
fetchSize500Sugerencia JDBC fetchSize
readonlyGuardstrictoff desactiva la verificación de solo SELECT del lado del cliente
poolMaximumSize40Tamaño máximo del grupo Hikari
poolMinimumIdle0Mínimo inactivo de Hikari; 0 mantiene el grupo perezoso
poolConnectionTimeoutMs10000Tiempo de espera de obtención de conexión de Hikari
poolValidationTimeoutMs5000Tiempo de espera de validación de Hikari
poolIdleTimeoutMs60000Las conexiones inactivas por encima de poolMinimumIdle se cierran después de esto
structureSnapshotSchemasel esquema predeterminadoEsquemas capturados por rebuildCatalog
structureSnapshotOracleColumnQueryTimeoutSeconds300Tiempo de espera solo de Oracle para la consulta de columnas masiva durante rebuildCatalog; 0 lo desactiva
usageCatalogEnabledtrueCuando false, las herramientas de uso informan el estado deshabilitado
usageCatalogPathsningunoDirectorios adicionales, archivos JSON o archivos zip con registros QueryUsage
usageNativeSchemasel esquema predeterminadoEsquemas escaneados para uso nativo
usageNativeIncludeViews, usageNativeIncludeRoutines, usageNativeIncludeTriggerstrueQué cubre el escaneo de uso nativo
usageNativeMaxObjects10000Máximo de registros de uso nativo por construcción de índice

Las banderas del grupo JDBC_MCP_TOOLS_* permanecen en el entorno — dan forma al manifiesto de herramientas, que es compartido por todas las conexiones. Consulte Configuración para las pocas variables que el propio servidor lee.

Ejemplos de URL

jdbc:postgresql://db.example.com:5432/myapp
jdbc:postgresql://db.example.com:5432/myapp?currentSchema=public&sslmode=require

jdbc:oracle:thin:@//db.example.com:1521/ORCLPDB1
jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=...)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=...)))

jdbc:sqlserver://db.example.com:1433;databaseName=myapp;encrypt=true;trustServerCertificate=false
jdbc:sqlserver://db.example.com;databaseName=myapp;integratedSecurity=false

Por qué existe esto

Escenario: usted pide a un LLM que "revise la base de datos y muestre cuántos pedidos tuvimos por estado el mes pasado." Sin este servidor, el LLM puede:

  • inventar nombres de tablas y columnas;
  • omitir los detalles reales del esquema, como campos anulables, tipos y claves foráneas;
  • generar accidentalmente un DELETE o TRUNCATE mientras "razona."

Con este servidor, el LLM puede:

  1. llamar a schemaBrief para descubrir el mapa del esquema, o a queryContext para obtener contexto detallado listo para usar: tablas, columnas, relaciones y restricciones;
  2. refinar el contexto con tableContext alrededor de una tabla específica o findJoinPaths para el descubrimiento de rutas JOIN;
  3. escribir una consulta y opcionalmente llamar a inspectQuery, queryLint o resolveQueryLineage para verificaciones de AST, metadatos y linaje de vistas/rutinas;
  4. llamar a validateQuery con el mismo params o namedParams que se usará para la ejecución, validando la sintaxis sin ejecutar la consulta;
  5. llamar a explainQuery cuando se necesite un plan;
  6. llamar a executeQuery para obtener datos.

Cualquier consulta que no sea SELECT se bloquea antes de llegar a la base de datos.

Más allá de la introspección de esquema en vivo, el servidor también mantiene un catálogo de uso local de consultas SQL conocidas utilizadas por aplicaciones e informes contra la base de datos inspeccionada, junto con su contexto comercial — significados de parámetros, descripciones de columnas de salida y dónde se representa cada salida (celda de Excel, widget de panel, región de BI Publisher). Esto permite que el LLM responda preguntas como "¿qué informes de producción ya tocan esta columna?" y "¿qué etiqueta comercial tiene este campo en la tarjeta del cliente?" contra un cuerpo curado de evidencia en lugar de adivinar solo por nombres. El catálogo también es la fuente del paquete evidence tipado de tres capas en los bordes de relación, por lo que los JOIN no declarados observados en consultas de producción se tratan como sugerencias de primera clase junto con las FK declaradas. Consulte Catálogo de uso a continuación.

Arquitectura

                                                  +------------+
                                            +---> | database A |
+-------------+     stdio      +----------+ |     +------------+
|  AI agent   | <------------> | jdbc-mcp | |     +------------+
| (Claude Code|  stdin/stdout  |  server  |-+---> | database B |
|  Cursor...) |                |  (Java)  | |     +------------+
+-------------+                +----------+ |
                                            +---> ...

                                          read-only JDBC
                                          PG / Oracle / SQL Server

El protocolo es solo stdio. El cliente inicia el servidor como un proceso hijo. Un proceso atiende cualquier número de bases de datos nombradas, todas declaradas en el archivo de conexiones; el grupo de conexiones de una conexión se abre la primera vez que una llamada de herramienta lo nombra.

Recursos MCP

Cuando JDBC_MCP_RESOURCES_ENABLED=true, el servidor expone — para cada conexión configurada que ya tenga un archivo de catálogo local — un manifiesto de catálogo concreto, un recurso concreto para cada tabla o vista ya persistida en la instantánea de estructura de esa conexión, y dos plantillas de recursos parametrizadas:

jdbc-mcp://catalog/<catalog>/manifest
jdbc-mcp://catalog/<catalog>/schemas/SSV/tables/CUSTOMERS
jdbc-mcp://catalog/<catalog>/schemas/{schema}/tables/{table}
jdbc-mcp://catalog/<catalog>/schemas/{schema}/tables/{table}/columns/{column}

<catalog> es el nombre de conexión codificado en porcentaje. Es fijo por conexión cuando el servidor se inicia y no es una variable de plantilla que los clientes puedan usar para cambiar de base de datos: una lectura resuelve la conexión desde el URI que se le dio. Esto mantiene los URI de recursos inequívocos tanto entre las conexiones de un servidor como entre varias instancias registradas de este jar. El manifiesto informa el tipo de base de datos, la versión de instantánea/tiempo de compilación/esquemas cubiertos y las plantillas exactas para su catálogo. Las lecturas de tablas y columnas reutilizan MetadataService, por lo que tienen la misma semántica de instantánea persistente y respaldo en vivo que describeTable. Los recursos de tablas concretas se cargan desde las instantáneas locales de SQLite cuando el servidor MCP se inicia — no se contacta ninguna base de datos ni se crea ningún pool JDBC para esto — por lo que los clientes con soporte de selector de recursos MCP pueden ofrecer entradas como SSV.CUSTOMERS sin consultar la base de datos en vivo. Sus descripciones compactas contienen el comentario de la base de datos cuando está presente, las columnas de clave primaria y las asignaciones de claves foráneas salientes; las listas de columnas y los recuentos se omiten intencionalmente. Después de ejecutar rebuildCatalog en un servidor ya en ejecución, reinicie o reconecte esa instancia del servidor MCP para actualizar su lista de recursos concretos.

Los recursos de columnas incluyen la definición de la columna más la posición de PK coincidente, restricciones únicas, índices, claves foráneas salientes/entrantes y restricciones CHECK. Los segmentos de ruta URI conservan mayúsculas y minúsculas y utilizan codificación porcentual UTF-8. Los recursos están deshabilitados por defecto; habilitarlos deja las herramientas MCP sin cambios.

Herramientas MCP

Las 49 herramientas se agrupan a continuación por propósito.

Cada herramienta toma connection como su argumento primero, obligatorio, nombrando la base de datos contra la que ejecutarse — incluyendo instalaciones que sirven exactamente una base de datos. listConnections lista los nombres. Consulte Sirviendo Varias Bases de Datos desde un Solo Servidor.

Grupos de Herramientas

Las herramientas están organizadas en grupos que se pueden activar o desactivar independientemente con banderas JDBC_MCP_TOOLS_*. Todos los grupos están activados por defecto, por lo que el conjunto completo de herramientas está disponible de fábrica. Desactivar grupos reduce el manifiesto tools/list, lo cual importa para modelos locales de contexto pequeño que de otro modo se verían inundados con esquemas de herramientas antes de la primera llamada.

GrupoBandeeraPredeterminadoHerramientas
MetadatosJDBC_MCP_TOOLS_METADATAactivadolistSchemas, listTables, describeTable, getTriggerDefinition, getViewDefinition, listRoutines, getRoutineDefinition, listSequences, searchObjects
ConsultaJDBC_MCP_TOOLS_QUERYactivadoexecuteQuery
AdministraciónJDBC_MCP_TOOLS_ADMINactivadorebuildCatalog
MuestraJDBC_MCP_TOOLS_SAMPLEactivadosampleRows
Análisis de consultasJDBC_MCP_TOOLS_ANALYSISactivadoexplainQuery, analyzePlan, validateQuery, inspectQuery, queryLint, resolveQueryLineage
DistribuciónJDBC_MCP_TOOLS_DISTRIBUTIONactivadocolumnStats, columnDistribution, columnHistogram, nullRatio, estimateSelectivity, joinCardinality
EstadísticasJDBC_MCP_TOOLS_STATSactivadotableStats, indexStats, unusedIndexes, redundantIndexes, fkIndexCoverage
BenchmarkJDBC_MCP_TOOLS_BENCHMARKactivadobenchmarkQuery, timedQuery
Catálogo de usoJDBC_MCP_TOOLS_USAGEactivadousageCatalogStatus, getQuery, listQueries, findQueriesByTable, findQueriesByColumn, observedRelationships, listKnownTags, listKnownDomains, listKnownKinds, invalidateUsageCatalogCache
Contexto de esquemaJDBC_MCP_TOOLS_SCHEMA_CONTEXTactivadotableContext, findJoinPaths, schemaLint, schemaBrief, schemaGraph, queryContext, schemaGraphDot
ConexionesJDBC_MCP_TOOLS_CONNECTIONSactivadolistConnections

Cada bandera acepta true / false. Para un modelo local de contexto pequeño, desactive los grupos que no necesite — por ejemplo, mantenga solo Metadatos + Consulta configurando el resto en false — para reducir el manifiesto a un conjunto mínimo de "explorar el esquema y ejecutar una consulta". Las secciones siguientes describen cada herramienta independientemente de su grupo.

Consulta

HerramientaDescripción
executeQueryEjecutar una declaración SELECT, WITH o EXPLAIN. Parámetros: sql, params (array para ?) o namedParams (objeto para :name), limit, timeoutSeconds. El resultado se marca con truncated: true si se alcanza el límite de filas
explainQueryDevolver el plan de ejecución. PostgreSQL: EXPLAIN (FORMAT TEXT). Oracle: EXPLAIN PLAN FOR más DBMS_XPLAN.DISPLAY. SQL Server: SET SHOWPLAN_TEXT ON en la misma sesión. Los parámetros se pueden pasar como params (?) o namedParams (:name). analyze=true en PostgreSQL habilita EXPLAIN ANALYZE; tenga cuidado, porque la consulta realmente se ejecuta. SQL Server actualmente devuelve solo planes estimados
analyzePlanResumen de plan compacto orientado a LLM en lugar de un volcado de plan sin procesar grande: nodos de mayor costo, escaneos completos en tablas grandes, errores de estimación (planificador vs. realidad, requiere analyze=true en PostgreSQL), bucles anidados riesgosos con entrada externa grande y derrames de ordenamiento en disco. PostgreSQL: EXPLAIN (FORMAT JSON) / EXPLAIN ANALYZE. Oracle: EXPLAIN PLAN más PLAN_TABLE (analyze se ignora porque Oracle proporciona un plan estático aquí). SQL Server: SET SHOWPLAN_XML ON plan estimado. Los parámetros se pueden pasar como params (?) o namedParams (:name)
validateQueryValidar sintaxis sin ejecución: guardia de solo lectura más prepareStatement del controlador, con un resumen inspection derivado de JSqlParser cuando el análisis tiene éxito. Los parámetros se pueden pasar como params (?) o namedParams (:name). Útil para la autocorrección del LLM
inspectQueryAnalizar SQL a través de JSqlParser sin tocar la base de datos y devolver un resumen AST: tablas, alias, CTEs, elementos de selección, uniones, predicados, order by, columnas, parámetros, características y advertencias del analizador
queryLintAnalizar SQL y combinar el AST con metadatos, índices y verificaciones de FK. Devuelve advertencias de asesoramiento como tablas o columnas desconocidas, SELECT *, uniones sin condiciones, FKs sin índices de soporte y columnas de predicado/order by que no son columnas de índice principales. SQL no se ejecuta
resolveQueryLineageResolver objetos directos referenciados por una consulta y expandir recursivamente vistas/vistas materializadas de base de datos a tablas físicas subyacentes. La expansión de funciones/procedimientos es de mejor esfuerzo: las declaraciones SELECT / WITH incrustadas se extraen del código fuente de la rutina cuando está disponible. Parámetros: sql, schema, expandViews, expandRoutines, maxDepth

Benchmarking

Herramientas para medir el costo real de una consulta, para que el LLM no tenga que adivinar a partir del plan y pueda ver milisegundos reales y contadores de búfer.

HerramientaDescripción
benchmarkQueryEjecutar la consulta coldRuns + warmRuns veces (predeterminado 1 en frío + 3 en caliente) y devolver min, median y max de tiempo de pared para ejecuciones en caliente; las ejecuciones en frío se informan por separado. Los parámetros se pueden pasar como params (?) o namedParams (:name). limit y timeoutSeconds son obligatorios; las consultas sin límite se rechazan. Devuelve el tamaño del último resultado (row_count, columnas, truncated), no las filas
timedQueryexecuteQuery regular más elapsed_ms de tiempo de pared. Los parámetros se pueden pasar como params (?) o namedParams (:name). En PostgreSQL, también captura instantáneas pg_stat_statements antes y después de la consulta; el diff muestra qué IDs de consulta agregaron calls, total_exec_time_ms, rows, shared_blks_hit y shared_blks_read, dejando claro dónde pasó tiempo el servidor. Requiere pg_stat_statements (CREATE EXTENSION pg_stat_statements; más shared_preload_libraries); si la extensión falta, devuelve pg_stat_statements.available: false

Metadatos

HerramientaDescripción
listSchemasListar esquemas. Los esquemas del sistema están ocultos por defecto; use includeSystem=true para mostrar todos
listTablesListar tablas y vistas en un esquema. Parámetros: schema, namePattern (con % / _), types (separados por comas, por ejemplo TABLE,VIEW,MATERIALIZED VIEW)
describeTableDescripción completa del objeto en una llamada: columnas, clave primaria, restricciones únicas, índices, FKs salientes/entrantes, restricciones CHECK y valores permitidos, más metadatos de disparadores compactos
getTriggerDefinitionCuerpo del disparador para un disparador nombrado. Parámetros: schema, table, trigger
getViewDefinitionDefinición SQL de una vista
listRoutinesFunciones, procedimientos y paquetes en un esquema
getRoutineDefinitionCódigo fuente de función o procedimiento. En Oracle, todas las líneas ALL_SOURCE se concatenan en orden
listSequencesSecuencias en un esquema, o en todos los esquemas cuando schema se omite
searchObjectsBúsqueda de subcadena sin distinción de mayúsculas y minúsculas en tablas, vistas, rutinas, secuencias y sinónimos que no son del sistema

Contexto de Esquema

Herramientas de alto nivel para orientación rápida de esquema y redacción de SQL. En lugar de llamar manualmente listTables -> describeTable -> sampleRows para cada tabla, un LLM puede obtener contexto listo para usar en una llamada: tablas, columnas, relaciones, restricciones y filas de muestra.

HerramientaDescripción
tableContextContexto alrededor de una tabla: la tabla en sí, padres de FK y opcionalmente tablas hijas y bordes de relación. El recorrido de FK usa la profundidad solicitada (predeterminado 1, máximo 4). Parámetros: schema, table, depth, includeIncoming, includeStats, includeObserved
findJoinPathsEncontrar rutas JOIN entre dos tablas a través de FKs. El grafo se recorre en ambas direcciones y cada borde incluye joinCondition y un paquete evidence tipado (consulte Evidencia de borde a continuación). Parámetros: fromSchema / fromTable, toSchema / toTable, maxDepth (predeterminado/máximo 4), maxPaths (predeterminado 5, máximo 25), scanLimit (predeterminado/máximo 300), includeObserved
schemaBriefMapa de esquema completo en texto plano para redacción de SQL: todas las tablas/vistas coincidentes con recuentos de columnas, PK, recuentos de relaciones entrantes/salientes, columnas tipo clave, tablas centrales/aisladas y relaciones FK de clave limitadas. Use esto primero cuando las tablas relevantes sean desconocidas; siga con queryContext para contexto detallado. Parámetros: schema, terms (búsqueda de subcadena opcional), maxTables (límite de seguridad; predeterminado 2000, máximo 5000)
schemaGraphMétricas del grafo de relaciones de esquema: nodos con grado de entrada/salida y clasificación, bordes, tablas centrales, tablas aisladas, componentes conectados y sugerencias de ciclos. Opcionalmente incluye la ruta más corta entre dos tablas
schemaLintAuditoría de lint de esquema: claves primarias faltantes, FKs sin índices, desajustes de tipo de FK, restricciones únicas anulables, columnas de estado/tipo sin restricciones CHECK, columnas *_id huérfanas, comentarios faltantes, tablas aisladas y tablas anchas. Las verificaciones son configurables a través de checks
queryContextConstruir contexto compacto de redacción de SQL a partir de términos de búsqueda y/o tablas explícitas. Encuentra tablas y columnas relevantes usando nombres/comentarios de esquema declarados más evidencia semántica del catálogo de uso cuando está disponible, incluye restricciones y valores permitidos, relaciones y rutas JOIN entre tablas seleccionadas y opcionalmente filas de muestra (hasta 3 por tabla)
schemaGraphDotRepresentación DOT/Graphviz del grafo de relaciones de esquema. Los nodos son tablas con todas las columnas y tipos (PK y FK marcados en línea), los bordes incluyen condiciones JOIN. Parámetros: schema, tables (filtro opcional separado por comas)

Evidencia de borde

Cuando includeObserved se deja sin configurar, tableContext / findJoinPaths lo habilitan automáticamente si el catálogo de uso local está habilitado (consulte Catálogo de uso a continuación). Cada borde de relación lleva entonces un paquete evidence tipado de tres capas. Cada capa es independientemente opcional y se omite cuando no hay señal:

  • declaredSchema — la relación es una clave foránea declarada en el catálogo de la base de datos. Incluye el nombre de la FK y las listas de columnas.
  • observedQuery — el par de equi-join aparece en consultas de aplicación almacenadas. Incluye joinSupport (número de consultas distintas) y queryUids (hasta 5 uids contribuyentes).
  • semanticUsage — términos compartidos entre consultas que tocan ambas tablas: dominios de negocio, objetos de negocio y etiquetas de salida, más el recuento de consultas co-ocurrentes y la vista previa de uids. Esta capa solo decora aristas existentes — nunca propone nuevas relaciones.
{
  "relationshipType": "foreignKey",
  "fromTable": "ORDERS", "fromColumns": ["CUSTOMER_ID"],
  "toTable": "CUSTOMERS", "toColumns": ["ID"],
  "evidence": {
    "declaredSchema": { "foreignKeyName": "FK_ORDERS_CUSTOMER", "fromColumns": ["CUSTOMER_ID"], "toColumns": ["ID"] },
    "observedQuery": { "joinSupport": 18, "queryUids": ["SHOP/InvoiceReport.json#header"] },
    "semanticUsage": {
      "sharedBusinessDomains": [{ "value": "Customers", "support": 12, "queryUids": [...] }],
      "sharedBusinessObjects": [{ "value": "Invoice payer", "support": 4, "queryUids": [...] }],
      "sharedOutputLabels":     [{ "value": "Payer name",   "support": 3, "queryUids": [...] }],
      "coOccurringQueryCount": 22,
      "coOccurringQueryUids": [...]
    }
  }
}

Los pares de equi-join vistos solo en consultas almacenadas (sin FK declarada) se añaden como nuevas aristas con relationshipType: "observed" y undirected: true, entre tablas ya en alcance. Las FKs compuestas (multi-columna) reciben una capa declaredSchema pero sin coincidencia de par observado en esta iteración. schemaBrief, schemaGraph y queryContext solo muestran relaciones de FK declaradas.

Modelo de evidencia

La capa de contexto de esquema mantiene tres fuentes de conocimiento separadas:

  • declared_schema - introspección en vivo de la base de datos: tablas, columnas, PK/FK, índices, restricciones, comentarios y estadísticas.
  • observed_query - el catálogo de consultas indexado: qué consultas de aplicación/informe almacenadas referencian una tabla o columna, y en qué contexto SQL (select, where, join, order_by, having).
  • semantic_usage - significado de negocio proporcionado por el adaptador: dominios/etiquetas de consulta, etiquetas de salida, descripciones de parámetros, usos de campo, objetos de negocio renderizados y confianza.

En tableContext, los campos de tabla existentes son la vista compacta declared_schema. Cuando includeObserved está habilitado y el catálogo de uso está disponible, cada tabla también recibe un bloque evidence:

{
  "evidence": {
    "observedQuery": {
      "queryCount": 12,
      "queryUids": ["SHOP/reports/customer-card#main"],
      "columns": [
        {"column": "STATUS", "queryCount": 5, "contexts": [{"value": "where", "support": 4}]}
      ]
    },
    "semanticUsage": {
      "businessDomains": [{"value": "Customers", "support": 8}],
      "businessTags": [{"value": "customer", "support": 6}],
      "queryLabels": [{"value": "Customer card", "support": 3}],
      "outputLabels": [{"value": "Customer name", "support": 4}],
      "businessObjects": [{"value": "Customer card", "support": 3}]
    }
  }
}

El servidor trata esto como evidencia, no como un único modelo de negocio canónico. Diferentes consultas pueden legítimamente asignar diferentes roles de negocio a la misma tabla o columna física.

queryContext también usa semantic_usage como señal de descubrimiento. Cuando el usuario pasa lenguaje natural terms, el servidor busca en dominios del catálogo de uso, etiquetas, etiquetas de consulta, etiquetas de salida y objetos de negocio. Las tablas coincidentes se devuelven en semanticMatches y se consideran antes del escaneo de respaldo por nombre/comentario sobre metadatos de esquema en vivo. Esto permite que términos como "pagador" encuentren una tabla física CUSTOMERS cuando los informes existentes exponen customers.name como "Nombre del pagador".

Catálogo de uso

Un catálogo de uso SQL conocido contra la base de datos inspeccionada, junto con contexto de negocio opcional: parámetros con descripciones, columnas de salida con su significado, y dónde se muestra cada salida en el artefacto consumidor (celda de Excel en un informe BI Publisher, widget de panel, etc.).

Hay dos fuentes. El uso respaldado por archivos proviene de directorios / archivos JSON / archivos zip que contienen registros JSON canónicos de QueryUsage. El uso nativo de la base de datos se deriva automáticamente de las vistas, rutinas y disparadores del esquema conectado. En tiempo de ejecución, el servidor analiza estos registros y construye un índice SQLite persistente con tablas / columnas / pares de equi-join extraídos como hechos. Los archivos JSON siguen siendo autoritativos para registros respaldados por archivos; los registros nativos se actualizan desde metadatos en vivo.

Por qué existe esto. Las herramientas de metadatos responden "qué tablas y columnas existen". El catálogo de uso responde "cómo las usan realmente las aplicaciones". Con ambos, un LLM puede reemplazar suposiciones sobre joins no declarados con razonamiento basado en evidencia ("estas dos columnas se unen en 17 informes de producción, aquí están sus uids").

Identidad. Cada consulta se identifica por (source.kind, source.path, source.unit). Los diagnósticos y la evidencia renderizan esta clave como:

{source.kind}/{source.path}#{source.unit}

El sufijo #unit se omite cuando no hay unidad. Ejemplos:

bi-publisher-report/reports/customers/CustomerCard.xdo#CUST
manual/manual/ad-hoc-2026-05-01
java-dao/src/main/java/com/example/shop/OrderDao.java#findByCustomer

source.kind y source.unit no deben contener / o #; source.path no debe contener #. Para claves de origen duplicadas, el primer registro gana para esa construcción de índice.

Dónde viven los archivos. El directorio de catálogo predeterminado es <data-dir>/<connection>/usage-catalog. Configure usageCatalogPaths en la conexión como una lista de directorios adicionales, archivos .json o archivos .zip. Los directorios se escanean recursivamente para *.json; los archivos zip se escanean para entradas JSON. Establezca usageCatalogEnabled: false para deshabilitar el catálogo. usageCatalogStatus entonces informa catalogEnabled: false; otras herramientas públicas de uso devuelven un error argument explicando cómo habilitarlo.

Uso nativo de la base de datos. El catálogo también indexa objetos de base de datos soportados del esquema predeterminado:

  • vistas / vistas materializadas como source.kind="database-view" o source.kind="database-materialized-view";
  • funciones y procedimientos como source.kind="database-function" / source.kind="database-procedure" donde el motor informa esa distinción;
  • disparadores como source.kind="database-trigger".

Las vistas generalmente contribuyen evidencia completamente analizada de tablas, columnas y joins. Los cuerpos de rutinas y disparadores son específicos del motor, por lo que el indexador primero usa un pre-extractor procedural basado en ANTLR para encontrar declaraciones SELECT / WITH / INSERT / UPDATE / DELETE / MERGE incrustadas, luego alimenta esas declaraciones en el pipeline de análisis JSqlParser existente. Si no se encuentra ninguna declaración incrustada, el objeto se mantiene como registro de procedencia. Use usageNativeSchemas en la conexión para escanear esquemas explícitos.

Índice persistente. El servidor nunca construye el índice de uso al inicio. La primera búsqueda en el catálogo de uso lo construye sincrónicamente desde registros respaldados por archivos y objetos nativos de la base de datos en el <catalog>.db SQLite local. Los archivos fuente y los objetos de la base de datos siguen siendo autoritativos. Use invalidateUsageCatalogCache después de cambiarlos; limpia las filas de uso indexadas y la siguiente búsqueda las reconstruye.

Escrituras solo locales. El catálogo de uso nunca escribe en la base de datos JDBC inspeccionada (PostgreSQL / Oracle / SQL Server). Las protecciones existentes de ReadOnlyGuard y a nivel de conexión permanecen en vigor.

Carga útil tipada. Los objetos canónicos source, parameters[], outputs[], fieldUsages[] y anidados se describen en el JSON Schema (nombres de campo, tipos, descripciones, valores de enumeración). Los mismos tipos de registro (QueryUsage y amigos en usage/format/) se usan para la indexación de archivos.

El formato JSON canónico independiente de la fuente está documentado en docs/usage-catalog-format.md; su JSON Schema vive en src/main/resources/schemas/query-usage-record.schema.json, con ejemplos bajo examples/usage/. Los adaptadores específicos de fuente deben emitir esta forma canónica en lugar de implementarse dentro del servidor MCP JDBC.

HerramientaDescripción
usageCatalogStatusEstado actual del catálogo (not_started, indexing, ready, failed o invalidated), indicador habilitado y fuentes configuradas
invalidateUsageCatalogCacheElimina el índice en tiempo de ejecución. La siguiente búsqueda lo reconstruye sincrónicamente desde archivos configurados y objetos nativos de la base de datos
getQueryRegistro completo seleccionado por sourceKind, sourcePath y sourceUnit opcional: encabezado, parámetros, tablas/columnas/pares de join analizados, salidas y usos de campo
listQueriesListado paginado con filtros opcionales: sourcePath (LIKE — % / _ permitidos), sourceKind, businessDomain, tag, parseStatus, searchText, limit, offset
findQueriesByTableTodas las consultas del catálogo que referencian una tabla dada. Coincidencia sin distinción de mayúsculas contra nombres de tabla resueltos por alias y en mayúsculas. Filtro schema opcional
findQueriesByColumnTodas las consultas del catálogo que referencian una columna dada, con el context SQL de la referencia (select / where / join / order_by / having). Filtros schema y table opcionales
observedRelationshipsAgrega pares de equi-join observados en consultas almacenadas, agrupados por (left_table.left_column = right_table.right_column) con recuento support y uids de consulta contribuyentes. Los joins no equi (BETWEEN, basados en funciones) se excluyen. Los mismos datos alimentan la capa observedQuery del paquete de relación evidence en tableContext / findJoinPaths
listKnownTagsEtiquetas actualmente usadas en el catálogo, con recuentos de consultas. Permite al agente reutilizar un vocabulario estable entre llamadas de ingesta
listKnownDomainsIgual para valores businessDomain
listKnownKindsTipos de fuente actualmente usados en el catálogo con sus recuentos de consultas. Ayuda al agente a descubrir valores válidos para el filtro listQueries sourceKind

Resolución. Durante la indexación, los calificadores de tabla / columna se resuelven económicamente a través del mapa de alias del analizador y se ponen en mayúsculas para coincidencia sin distinción de mayúsculas. Un esquema explícito en el SQL (SCHEMA.TABLE) se conserva textualmente. Las referencias de tabla sin calificar se resuelven como parte de la construcción del índice contra el esquema JDBC en vivo: exactamente una coincidencia llena el esquema, múltiples coincidencias se marcan ambiguous, y cero coincidencias permanecen unresolved.

Administración del catálogo

HerramientaDescripción
rebuildCatalogReconstruye la instantánea de estructura persistente y el índice de uso para schemas separados por comas (o el alcance configurado/predeterminado), hace checkpoint del WAL de SQLite, y devuelve la ruta <catalog>.db distribuible y la conexión para la que se construyó

Esta herramienta escribe solo en el catálogo local. No modifica la base de datos inspeccionada.

Conexiones

HerramientaDescripción
listConnectionsLista las bases de datos que sirve este servidor: name (el valor a pasar como connection), description, tipo de motor, esquema predeterminado, si ya existe un archivo de catálogo local, y si el pool se ha construido en este proceso

listConnections lee configuración y el sistema de archivos local solamente — no abre ninguna conexión de base de datos, por lo que aún responde cuando algunas de las bases de datos configuradas están caídas. En una instalación desconocida es la primera llamada que vale la pena hacer.

Instantánea de estructura persistente

Los metadatos estructurales (columnas, claves, índices, FKs, vistas, rutinas, disparadores, secuencias) se mantienen en una instantánea de estructura persistente almacenada en el archivo <catalog>.db SQLite local (el mismo archivo de base de datos que el catálogo de uso, bajo <data-dir>/<catalog>/). SQLite se ejecuta en modo WAL, por lo que Codex, Claude y otros procesos de agente local pueden usar el mismo catálogo concurrentemente. Esto acelera llamadas repetidas a tableContext, findJoinPaths, schemaLint, schemaGraph, queryContext, describeTable, searchObjects y al re-resolvedor del catálogo de uso. Las herramientas de estadísticas como tableStats, indexStats, columnStats y sampleRows no se almacenan en caché; sus contadores están en vivo.

La instantánea es autoritativa ("caché para siempre") — no hay TTL ni detección de obsolescencia. Se llena perezosamente (describeTable persiste cada tabla que carga) y se puede precargar para esquemas completos con la herramienta rebuildCatalog, que construye la instantánea de estructura y el índice de uso en un <catalog>.db distribuible. rebuildCatalog hace checkpoint del WAL antes de devolver. Limpie el catálogo mientras todos los procesos del servidor estén detenidos eliminando <catalog>.db y cualquier archivo <catalog>.db-wal / <catalog>.db-shm adyacente.

Los archivos H2 <catalog>.mv.db existentes no se convierten ni eliminan. En el primer inicio de SQLite, el servidor crea un nuevo <catalog>.db, registra una advertencia y deja el archivo heredado intacto; ejecute rebuildCatalog para poblar el nuevo catálogo.

Configuración:

  • structureSnapshotSchemas - esquemas a precargar en una reconstrucción completa (vacío → el esquema predeterminado).
  • structureSnapshotOracleColumnQueryTimeoutSeconds - tiempo de espera solo para Oracle para la consulta masiva de columna/valor predeterminado respaldada por DBMS_XMLGEN durante una reconstrucción completa (predeterminado 300; 0 lo deshabilita). Ambos son campos por conexión en connections.json.

Exploración de Datos

HerramientaDescripción
sampleRowsDevuelve unas pocas filas de una tabla o vista (LIMIT / FETCH FIRST / TOP según la base de datos). Parámetros: schema, table, limit (por defecto 10, máximo 100)

Selectividad y Distribución

HerramientaDescripción
columnStatsEstadísticas básicas de columna: total_rows, non_null_rows, distinct_values, min, max. Una agregación única de bajo costo cuando solo se necesitan los extremos

columnStats solo reporta extremos. Las otras herramientas responden "¿qué tan selectivo es este predicado?" y "¿qué tan sesgados están los valores en esta columna?", que es la información que un LLM necesita para elegir un índice o reescribir un JOIN de manera significativa.

HerramientaDescripción
columnDistributionValores Top-N más frecuentes de una columna más su proporción. Revela sesgo, por ejemplo 70% de filas con status='OK', donde un índice en status solo no es útil. Parámetros: schema, table, column, topN (por defecto 20, máximo 1000)
columnHistogramPercentiles P25 / P50 / P75 / P90 / P95 / P99 más min, max y conteo de nulos. Usa SQL:2003 WITHIN GROUP: percentile_cont para tipos numéricos y percentile_disc para todos los demás, incluyendo fechas, marcas de tiempo y texto
nullRatioUn solo escaneo para conteos de nulos / no nulos en cada columna de la tabla. Las columnas se ordenan por null_ratio descendente. sparse=true marca columnas donde más del 50% de las filas son nulas, que pueden ser candidatas para un índice parcial
estimateSelectivityEstima cuántas filas devolvería un predicado sin ejecutar la consulta, usando EXPLAIN en SELECT 1 FROM t WHERE <predicate>. Devuelve filas estimadas, conteo base de filas sin el filtro y selectividad. Útil para colocar el predicado más selectivo primero en un índice compuesto
joinCardinalityEstima el conteo de filas de salida de un JOIN sin ejecutarlo. Devuelve la estimación del planificador, conteos de filas por lado y selectivity_vs_cartesian. Soporta INNER, LEFT, RIGHT y FULL

Estadísticas de Objetos

Estas herramientas dan al LLM señales de escala y salud de objetos; sin ellas, el consejo de optimización se convierte en conjeturas. Los datos provienen de catálogos del sistema (pg_class, pg_stat_*, ALL_TABLES, ALL_INDEXES, DBA_SEGMENTS) y se agregan en el lado de Java.

HerramientaDescripción
tableStatsTamaños de tablas e índices en bytes, conteo estimado de filas, tuplas muertas en PostgreSQL, último vacuum/analyze y contadores de seq/idx scan. En Oracle, también incluye datos de DBA_SEGMENTS de mejor esfuerzo cuando están disponibles
indexStatsTamaño por índice, contador de escaneo, columnas, bandera único/primario y tipo de índice. Extras de PostgreSQL: idx_tup_read/fetch, pg_get_indexdef. Extras de Oracle: distinct_keys, clustering_factor, blevel, leaf_blocks, last_analyzed
unusedIndexesÍndices con cero escaneos en PostgreSQL (pg_stat_user_indexes). Los índices PK y UNIQUE se excluyen. En Oracle, devuelve una nota de diagnóstico porque ALL_INDEXES no expone contadores de uso; se necesita DBA_INDEX_USAGE 12.2+ o V$OBJECT_USAGE con ALTER INDEX ... MONITORING USAGE
redundantIndexesÍndices cuya lista de columnas es un prefijo estricto de otro índice en la misma tabla. Los índices únicos no se reportan porque eliminarlos quitaría una restricción. El tipo de índice debe coincidir
fkIndexCoverageClaves foráneas en el lado hijo que carecen de un índice de soporte, una causa clásica de DELETE / UPDATE CASCADE lentos y JOINs lentos. El resultado incluye suggested_index_columns listo para CREATE INDEX

Todas las herramientas son de solo lectura; los datos no se modifican.

Formato de Error

Todas las herramientas devuelven errores con la misma forma: JSON con campos error y kind.

{"error": "Only SELECT / WITH / EXPLAIN statements are allowed", "kind": "rejected"}
kindCuándo
sqlLa base de datos devolvió un SQLException por sintaxis, objeto faltante, permiso faltante y casos similares
argumentArgumento de herramienta inválido
rejectedEl guardián de solo lectura bloqueó la consulta antes de que llegara a la base de datos
not_foundgetViewDefinition, getRoutineDefinition o getTriggerDefinition no encontraron nada. El cuerpo de la respuesta también incluye missing y name
driver / unexpected / plan_parseFallo interno del controlador, fallo no manejado o fallo de análisis del plan

validateQuery usa su propia forma, sin kind; valid es el discriminador.

{"valid": true,  "parameters": 1, "columns": 3}
{"valid": false, "stage": "guard|params|driver", "error": "..."}

Protección de Solo Lectura

La protección está en capas y está diseñada principalmente para declaraciones accidentales de DELETE / DROP de un LLM, no para un actor malicioso. Un actor malicioso ya tiene la URL de la base de datos, el nombre de usuario y la contraseña — que es también la razón por la que el servidor mantiene las credenciales fuera del entorno, para que un agente no las adquiera casualmente.

  1. ReadOnlyGuard en el código del proyecto. Antes de enviar SQL a la base de datos, el servidor primero lo analiza con JSqlParser y verifica el AST. Solo se permite un único SELECT, WITH o EXPLAIN. Los CTEs de escritura, SELECT INTO y cláusulas de bloqueo como FOR UPDATE están prohibidos. Si JSqlParser no puede analizar SQL específico del dialecto, el guardián recurre a la verificación léxica más antigua: primer token significativo, rechazo de múltiples declaraciones, omisión de comentarios y detección de palabras clave de escritura fuera de cadenas e identificadores entre comillas.
  2. connection.setReadOnly(true). Establecido por Hikari y nuevamente por este servidor en cada checkout.
  3. PostgreSQL: default_transaction_read_only=on. Añadido a la URL JDBC automáticamente a menos que ya hayas proporcionado tu propio options=. Incluso el DDL del lado del servidor se rechaza.
  4. Oracle: sugerencia JDBC de solo lectura. Oracle JDBC trata setReadOnly(true) principalmente como una sugerencia de asesoramiento. El guardián del lado del cliente y un usuario de base de datos dedicado de solo lectura son las protecciones principales de Oracle. Oracle EXPLAIN PLAN escribe un plan estático en PLAN_TABLE; este servidor limita esas lecturas con un STATEMENT_ID generado.
  5. SQL Server: sugerencia JDBC de solo lectura más planes estimados SHOWPLAN. SQL Server también trata setReadOnly(true) como una sugerencia. Usa un inicio de sesión/usuario de privilegios mínimos para una aplicación fuerte. explainQuery y analyzePlan usan SHOWPLAN_TEXT/XML, que devuelve planes estimados sin ejecutar la declaración.

Protección Máxima: Usar un Usuario de Base de Datos de Solo Lectura

Si puedes dedicar cinco minutos, crea un usuario dedicado con permisos de solo lectura. Esta es la garantía más fuerte incluso si el guardián se desactiva accidentalmente.

PostgreSQL:

CREATE ROLE ai_readonly LOGIN PASSWORD 'strong-password';
GRANT CONNECT ON DATABASE mydb TO ai_readonly;
GRANT USAGE ON SCHEMA public TO ai_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
    GRANT SELECT ON TABLES TO ai_readonly;

Oracle:

CREATE USER ai_readonly IDENTIFIED BY "strong-password";
GRANT CREATE SESSION TO ai_readonly;
GRANT SELECT ANY DICTIONARY TO ai_readonly;  -- for metadata
-- For each required table/view:
GRANT SELECT ON app_schema.customers TO ai_readonly;
-- ...or a role collecting all SELECT grants:
-- CREATE ROLE ai_ro_role; GRANT ai_ro_role TO ai_readonly;

SQL Server:

CREATE LOGIN ai_readonly WITH PASSWORD = 'strong-password';
CREATE USER ai_readonly FOR LOGIN ai_readonly;
GRANT SELECT ON SCHEMA::dbo TO ai_readonly;
GRANT VIEW DEFINITION TO ai_readonly; -- for object definitions and richer metadata
GRANT SHOWPLAN TO ai_readonly;        -- for explainQuery/analyzePlan estimated plans

Desactivar el Guardián

Si necesitas llamar, por ejemplo, a un procedimiento almacenado con semántica de solo lectura que el guardián no permite, puedes desactivar la validación del lado del cliente:

"readonlyGuard": "off"

Las protecciones a nivel de conexión (setReadOnly y, en PostgreSQL, default_transaction_read_only) permanecen habilitadas. En Oracle y SQL Server, setReadOnly es de mejor esfuerzo; usa un usuario de base de datos de solo lectura para la garantía más fuerte.

Stack

  • Java 21, Spring Boot 4.0, Spring AI MCP 2.0.0-M6 (transporte stdio)
  • HikariCP a través de Spring Boot starter-jdbc
  • PostgreSQL JDBC 42.7.4
  • Oracle JDBC ojdbc11 23.6.0.24.10
  • Microsoft SQL Server JDBC 12.8.1
  • SQLite 3.51.3 WAL catalog (<catalog>.db) que contiene el índice de uso y la instantánea de estructura persistente
  • Gradle 9.3.1 con catálogo de versiones

Licencia

Este proyecto está licenciado bajo la Apache License, Versión 2.0. Ver LICENSE.

Las dependencias de ejecución y prueba están licenciadas por sus respectivos propietarios. Ver THIRD_PARTY_NOTICES.md, especialmente si distribuyes un fat jar compilado que contenga controladores JDBC incluidos.

Compilación

# Set JDK 21+ explicitly if it is not your default JDK:
export JAVA_HOME="$HOME/.jdks/jdk-21.0.6"

./gradlew build

Resultado: build/libs/jdbc-mcp-server.jar (incluye controladores de PostgreSQL, Oracle y SQL Server).

Pruebas de Integración

Las pruebas de integración inician instancias reales de PostgreSQL, Oracle Free y SQL Server a través de Testcontainers, por lo que se requiere Docker. Están excluidas de la compilación regular y se ejecutan por separado:

./gradlew integrationTest

Para ejecutar solo el conjunto de pruebas de SQL Server Testcontainers:

./gradlew integrationTest --tests "*SqlServerIntegration*"

Las primeras ejecuciones de Oracle Free y SQL Server descargan imágenes grandes y pueden tardar varios minutos en iniciarse.

Pruebas de Humo Contra una Base de Datos Oracle Real

Si tienes acceso a una base de datos Oracle existente, puedes ejecutar pruebas de humo de solo lectura (LiveOracleIntegrationTest) directamente contra ella. Las pruebas ejecutan solo consultas SELECT contra el diccionario (DUAL, ALL_TABLES) y el esquema del usuario; no hay declaraciones CREATE / INSERT / UPDATE.

El nombre de usuario y la contraseña no se almacenan en el repositorio; se pasan a través de variables de entorno. Si no están configuradas, las pruebas se omiten silenciosamente y no rompen la compilación regular.

export LIVE_ORACLE_URL='jdbc:oracle:thin:@db.example.com:1521:ORCL'
export LIVE_ORACLE_USERNAME='ai_readonly'
export LIVE_ORACLE_PASSWORD='secret'
# optional, defaults to LIVE_ORACLE_USERNAME uppercased:
# export LIVE_ORACLE_SCHEMA='APP_SCHEMA'

./gradlew liveOracleTest

Windows (PowerShell):

$env:LIVE_ORACLE_URL      = 'jdbc:oracle:thin:@db.example.com:1521:ORCL'
$env:LIVE_ORACLE_USERNAME = 'ai_readonly'
$env:LIVE_ORACLE_PASSWORD = 'secret'
./gradlew liveOracleTest

.env está listado en .gitignore; si lo deseas, almacena variables allí y cárgalas antes de ejecutar pruebas, por ejemplo con direnv, dotenv-cli o set -a; . ./.env; set +a en bash. Gradle no analiza .env por sí mismo; las variables ya deben estar presentes en el entorno cuando Gradle se inicia.

Configuración

Las bases de datos, credenciales y todo lo que varía por base de datos viven en connections.json — deliberadamente no en el entorno. El entorno configura solo el proceso del servidor en sí:

VariableRequeridaDescripción
JDBC_MCP_CONNECTIONS_FILEnoRuta del archivo JSON que describe las conexiones nombradas que sirve este servidor; por defecto <data-dir>/connections.json. Un archivo faltante o vacío inicia el servidor sin conexiones (se registra una advertencia); uno malformado es un error de inicio
JDBC_MCP_DATA_DIRnoDirectorio raíz para datos locales del servidor, por defecto ~/.jdbc-mcp-server. Cada conexión obtiene su propio subdirectorio bajo él
JDBC_MCP_RESOURCES_ENABLEDnoExpone el manifiesto calificado por catálogo más recursos de tabla concretos y plantillas de recursos de tabla/columna; por defecto false
JDBC_MCP_TOOLS_*noAlternadores de herramientas por grupo que controlan qué herramientas aparecen en tools/list. Todos los grupos por defecto a true; establece un grupo a false para ocultarlo (útil para modelos de contexto pequeño). Ver Grupos de Herramientas

La configuración propia de una conexión — URL, credenciales, esquema por defecto, tiempos de espera, límites de filas, tamaños de pool, el guardián de solo lectura, opciones de instantánea y uso — son campos de su entrada connections.json; ver Campos de Conexión.

Ejecución

Con connections.json en su lugar:

java -jar jdbc-mcp-server.jar

(Usa build/libs/jdbc-mcp-server.jar si lo compilaste localmente, o el archivo descargado de Releases.)

El servidor inmediatamente comienza a escuchar MCP sobre stdin/stdout. Los registros se escriben en stderr. Las llamadas a herramientas abordan una base de datos por el nombre que tiene en el archivo: "connection": "myapp".

Docker

La imagen se publica en GHCR con cada lanzamiento. Monta el directorio que contiene connections.json en /data — también es donde el servidor mantiene sus catálogos y registros locales:

docker run -i --rm -v ~/.jdbc-mcp-server:/data ghcr.io/igorolv/jdbc-mcp-server:latest

El mismo comando es lo que un cliente MCP debería lanzar (-i mantiene stdin abierto para el transporte stdio). Las URLs JDBC en connections.json deben ser alcanzables desde dentro del contenedor: usa el nombre de host de la base de datos, no localhost, o añade --network host en Linux. Para compilar la imagen localmente:

docker build -t jdbc-mcp-server .

Conectar un Cliente de IA

Añade este servidor a la configuración del cliente:

{
  "command": "java",
  "args": ["-jar", "<absolute-path>/jdbc-mcp-server.jar"],
  "env": {}
}

No hay nada que poner en env: las bases de datos provienen de connections.json, y mantener las credenciales fuera de la configuración del cliente es el punto. Añade JDBC_MCP_CONNECTIONS_FILE solo si guardas el archivo en una ubicación distinta a la ruta predeterminada.

Dónde configurarlo

ClienteMétodo de conexión
Claude Codeclaude mcp add --scope user jdbc java -jar /path/to/jdbc-mcp-server.jar
Qwen Code~/.qwen/settings.json -> "mcpServers" -> "jdbc"
VS Code.vscode/mcp.json -> "servers" -> "jdbc"
Cursor.cursor/mcp.json -> "mcpServers" -> "jdbc"
Claude Desktopclaude_desktop_config.json -> "mcpServers" -> "jdbc"

Para Claude Code, omitir --scope user añade el servidor solo al proyecto actual. Comprueba la conexión con claude mcp list. Reinicia el cliente después de añadir el servidor.

Servir varias bases de datos desde un solo servidor

Un proceso de servidor puede servir cualquier número de bases de datos con nombre. El manifiesto de herramientas sigue siendo un único conjunto de 49 herramientas sin importar cuántas estén configuradas — cada herramienta toma connection como su primer argumento — y el pool de una base de datos, el catálogo local y los servicios se crean la primera vez que algo realmente solicita esa conexión.

Esto importa a escala: registrar quince instancias de servidor MCP coloca quince manifiestos de herramientas en el contexto del agente y quince JVMs en memoria, cuando la sesión puede terminar tocando dos de las bases de datos.

Cada base de datos es una entrada en connections.json; añadir una base de datos significa añadir una entrada y reiniciar el servidor.

Elegir una conexión

No hay conexión predeterminada: cada llamada a una herramienta nombra la base de datos a la que se refiere en su primer argumento. Un nombre faltante o desconocido devuelve un error argument que lista los nombres disponibles. Llama a listConnections para ver qué existe — solo lee la configuración, por lo que funciona incluso cuando algunas de las bases de datos configuradas están caídas.

Una sola base de datos

Nada cambia para una sola base de datos: un connections.json con una única entrada, y su nombre pasado como connection. No hay atajo de variable de entorno — un archivo es toda la configuración.

Aislamiento

  • Configurar una conexión no cuesta nada hasta que se usa: sin pool, sin archivo de catálogo, sin conexión.
  • Alcanzar la base de datos X abre pools solo para X.
  • Una base de datos que está caída, o una entrada cuya URL no es una URL JDBC compatible, hace fallar las llamadas realizadas contra ella y deja las demás conexiones funcionando. listConnections informa el motivo en configError.
  • Cada conexión mantiene su propio catálogo local en <data-dir>/<name>/<name>.db, por lo que las instantáneas de estructura y los índices de uso nunca se mezclan.
  • Los recursos MCP (cuando JDBC_MCP_RESOURCES_ENABLED=true) se publican para cada conexión configurada que ya tenga un archivo de catálogo local; los URI ya estaban calificados por catálogo.

El proceso único del servidor mantiene su registro rotativo compartido en <data-dir>/logs/jdbc-mcp-server.log. Las entradas de registro emitidas al manejar una llamada de herramienta incluyen su nombre connection; las entradas a nivel de proceso usan connection=server.

Una instancia por base de datos (el enfoque anterior)

Registrar una instancia de servidor por base de datos sigue funcionando y sigue siendo una opción razonable para una o dos bases de datos. El cliente organiza las herramientas por clave de servidor, a costa de un manifiesto de herramientas y una JVM por base de datos:

{
  "mcpServers": {
    "jdbc-orders": {
      "command": "java",
      "args": ["-jar", "<absolute-path>/jdbc-mcp-server.jar"],
      "env": {"JDBC_MCP_CONNECTIONS_FILE": "<absolute-path>/orders-connections.json"}
    },
    "jdbc-billing": {
      "command": "java",
      "args": ["-jar", "<absolute-path>/jdbc-mcp-server.jar"],
      "env": {"JDBC_MCP_CONNECTIONS_FILE": "<absolute-path>/billing-connections.json"}
    }
  }
}

No des a dos bases de datos el mismo nombre de conexión, en ninguna de las configuraciones: su índice de uso y la instantánea de estructura compartirían un único archivo <catalog>.db.

Estructura del proyecto

+-- src/main/java/ru/it_spectrum/ai/jdbc/mcp/
|   +-- JdbcMcpServerApplication.java   - Spring Boot entry point
|   +-- config/
|   |   +-- JdbcProperties.java         - connection settings from env
|   |   +-- JdbcMcpProperties.java      - local data directory and catalog name
|   |   +-- UsageProperties.java        - usage-catalog sources and native-object settings
|   |   +-- StructureSnapshotProperties.java - schemas captured by rebuildCatalog
|   |   +-- DatabaseKind.java           - PG/Oracle/SQL Server autodetection from URL
|   |   +-- DataSourceConfig.java       - Hikari pool builder + connection-level read-only mode
|   |   +-- ConnectionsConfig.java      - global defaults and the connection registry bean
|   +-- connection/
|   |   +-- ConnectionsFile.java        - connections.json shape
|   |   +-- ConnectionsLoader.java      - file + env defaults -> connection definitions
|   |   +-- EnvironmentPlaceholders.java - ${ENV_VAR} substitution
|   |   +-- ConnectionDefinition.java   - one named database and its effective settings
|   |   +-- ConnectionRegistry.java     - configured connections, lazily built, closed on shutdown
|   |   +-- ConnectionContext.java      - the service graph of one connection
|   |   +-- SpringConnectionContextFactory.java - builds it as a lazy child ApplicationContext
|   |   +-- ConnectionScopeConfig.java  - per-connection DataSource and DatabaseKind beans
|   +-- dialect/
|   |   +-- SqlDialect.java             - dialect interface
|   |   +-- PostgresDialect.java        - EXPLAIN, pg_catalog, pg_get_viewdef
|   |   +-- OracleDialect.java          - EXPLAIN PLAN, ALL_VIEWS, ALL_SOURCE, Oracle metadata queries
|   |   +-- SqlServerDialect.java       - SHOWPLAN, sys catalog metadata, SQL Server pagination
|   |   +-- DialectConfig.java          - implementation selection by DatabaseKind
|   +-- sql/
|   |   +-- ReadOnlyGuard.java          - JSqlParser AST guard + lexical fallback
|   |   +-- SqlNotAllowedException.java
|   |   +-- QueryResult.java            - result shape
|   |   +-- SqlExecutor.java            - query execution with limits
|   |   +-- BenchmarkService.java       - benchmark (cold+warm) and timed (+ pg_stat_statements diff)
|   +-- metadata/
|   |   +-- MetadataService.java        - DatabaseMetaData + dialect-specific metadata
|   |   +-- SqliteStructureSnapshotStore.java - persistent SQLite structure snapshot
|   |   +-- StatsService.java           - table/index stats, FK coverage, redundant/unused indexes
|   |   +-- DistributionService.java    - column distribution / histogram / null ratio / selectivity / join cardinality
|   |   +-- SchemaContextService.java   - high-level schema context: overview, table context, join paths, graph, lint, brief, query context
|   +-- plan/
|   |   +-- ParsedPlan.java / PlanNode.java - unified engine-agnostic plan model
|   |   +-- PlanParser.java             - parser interface
|   |   +-- PostgresPlanParser.java     - JSON EXPLAIN -> tree
|   |   +-- OraclePlanParser.java       - PLAN_TABLE -> tree
|   |   +-- SqlServerPlanParser.java    - SHOWPLAN_XML -> tree
|   |   +-- PlanAnalyzer.java           - summary: expensive / full scan / estimate error / nested loop / spill
|   +-- usage/
|   |   +-- CatalogDataSourceConfig.java - SQLite WAL datasource + schema init
|   |   +-- CatalogStorageService.java  - WAL checkpoint for distributable catalogs
|   |   +-- UsageCatalogService.java    - ingest, lookups, observed-relationships aggregation
|   |   +-- format/
|   |   |   +-- QueryUsage.java         - canonical query usage record DTO
|   +-- tools/
|       +-- QueryTools.java             - executeQuery, explainQuery, analyzePlan, validateQuery, inspectQuery, queryLint, resolveQueryLineage
|       +-- MetadataTools.java          - schemas / tables / describe / view / routines / sequences / search
|       +-- AdminTools.java             - rebuildCatalog (build structure snapshot + usage index into a distributable <catalog>.db)
|       +-- SampleTools.java            - sampleRows
|       +-- DistributionTools.java      - columnStats, columnDistribution, columnHistogram, nullRatio, estimateSelectivity, joinCardinality
|       +-- StatsTools.java             - tableStats, indexStats, unusedIndexes, redundantIndexes, fkIndexCoverage
|       +-- BenchmarkTools.java         - benchmarkQuery, timedQuery
|       +-- SchemaContextTools.java     - schemaBrief, tableContext, findJoinPaths, schemaLint, schemaGraph, queryContext, schemaGraphDot
|       +-- UsageTools.java             - usageCatalogStatus, invalidateUsageCatalogCache, getQuery, listQueries, findQueriesBy(Table|Column), observedRelationships, listKnownTags/Domains/Kinds
+-- src/main/resources/
    +-- application.yml                 - MCP stdio + JDBC properties
    +-- usage-catalog-schema.sql        - DDL for the usage-catalog index (in <catalog>.db)
    +-- structure-snapshot-schema.sql   - DDL for the persistent structure snapshot (in <catalog>.db)
    +-- logback-spring.xml              - logs to stderr because stdout is used by MCP

Solución de problemas

  • "Cannot find a Java installation ... matching languageVersion=21" - instala JDK 21+ y establece JAVA_HOME. Las cadenas de herramientas de Gradle no pueden descargarlo sin acceso a internet.
  • Conexión rechazada / ORA-01017 / FATAL / error de inicio de sesión en SQL Server - comprueba el url, username y password de la conexión en connections.json. Para PostgreSQL, prueba la URL con psql; para Oracle, usa sqlplus user/password@...; para SQL Server, prueba con sqlcmd -S host,1433 -d database -U user -P password.
  • {"kind":"rejected","error":"Only SELECT / WITH / EXPLAIN statements are allowed"} - la protección funcionó. Esto es esperado para cualquier operación de escritura. Si la consulta es realmente de solo lectura, por ejemplo una llamada a una función de solo lectura a través de SELECT func(...), pasará. Para casos completamente no triviales, puedes desactivar la protección con "readonlyGuard": "off" en esa conexión.
  • El intento de escritura en Oracle llegó a la base de datos - esto normalmente debería estar bloqueado por la protección primero. Si readonlyGuard es off, confía en un usuario de Oracle de solo lectura; setReadOnly(true) de JDBC es solo una sugerencia de mejor esfuerzo para Oracle.
  • Resultado vacío de describeTable / listTables en Oracle - Oracle almacena los nombres de objetos en mayúsculas. Pasa CUSTOMERS, no customers.
  • Errores de certificado en SQL Server - establece explícitamente las opciones de cifrado de la URL JDBC, por ejemplo encrypt=true;trustServerCertificate=false con un certificado de confianza, o trustServerCertificate=true solo para uso local/de desarrollo.
  • unusedIndexes no compatible en SQL Server - esta herramienta evita intencionalmente sys.dm_db_index_usage_stats porque normalmente requiere permisos elevados de vista de estado. Usa indexStats, fkIndexCoverage y redundantIndexes para auditorías de SQL Server con privilegios bajos.