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
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
enven 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 600mantiene 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:
| Campo | Predeterminado | Significado |
|---|---|---|
url | obligatorio | URL JDBC; también selecciona el motor |
username, password | ninguno | Credenciales de la base de datos |
description | ninguno | Texto libre devuelto por listConnections |
defaultSchema | el esquema de la sesión | Esquema utilizado cuando una llamada de herramienta de metadatos omite uno |
queryTimeoutSeconds | 30 | Tiempo de espera por consulta; 0 lo desactiva |
maxRows | 1000 | Límite de filas para una respuesta; truncated: true cuando se alcanza |
fetchSize | 500 | Sugerencia JDBC fetchSize |
readonlyGuard | strict | off desactiva la verificación de solo SELECT del lado del cliente |
poolMaximumSize | 40 | Tamaño máximo del grupo Hikari |
poolMinimumIdle | 0 | Mínimo inactivo de Hikari; 0 mantiene el grupo perezoso |
poolConnectionTimeoutMs | 10000 | Tiempo de espera de obtención de conexión de Hikari |
poolValidationTimeoutMs | 5000 | Tiempo de espera de validación de Hikari |
poolIdleTimeoutMs | 60000 | Las conexiones inactivas por encima de poolMinimumIdle se cierran después de esto |
structureSnapshotSchemas | el esquema predeterminado | Esquemas capturados por rebuildCatalog |
structureSnapshotOracleColumnQueryTimeoutSeconds | 300 | Tiempo de espera solo de Oracle para la consulta de columnas masiva durante rebuildCatalog; 0 lo desactiva |
usageCatalogEnabled | true | Cuando false, las herramientas de uso informan el estado deshabilitado |
usageCatalogPaths | ninguno | Directorios adicionales, archivos JSON o archivos zip con registros QueryUsage |
usageNativeSchemas | el esquema predeterminado | Esquemas escaneados para uso nativo |
usageNativeIncludeViews, usageNativeIncludeRoutines, usageNativeIncludeTriggers | true | Qué cubre el escaneo de uso nativo |
usageNativeMaxObjects | 10000 | Má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
DELETEoTRUNCATEmientras "razona."
Con este servidor, el LLM puede:
- llamar a
schemaBriefpara descubrir el mapa del esquema, o aqueryContextpara obtener contexto detallado listo para usar: tablas, columnas, relaciones y restricciones; - refinar el contexto con
tableContextalrededor de una tabla específica ofindJoinPathspara el descubrimiento de rutas JOIN; - escribir una consulta y opcionalmente llamar a
inspectQuery,queryLintoresolveQueryLineagepara verificaciones de AST, metadatos y linaje de vistas/rutinas; - llamar a
validateQuerycon el mismoparamsonamedParamsque se usará para la ejecución, validando la sintaxis sin ejecutar la consulta; - llamar a
explainQuerycuando se necesite un plan; - llamar a
executeQuerypara 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.
| Grupo | Bandeera | Predeterminado | Herramientas |
|---|---|---|---|
| Metadatos | JDBC_MCP_TOOLS_METADATA | activado | listSchemas, listTables, describeTable, getTriggerDefinition, getViewDefinition, listRoutines, getRoutineDefinition, listSequences, searchObjects |
| Consulta | JDBC_MCP_TOOLS_QUERY | activado | executeQuery |
| Administración | JDBC_MCP_TOOLS_ADMIN | activado | rebuildCatalog |
| Muestra | JDBC_MCP_TOOLS_SAMPLE | activado | sampleRows |
| Análisis de consultas | JDBC_MCP_TOOLS_ANALYSIS | activado | explainQuery, analyzePlan, validateQuery, inspectQuery, queryLint, resolveQueryLineage |
| Distribución | JDBC_MCP_TOOLS_DISTRIBUTION | activado | columnStats, columnDistribution, columnHistogram, nullRatio, estimateSelectivity, joinCardinality |
| Estadísticas | JDBC_MCP_TOOLS_STATS | activado | tableStats, indexStats, unusedIndexes, redundantIndexes, fkIndexCoverage |
| Benchmark | JDBC_MCP_TOOLS_BENCHMARK | activado | benchmarkQuery, timedQuery |
| Catálogo de uso | JDBC_MCP_TOOLS_USAGE | activado | usageCatalogStatus, getQuery, listQueries, findQueriesByTable, findQueriesByColumn, observedRelationships, listKnownTags, listKnownDomains, listKnownKinds, invalidateUsageCatalogCache |
| Contexto de esquema | JDBC_MCP_TOOLS_SCHEMA_CONTEXT | activado | tableContext, findJoinPaths, schemaLint, schemaBrief, schemaGraph, queryContext, schemaGraphDot |
| Conexiones | JDBC_MCP_TOOLS_CONNECTIONS | activado | listConnections |
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
| Herramienta | Descripción |
|---|---|
executeQuery | Ejecutar 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 |
explainQuery | Devolver 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 |
analyzePlan | Resumen 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) |
validateQuery | Validar 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 |
inspectQuery | Analizar 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 |
queryLint | Analizar 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 |
resolveQueryLineage | Resolver 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.
| Herramienta | Descripción |
|---|---|
benchmarkQuery | Ejecutar 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 |
timedQuery | executeQuery 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
| Herramienta | Descripción |
|---|---|
listSchemas | Listar esquemas. Los esquemas del sistema están ocultos por defecto; use includeSystem=true para mostrar todos |
listTables | Listar tablas y vistas en un esquema. Parámetros: schema, namePattern (con % / _), types (separados por comas, por ejemplo TABLE,VIEW,MATERIALIZED VIEW) |
describeTable | Descripció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 |
getTriggerDefinition | Cuerpo del disparador para un disparador nombrado. Parámetros: schema, table, trigger |
getViewDefinition | Definición SQL de una vista |
listRoutines | Funciones, procedimientos y paquetes en un esquema |
getRoutineDefinition | Código fuente de función o procedimiento. En Oracle, todas las líneas ALL_SOURCE se concatenan en orden |
listSequences | Secuencias en un esquema, o en todos los esquemas cuando schema se omite |
searchObjects | Bú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.
| Herramienta | Descripción |
|---|---|
tableContext | Contexto 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 |
findJoinPaths | Encontrar 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 |
schemaBrief | Mapa 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) |
schemaGraph | Mé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 |
schemaLint | Auditorí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 |
queryContext | Construir 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) |
schemaGraphDot | Representació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. IncluyejoinSupport(número de consultas distintas) yqueryUids(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"osource.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.
| Herramienta | Descripción |
|---|---|
usageCatalogStatus | Estado actual del catálogo (not_started, indexing, ready, failed o invalidated), indicador habilitado y fuentes configuradas |
invalidateUsageCatalogCache | Elimina 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 |
getQuery | Registro completo seleccionado por sourceKind, sourcePath y sourceUnit opcional: encabezado, parámetros, tablas/columnas/pares de join analizados, salidas y usos de campo |
listQueries | Listado paginado con filtros opcionales: sourcePath (LIKE — % / _ permitidos), sourceKind, businessDomain, tag, parseStatus, searchText, limit, offset |
findQueriesByTable | Todas 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 |
findQueriesByColumn | Todas 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 |
observedRelationships | Agrega 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 |
listKnownTags | Etiquetas actualmente usadas en el catálogo, con recuentos de consultas. Permite al agente reutilizar un vocabulario estable entre llamadas de ingesta |
listKnownDomains | Igual para valores businessDomain |
listKnownKinds | Tipos 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
| Herramienta | Descripción |
|---|---|
rebuildCatalog | Reconstruye 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
| Herramienta | Descripción |
|---|---|
listConnections | Lista 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 porDBMS_XMLGENdurante una reconstrucción completa (predeterminado300;0lo deshabilita). Ambos son campos por conexión enconnections.json.
Exploración de Datos
| Herramienta | Descripción |
|---|---|
sampleRows | Devuelve 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
| Herramienta | Descripción |
|---|---|
columnStats | Estadí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.
| Herramienta | Descripción |
|---|---|
columnDistribution | Valores 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) |
columnHistogram | Percentiles 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 |
nullRatio | Un 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 |
estimateSelectivity | Estima 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 |
joinCardinality | Estima 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.
| Herramienta | Descripción |
|---|---|
tableStats | Tamañ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 |
indexStats | Tamañ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 |
fkIndexCoverage | Claves 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"}
kind | Cuándo |
|---|---|
sql | La base de datos devolvió un SQLException por sintaxis, objeto faltante, permiso faltante y casos similares |
argument | Argumento de herramienta inválido |
rejected | El guardián de solo lectura bloqueó la consulta antes de que llegara a la base de datos |
not_found | getViewDefinition, getRoutineDefinition o getTriggerDefinition no encontraron nada. El cuerpo de la respuesta también incluye missing y name |
driver / unexpected / plan_parse | Fallo 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.
- 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,WITHoEXPLAIN. Los CTEs de escritura,SELECT INTOy cláusulas de bloqueo comoFOR UPDATEestá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. connection.setReadOnly(true). Establecido por Hikari y nuevamente por este servidor en cada checkout.- PostgreSQL:
default_transaction_read_only=on. Añadido a la URL JDBC automáticamente a menos que ya hayas proporcionado tu propiooptions=. Incluso el DDL del lado del servidor se rechaza. - 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. OracleEXPLAIN PLANescribe un plan estático enPLAN_TABLE; este servidor limita esas lecturas con unSTATEMENT_IDgenerado. - 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.explainQueryyanalyzePlanusanSHOWPLAN_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
ojdbc1123.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í:
| Variable | Requerida | Descripción |
|---|---|---|
JDBC_MCP_CONNECTIONS_FILE | no | Ruta 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_DIR | no | Directorio raíz para datos locales del servidor, por defecto ~/.jdbc-mcp-server. Cada conexión obtiene su propio subdirectorio bajo él |
JDBC_MCP_RESOURCES_ENABLED | no | Expone 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_* | no | Alternadores 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
| Cliente | Método de conexión |
|---|---|
| Claude Code | claude 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 Desktop | claude_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
Xabre pools solo paraX. - 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.
listConnectionsinforma el motivo enconfigError. - 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,usernameypasswordde la conexión enconnections.json. Para PostgreSQL, prueba la URL conpsql; para Oracle, usasqlplus user/password@...; para SQL Server, prueba consqlcmd -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 deSELECT 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
readonlyGuardesoff, 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/listTablesen Oracle - Oracle almacena los nombres de objetos en mayúsculas. PasaCUSTOMERS, nocustomers. - Errores de certificado en SQL Server - establece explícitamente las opciones de cifrado de la URL JDBC, por ejemplo
encrypt=true;trustServerCertificate=falsecon un certificado de confianza, otrustServerCertificate=truesolo para uso local/de desarrollo. unusedIndexesno compatible en SQL Server - esta herramienta evita intencionalmentesys.dm_db_index_usage_statsporque normalmente requiere permisos elevados de vista de estado. UsaindexStats,fkIndexCoverageyredundantIndexespara auditorías de SQL Server con privilegios bajos.