jdbc-mcp-server

Acesso somente leitura ao PostgreSQL, Oracle e SQL Server para agentes de IA: descoberta de esquema, validação de consultas, planos de execução, benchmarking e análise de índices/estatísticas. Drivers JDBC incluídos

Documentação

Servidor MCP JDBC

CI Release License Java 21 MCP

Um servidor MCP local para acesso somente leitura a bancos de dados PostgreSQL, Oracle e Microsoft SQL Server. Ele permite que agentes de IA como Claude Code, Cursor, VS Code Copilot e outros escrevam consultas SQL, inspecionem planos de execução e explorem a estrutura do banco de dados: tabelas, colunas, índices, chaves estrangeiras, visões, funções e sequências.

Os drivers JDBC para PostgreSQL, Oracle e Microsoft SQL Server estão incluídos no fat jar, portanto não é necessária instalação adicional de drivers.

O servidor expõe 49 ferramentas MCP e pode opcionalmente expor recursos MCP qualificados por catálogo para metadados de tabelas e colunas. As ferramentas podem atualizar o catálogo SQLite local, mas nunca gravam no banco de dados PostgreSQL, Oracle ou SQL Server inspecionado.

Um único processo de servidor pode atender vários bancos de dados: nomeie-os em connections.json e passe connection para qualquer ferramenta. O manifesto de ferramentas permanece um conjunto único, independentemente de quantos bancos de dados estejam configurados, e os pools são abertos apenas para os bancos de dados realmente utilizados.

Início Rápido

1. Obtenha o jar — baixe jdbc-mcp-server.jar do último release (requer JDK 21+; todos os drivers JDBC estão incluídos), ou compile você mesmo:

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

2. Descreva seus bancos de dados em ~/.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 um usuário de banco de dados somente leitura; cinco minutos lá valem mais do que qualquer outra proteção neste servidor.

3. Registre o servidor no seu cliente MCP — sem configurações de banco de dados no config do cliente:

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

Para Claude Code, isso é um único comando:

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

4. Peça ao agente por listConnections. Ele responde com os bancos de dados que este servidor atende; toda outra ferramenta recebe esse nome como primeiro argumento:

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

Detalhes completos: Bancos de Dados e Credenciais, Conectando um Cliente de IA, Atendendo Vários Bancos de Dados a Partir de Um Servidor.

Bancos de Dados e Credenciais

Cada banco de dados atendido por este servidor é descrito em um único arquivo JSON. Nada sobre um banco de dados — URL, credenciais, esquema, timeouts, limites — vem do ambiente.

O arquivo de conexões

Caminho padrão ~/.jdbc-mcp-server/connections.json (<data-dir>/connections.json), substituído por JDBC_MCP_CONNECTIONS_FILE. Quando o arquivo está ausente ou não define nenhuma conexão, o servidor ainda inicia (para que um cliente MCP possa listar suas ferramentas), registra um aviso e listConnections retorna uma lista vazia; toda outra ferramenta então informa que nenhuma conexão está disponível. Um arquivo presente, mas malformado, é um erro de inicialização.

{
  "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)"
    }
  }
}

A chave do objeto é o nome da conexão. É também o nome do diretório de catálogo local da conexão (<data-dir>/<name>/) e aparece nos URIs de recursos MCP, portanto deve corresponder a [A-Za-z0-9._-]+(@[A-Za-z0-9._-]+)?, ter no máximo 64 caracteres e não ser . ou ...

O @ opcional existe para nomear os dois eixos separadamente: <service>@<stand>, como em ssj@dev, nsi@dev, ssj@tst. Um hífen não consegue fazer esse trabalho, porque hífens já ocorrem dentro de nomes de serviço (ssj-ws, ssj-ek-export, ais-ui), então ssj-ws-dev é ambíguo tanto para humanos quanto para modelos. @ nunca ocorre em um nome de serviço e é lido como "o quê, onde" da mesma forma que user@host. Ele é codificado em percentual para %40 nos URIs de recursos; nada mais no nome muda — o diretório em disco é o nome como escrito.

url é o único campo obrigatório; o mecanismo é detectado pelo seu prefixo (jdbc:postgresql:, jdbc:oracle:, jdbc:sqlserver:). description é texto livre retornado por listConnections, então um agente pode escolher um banco de dados pelo significado em vez do nome — vale a pena preencher.

Qualquer valor de string pode referenciar uma variável de ambiente como ${VAR}. Uma variável referenciada que não está definida falha na inicialização com uma mensagem nomeando a variável e o campo; ela nunca se torna uma senha vazia. Leia a próxima seção antes de usar isso.

Por que as credenciais ficam em um arquivo, não em variáveis de ambiente

O objetivo deste servidor é que o agente alcance o banco de dados apenas através dele: cada instrução passa pelo guarda de somente leitura, cada resultado é limitado por maxRows, e nada além de SELECT / WITH / EXPLAIN passa.

Credenciais em variáveis de ambiente minam exatamente isso. Elas são definidas no processo do servidor pelo cliente MCP, o que significa que também ficam na configuração do próprio cliente — um arquivo que agentes leem e editam rotineiramente — e no ambiente de qualquer shell que o iniciou. Um agente que viu uma URL, um usuário e uma senha não precisa mais das ferramentas: psql, sqlplus, sqlcmd ou três linhas de Python conectam direto ao banco de dados, sem guarda, sem limite de linhas e sem rastro no log deste servidor.

Portanto, o servidor não aceita nenhuma credencial de banco de dados do ambiente — não existem variáveis JDBC_URL / JDBC_USERNAME / JDBC_PASSWORD. Elas ficam em connections.json, que apenas o servidor lê.

Seja claro sobre o que isso compra e o que não compra:

  • Remove o caminho fácil. As credenciais deixam de fazer parte do material que um agente manipula rotineiramente: configs de cliente MCP, ambiente de shell, dumps de env em logs e relatórios de bugs.
  • Não é uma sandbox. Um agente com acesso ao shell executando como você pode ler o arquivo; chmod 600 mantém outros usuários fora, não um processo executando como seu usuário.
  • A garantia que sobrevive a tudo é um usuário de banco de dados somente leitura. O arquivo estreita a superfície de ataque; as permissões do próprio banco de dados a fecham.

Pela mesma razão, prefira uma senha literal no arquivo em vez de uma referência ${VAR} cuja variável estaria definida no bloco env do cliente MCP — isso coloca o segredo de volta exatamente onde o agente procura. ${VAR} ganha seu lugar quando o valor é injetado de fora do alcance do agente (uma unit do systemd, um script wrapper, um gerenciador de segredos), ou quando o próprio arquivo é compartilhado ou commitado e o segredo não deve estar nele.

Campos de conexão

Tudo exceto url é opcional; um campo omitido recai no padrão embutido:

CampoPadrãoSignificado
urlobrigatórioURL JDBC; também seleciona o mecanismo
username, passwordnenhumCredenciais do banco de dados
descriptionnenhumTexto livre retornado por listConnections
defaultSchemao esquema da sessãoEsquema usado quando uma chamada de ferramenta de metadados omite um
queryTimeoutSeconds30Timeout por consulta; 0 desativa
maxRows1000Limite de linhas para uma resposta; truncated: true quando atingido
fetchSize500Dica JDBC fetchSize
readonlyGuardstrictoff desativa a verificação somente-SELECT no lado do cliente
poolMaximumSize40Tamanho máximo do pool Hikari
poolMinimumIdle0Mínimo ocioso Hikari; 0 mantém o pool preguiçoso
poolConnectionTimeoutMs10000Timeout de checkout de conexão Hikari
poolValidationTimeoutMs5000Timeout de validação Hikari
poolIdleTimeoutMs60000Conexões ociosas acima de poolMinimumIdle são fechadas após isso
structureSnapshotSchemaso esquema padrãoEsquemas capturados por rebuildCatalog
structureSnapshotOracleColumnQueryTimeoutSeconds300Timeout somente Oracle para a consulta em massa de colunas durante rebuildCatalog; 0 desativa
usageCatalogEnabledtrueQuando false, ferramentas de uso relatam o estado desativado
usageCatalogPathsnenhumDiretórios extras, arquivos JSON ou arquivos zip com registros QueryUsage
usageNativeSchemaso esquema padrãoEsquemas varridos para uso nativo
usageNativeIncludeViews, usageNativeIncludeRoutines, usageNativeIncludeTriggerstrueO que a varredura de uso nativo cobre
usageNativeMaxObjects10000Máximo de registros de uso nativo por construção de índice

As flags do grupo JDBC_MCP_TOOLS_* permanecem no ambiente — elas moldam o manifesto de ferramentas, que é compartilhado por todas as conexões. Veja Configuração para as poucas variáveis que o próprio servidor lê.

Exemplos 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 Que Isso Existe

Cenário: você pede a um LLM para "verificar o banco de dados e mostrar quantos pedidos tivemos por status no mês passado." Sem este servidor, o LLM pode:

  • inventar nomes de tabelas e colunas;
  • perder detalhes reais do esquema, como campos anuláveis, tipos e chaves estrangeiras;
  • acidentalmente gerar um DELETE ou TRUNCATE enquanto "raciocina."

Com este servidor, o LLM pode:

  1. chamar schemaBrief para descobrir o mapa do esquema, ou queryContext para obter contexto detalhado pronto para uso: tabelas, colunas, relacionamentos e restrições;
  2. refinar o contexto com tableContext em torno de uma tabela específica ou findJoinPaths para descoberta de caminhos JOIN;
  3. escrever uma consulta e opcionalmente chamar inspectQuery, queryLint ou resolveQueryLineage para verificações de AST, metadados e linhagem de visões/rotinas;
  4. chamar validateQuery com o mesmo params ou namedParams que será usado para execução, validando a sintaxe sem executar a consulta;
  5. chamar explainQuery quando um plano for necessário;
  6. chamar executeQuery para buscar dados.

Qualquer consulta não-SELECT é bloqueada antes de chegar ao banco de dados.

Além da introspecção de esquema ao vivo, o servidor também mantém um catálogo de uso local de consultas SQL conhecidas usadas por aplicações e relatórios contra o banco de dados inspecionado, juntamente com seu contexto de negócio — significados de parâmetros, descrições de colunas de saída e onde cada saída é renderizada (célula do Excel, widget de dashboard, região do BI Publisher). Isso permite que o LLM responda a perguntas como "quais relatórios de produção já tocam nesta coluna?" e "qual rótulo de negócio este campo tem no cartão do cliente?" com base em um corpo curado de evidências em vez de adivinhar a partir de nomes sozinhos. O catálogo também é a fonte do pacote tipado de três camadas evidence nas arestas de relacionamento, então joins não declarados observados em consultas de produção são tratados como dicas de primeira classe ao lado de FKs declaradas. Veja Catálogo de Uso abaixo.

Arquitetura

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

                                          read-only JDBC
                                          PG / Oracle / SQL Server

O protocolo é somente stdio. O cliente inicia o servidor como um processo filho. Um processo atende qualquer número de bancos de dados nomeados, todos declarados no arquivo de conexões; o pool de uma conexão é aberto na primeira vez que uma chamada de ferramenta o nomeia.

Recursos MCP

Quando JDBC_MCP_RESOURCES_ENABLED=true, o servidor expõe — para cada conexão configurada que já tenha um arquivo de catálogo local — um manifesto de catálogo concreto, um recurso concreto para cada tabela ou visão já persistida no snapshot de estrutura dessa conexão, e dois modelos de recurso parametrizados:

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> é o nome da conexão codificado em percentual. Ele é fixo por conexão quando o servidor inicia e não é uma variável de modelo que os clientes podem usar para trocar de banco de dados: uma leitura resolve a conexão a partir do URI que recebeu. Isso mantém os URIs de recursos inequívocos tanto entre as conexões de um servidor quanto entre várias instâncias registradas deste jar. O manifesto informa o tipo de banco de dados, versão do snapshot/tempo de build/esquemas cobertos e os modelos exatos para seu catálogo. Leituras de tabelas e colunas reutilizam MetadataService, portanto têm as mesmas semânticas de snapshot persistente e fallback ao vivo que describeTable. Recursos de tabela concretos são carregados dos snapshots SQLite locais quando o servidor MCP inicia — nenhum banco de dados é contatado e nenhum pool JDBC é criado para isso — então clientes com suporte a seletor de recursos MCP podem oferecer entradas como SSV.CUSTOMERS sem consultar o banco de dados ativo. Suas descrições compactas contêm o comentário do banco quando presente, as colunas de chave primária e mapeamentos de chaves estrangeiras de saída; listas de colunas e contagens são intencionalmente omitidas. Após executar rebuildCatalog em um servidor já em execução, reinicie ou reconecte essa instância do servidor MCP para atualizar sua lista de recursos concretos.

Recursos de coluna incluem a definição da coluna mais a posição de PK correspondente, restrições únicas, índices, chaves estrangeiras de saída/entrada e restrições CHECK. Segmentos de caminho de URI preservam maiúsculas/minúsculas e usam codificação percentual UTF-8. Recursos são desabilitados por padrão; habilitá-los não altera as ferramentas MCP.

Ferramentas MCP

As 49 ferramentas estão agrupadas abaixo por propósito.

Toda ferramenta recebe connection como seu primeiro argumento, obrigatório, nomeando o banco de dados para executar — incluindo instalações que servem exatamente um banco de dados. listConnections lista os nomes. Veja Servindo Vários Bancos de Dados a Partir de um Único Servidor.

Grupos de Ferramentas

As ferramentas são organizadas em grupos que podem ser ativados ou desativados independentemente com flags JDBC_MCP_TOOLS_*. Todos os grupos estão ativados por padrão, então o conjunto completo de ferramentas está disponível imediatamente. Desativar grupos reduz o manifesto tools/list, o que importa para modelos locais de contexto pequeno que, de outra forma, seriam inundados com esquemas de ferramentas antes da primeira chamada.

GrupoFlagPadrãoFerramentas
MetadadosJDBC_MCP_TOOLS_METADATAativadolistSchemas, listTables, describeTable, getTriggerDefinition, getViewDefinition, listRoutines, getRoutineDefinition, listSequences, searchObjects
ConsultaJDBC_MCP_TOOLS_QUERYativadoexecuteQuery
AdministraçãoJDBC_MCP_TOOLS_ADMINativadorebuildCatalog
AmostraJDBC_MCP_TOOLS_SAMPLEativadosampleRows
Análise de consultaJDBC_MCP_TOOLS_ANALYSISativadoexplainQuery, analyzePlan, validateQuery, inspectQuery, queryLint, resolveQueryLineage
DistribuiçãoJDBC_MCP_TOOLS_DISTRIBUTIONativadocolumnStats, columnDistribution, columnHistogram, nullRatio, estimateSelectivity, joinCardinality
EstatísticasJDBC_MCP_TOOLS_STATSativadotableStats, indexStats, unusedIndexes, redundantIndexes, fkIndexCoverage
BenchmarkJDBC_MCP_TOOLS_BENCHMARKativadobenchmarkQuery, timedQuery
Catálogo de usoJDBC_MCP_TOOLS_USAGEativadousageCatalogStatus, getQuery, listQueries, findQueriesByTable, findQueriesByColumn, observedRelationships, listKnownTags, listKnownDomains, listKnownKinds, invalidateUsageCatalogCache
Contexto de esquemaJDBC_MCP_TOOLS_SCHEMA_CONTEXTativadotableContext, findJoinPaths, schemaLint, schemaBrief, schemaGraph, queryContext, schemaGraphDot
ConexõesJDBC_MCP_TOOLS_CONNECTIONSativadolistConnections

Cada flag aceita true / false. Para um modelo local de contexto pequeno, desative os grupos que você não precisa — por exemplo, mantenha apenas Metadados + Consulta definindo o restante como false — para reduzir o manifesto a um conjunto mínimo de "explorar o esquema e executar uma consulta". As seções abaixo descrevem cada ferramenta independentemente do seu grupo.

Consulta

FerramentaDescrição
executeQueryExecuta uma instrução SELECT, WITH ou EXPLAIN. Parâmetros: sql, params (array para ?) ou namedParams (objeto para :name), limit, timeoutSeconds. O resultado é marcado com truncated: true se o limite de linhas for atingido
explainQueryRetorna o plano de execução. PostgreSQL: EXPLAIN (FORMAT TEXT). Oracle: EXPLAIN PLAN FOR mais DBMS_XPLAN.DISPLAY. SQL Server: SET SHOWPLAN_TEXT ON na mesma sessão. Parâmetros podem ser passados como params (?) ou namedParams (:name). analyze=true no PostgreSQL habilita EXPLAIN ANALYZE; tenha cuidado, pois a consulta é realmente executada. SQL Server atualmente retorna apenas planos estimados
analyzePlanResumo compacto do plano orientado a LLM em vez de um grande despejo de plano bruto: nós de maior custo, varreduras completas em tabelas grandes, erros de estimativa (planejador vs. realidade, requer analyze=true no PostgreSQL), loops aninhados arriscados com grande entrada externa e derramamentos de classificação em disco. PostgreSQL: EXPLAIN (FORMAT JSON) / EXPLAIN ANALYZE. Oracle: EXPLAIN PLAN mais PLAN_TABLE (analyze é ignorado porque o Oracle fornece um plano estático aqui). SQL Server: SET SHOWPLAN_XML ON plano estimado. Parâmetros podem ser passados como params (?) ou namedParams (:name)
validateQueryValida a sintaxe sem execução: guarda de somente leitura mais prepareStatement do driver, com um resumo inspection derivado de JSqlParser quando o parsing é bem-sucedido. Parâmetros podem ser passados como params (?) ou namedParams (:name). Útil para autocorreção do LLM
inspectQueryAnalisa SQL através de JSqlParser sem tocar no banco de dados e retorna um resumo AST: tabelas, aliases, CTEs, itens de seleção, junções, predicados, order by, colunas, parâmetros, recursos e avisos do parser
queryLintAnalisa SQL e combina o AST com verificações de metadados, índices e FKs. Retorna avisos consultivos como tabelas ou colunas desconhecidas, SELECT *, junções sem condições, FKs sem índices de suporte e colunas de predicado/order by que não são colunas líderes de índice. SQL não é executado
resolveQueryLineageResolve objetos diretos referenciados por uma consulta e expande recursivamente views/views materializadas do banco para tabelas físicas subjacentes. A expansão de funções/procedimentos é melhor esforço: instruções SELECT / WITH embutidas são extraídas do código-fonte da rotina quando disponível. Parâmetros: sql, schema, expandViews, expandRoutines, maxDepth

Benchmarking

Ferramentas para medir o custo real de uma consulta, para que o LLM não precise adivinhar a partir do plano e possa ver milissegundos reais e contadores de buffer.

FerramentaDescrição
benchmarkQueryExecuta a consulta coldRuns + warmRuns vezes (padrão: 1 fria + 3 quentes) e retorna min de relógio de parede, median e max para execuções quentes; execuções frias são relatadas separadamente. Parâmetros podem ser passados como params (?) ou namedParams (:name). limit e timeoutSeconds são obrigatórios; consultas sem limite são rejeitadas. Retorna o tamanho do último resultado (row_count, colunas, truncated), não as linhas
timedQueryexecuteQuery regular mais elapsed_ms de relógio de parede. Parâmetros podem ser passados como params (?) ou namedParams (:name). No PostgreSQL, também captura snapshots pg_stat_statements antes e depois da consulta; a diferença mostra quais IDs de consulta adicionaram calls, total_exec_time_ms, rows, shared_blks_hit e shared_blks_read, tornando claro onde o servidor gastou tempo. Requer pg_stat_statements (CREATE EXTENSION pg_stat_statements; mais shared_preload_libraries); se a extensão estiver ausente, retorna pg_stat_statements.available: false

Metadados

FerramentaDescrição
listSchemasLista esquemas. Esquemas de sistema são ocultados por padrão; use includeSystem=true para mostrar todos
listTablesLista tabelas e views em um esquema. Parâmetros: schema, namePattern (com % / _), types (separados por vírgula, por exemplo TABLE,VIEW,MATERIALIZED VIEW)
describeTableDescrição completa do objeto em uma chamada: colunas, chave primária, restrições únicas, índices, FKs de saída/entrada, restrições CHECK e valores permitidos, mais metadados compactos de triggers
getTriggerDefinitionCorpo do trigger para um trigger nomeado. Parâmetros: schema, table, trigger
getViewDefinitionDefinição SQL de uma view
listRoutinesFunções, procedimentos e pacotes em um esquema
getRoutineDefinitionCódigo-fonte de uma função ou procedimento. No Oracle, todas as linhas ALL_SOURCE são concatenadas em ordem
listSequencesSequências em um esquema, ou entre esquemas quando schema é omitido
searchObjectsBusca de substring sem diferenciar maiúsculas/minúsculas em tabelas não-sistema, views, rotinas, sequências e sinônimos

Contexto de Esquema

Ferramentas de alto nível para orientação rápida de esquema e autoria de SQL. Em vez de chamar manualmente listTables -> describeTable -> sampleRows para cada tabela, um LLM pode obter contexto pronto para uso em uma chamada: tabelas, colunas, relacionamentos, restrições e linhas de amostra.

FerramentaDescrição
tableContextContexto em torno de uma tabela: a própria tabela, pais de FK e, opcionalmente, tabelas filhas e arestas de relacionamento. A travessia de FK usa a profundidade solicitada (padrão 1, máx. 4). Parâmetros: schema, table, depth, includeIncoming, includeStats, includeObserved
findJoinPathsEncontra caminhos de JOIN entre duas tabelas através de FKs. O grafo é percorrido em ambas as direções e cada aresta inclui joinCondition e um pacote evidence tipado (veja Evidência de aresta abaixo). Parâmetros: fromSchema / fromTable, toSchema / toTable, maxDepth (padrão/máx. 4), maxPaths (padrão 5, máx. 25), scanLimit (padrão/máx. 300), includeObserved
schemaBriefMapa de esquema completo em texto simples para autoria de SQL: todas as tabelas/views correspondentes com contagens de colunas, PK, contagens de relacionamentos de entrada/saída, colunas semelhantes a chaves, tabelas centrais/isoladas e relacionamentos de FK limitados. Use isso primeiro quando tabelas relevantes forem desconhecidas; siga com queryContext para contexto detalhado. Parâmetros: schema, terms (busca de substring opcional), maxTables (limite de segurança; padrão 2000, máx. 5000)
schemaGraphMétricas do grafo de relacionamento do esquema: nós com grau de entrada/saída e classificação, arestas, tabelas centrais, tabelas isoladas, componentes conectados e dicas de ciclos. Opcionalmente inclui o caminho mais curto entre duas tabelas
schemaLintAuditoria de lint do esquema: chaves primárias ausentes, FKs sem índices, incompatibilidades de tipo de FK, restrições únicas anuláveis, colunas de status/tipo sem restrições CHECK, colunas *_id órfãs, comentários ausentes, tabelas isoladas e tabelas largas. Verificações são configuráveis através de checks
queryContextConstrói contexto compacto de autoria de SQL a partir de termos de busca e/ou tabelas explícitas. Encontra tabelas e colunas relevantes usando nomes/comentários de esquema declarados mais evidência semântica do catálogo de uso quando disponível, inclui restrições e valores permitidos, relacionamentos e caminhos de JOIN entre tabelas selecionadas e, opcionalmente, linhas de amostra (até 3 por tabela)
schemaGraphDotRepresentação DOT/Graphviz do grafo de relacionamento do esquema. Nós são tabelas com todas as colunas e tipos (PK e FK marcados inline), arestas incluem condições de JOIN. Parâmetros: schema, tables (filtro opcional separado por vírgula)

Evidência de aresta

Quando includeObserved é deixado sem definição, tableContext / findJoinPaths o habilitam automaticamente se o catálogo de uso local estiver habilitado (veja Catálogo de Uso abaixo). Cada aresta de relacionamento então carrega um pacote evidence tipado de três camadas. Cada camada é independentemente opcional e é omitida quando não há sinal:

  • declaredSchema — o relacionamento é uma chave estrangeira declarada no catálogo do banco de dados. Carrega o nome da FK e as listas de colunas.
  • observedQuery — o par de junção equi aparece em consultas de aplicativos armazenadas. Carrega joinSupport (número de consultas distintas) e queryUids (até 5 uids contribuintes).
  • semanticUsage — termos compartilhados entre consultas que tocam ambas as tabelas: domínios de negócio, objetos de negócio e rótulos de saída, além da contagem de consultas co-ocorrentes e a prévia de uids. Esta camada decora apenas arestas existentes — nunca propõe novos relacionamentos.
{
  "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": [...]
    }
  }
}

Pares de junção equi vistos apenas em consultas armazenadas (sem FK declarada) são anexados como novas arestas com relationshipType: "observed" e undirected: true, entre tabelas já no escopo. FKs compostas (multicoluna) recebem uma camada declaredSchema mas nenhuma correspondência de par observado nesta iteração. schemaBrief, schemaGraph e queryContext apenas revelam relacionamentos de FK declarados.

Modelo de evidência

A camada de contexto de esquema mantém três fontes de conhecimento separadas:

  • declared_schema — introspecção ao vivo do banco de dados: tabelas, colunas, PK/FK, índices, restrições, comentários e estatísticas.
  • observed_query — o catálogo de consultas indexado: quais consultas armazenadas de aplicativos/relatórios referenciam uma tabela ou coluna, e em qual contexto SQL (select, where, join, order_by, having).
  • semantic_usage — significado de negócio fornecido pelo adaptador: domínios/tags/rótulos de consulta, rótulos de saída, descrições de parâmetros, usos de campos, objetos de negócio renderizados e confiança.

Em tableContext, os campos de tabela existentes são a visão compacta declared_schema. Quando includeObserved está habilitado e o catálogo de uso está disponível, cada tabela também recebe um bloco 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}]
    }
  }
}

O servidor trata isso como evidência, não como um modelo de negócio canônico único. Consultas diferentes podem legitimamente atribuir papéis de negócio diferentes à mesma tabela ou coluna física.

queryContext também usa semantic_usage como um sinal de descoberta. Quando o usuário passa linguagem natural terms, o servidor pesquisa domínios, tags, rótulos de consulta, rótulos de saída e objetos de negócio do catálogo de uso. Tabelas correspondentes são retornadas em semanticMatches e são consideradas antes da varredura de fallback por nome/comentário sobre metadados de esquema ao vivo. Isso permite que termos como "pagador" encontrem uma tabela física CUSTOMERS quando relatórios existentes expõem customers.name como "Nome do pagador".

Catálogo de Uso

Um catálogo de uso SQL conhecido contra o banco de dados inspecionado, juntamente com contexto de negócio opcional: parâmetros com descrições, colunas de saída com seu significado e onde cada saída é exibida no artefato consumidor (célula Excel em um relatório BI Publisher, widget de painel, etc.).

Existem duas fontes. O uso baseado em arquivos vem de diretórios / arquivos JSON / arquivos zip contendo registros canônicos QueryUsage JSON. O uso nativo do banco de dados é derivado automaticamente das views, rotinas e triggers do esquema conectado. Em tempo de execução, o servidor analisa esses registros e constrói um índice SQLite persistente com tabelas / colunas / pares de junção equi extraídos como fatos. Arquivos JSON permanecem autoritativos para registros baseados em arquivos; registros nativos são atualizados a partir de metadados ao vivo.

Por que isso existe. As ferramentas de metadados respondem "quais tabelas e colunas existem". O catálogo de uso responde "como elas são realmente usadas pelos aplicativos". Com ambos, um LLM pode substituir suposições sobre junções não declaradas por raciocínio baseado em evidências ("essas duas colunas são unidas em 17 relatórios de produção, aqui estão seus uids").

Identidade. Cada consulta é identificada por (source.kind, source.path, source.unit). Diagnósticos e evidências renderizam essa chave como:

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

O sufixo #unit é omitido quando não há unidade. Exemplos:

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 e source.unit não devem conter / ou #; source.path não deve conter #. Para chaves de origem duplicadas, o primeiro registro vence para essa construção de índice.

Onde os arquivos ficam. O diretório padrão do catálogo é <data-dir>/<connection>/usage-catalog. Configure usageCatalogPaths na conexão como uma lista de diretórios adicionais, arquivos .json ou arquivos .zip. Diretórios são varridos recursivamente por *.json; arquivos zip são varridos por entradas JSON. Defina usageCatalogEnabled: false para desabilitar o catálogo. usageCatalogStatus então relata catalogEnabled: false; outras ferramentas públicas de uso retornam um erro argument explicando como habilitá-lo.

Uso nativo do banco de dados. O catálogo também indexa objetos de banco de dados suportados do esquema padrão:

  • views / materialized views como source.kind="database-view" ou source.kind="database-materialized-view";
  • funções e procedimentos como source.kind="database-function" / source.kind="database-procedure" onde o mecanismo relata essa distinção;
  • triggers como source.kind="database-trigger".

Views geralmente contribuem com evidência totalmente analisada de tabelas, colunas e junções. Corpos de rotinas e triggers são específicos do mecanismo, então o indexador primeiro usa um pré-extrator procedural baseado em ANTLR para encontrar declarações SELECT / WITH / INSERT / UPDATE / DELETE / MERGE embutidas, e então alimenta essas declarações no pipeline de análise JSqlParser existente. Se nenhuma declaração embutida for encontrada, o objeto ainda é mantido como um registro de proveniência. Use usageNativeSchemas na conexão para escanear esquemas explícitos.

Índice persistente. O servidor nunca constrói o índice de uso na inicialização. A primeira consulta ao catálogo de uso o constrói sincronamente a partir de registros baseados em arquivos e objetos nativos do banco de dados no <catalog>.db SQLite local. Arquivos de origem e objetos de banco de dados permanecem autoritativos. Use invalidateUsageCatalogCache após alterá-los; ele limpa as linhas de uso indexadas e a próxima consulta as reconstrói.

Escritas apenas locais. O catálogo de uso nunca escreve no banco de dados JDBC inspecionado (PostgreSQL / Oracle / SQL Server). As proteções existentes de ReadOnlyGuard e de nível de conexão permanecem em vigor.

Payload tipado. Os objetos canônicos source, parameters[], outputs[], fieldUsages[] e aninhados são descritos pelo JSON Schema (nomes de campos, tipos, descrições, valores de enum). Os mesmos tipos de registro (QueryUsage e amigos em usage/format/) são usados pela indexação de arquivos.

O formato JSON canônico e independente de fonte é documentado em docs/usage-catalog-format.md; seu JSON Schema está em src/main/resources/schemas/query-usage-record.schema.json, com exemplos em examples/usage/. Adaptadores específicos de fonte devem emitir essa forma canônica em vez de serem implementados dentro do servidor MCP JDBC.

FerramentaDescrição
usageCatalogStatusEstado atual do catálogo (not_started, indexing, ready, failed ou invalidated), flag habilitado e fontes configuradas
invalidateUsageCatalogCacheDescarta o índice em tempo de execução. A próxima consulta o reconstrói sincronamente a partir de arquivos configurados e objetos nativos do banco de dados
getQueryRegistro completo selecionado por sourceKind, sourcePath e sourceUnit opcional: cabeçalho, parâmetros, tabelas/colunas/pares de junção analisados, saídas e usos de campos
listQueriesListagem paginada com filtros opcionais: sourcePath (LIKE — % / _ permitidos), sourceKind, businessDomain, tag, parseStatus, searchText, limit, offset
findQueriesByTableTodas as consultas do catálogo que referenciam uma determinada tabela. Correspondência sem distinção de maiúsculas/minúsculas contra nomes de tabela em maiúsculas e resolvidos por alias. Filtro schema opcional
findQueriesByColumnTodas as consultas do catálogo que referenciam uma determinada coluna, com o context SQL da referência (select / where / join / order_by / having). Filtros schema e table opcionais
observedRelationshipsAgrega pares de junção equi observados entre consultas armazenadas, agrupados por (left_table.left_column = right_table.right_column) com contagem de support e uids de consultas contribuintes. Junções não equi (BETWEEN, baseadas em função) são excluídas. Os mesmos dados alimentam a camada observedQuery do pacote de relacionamento evidence em tableContext / findJoinPaths
listKnownTagsTags atualmente usadas no catálogo, com contagens de consultas. Permite que o agente reutilize um vocabulário estável entre chamadas de ingestão
listKnownDomainsO mesmo para valores businessDomain
listKnownKindsTipos de fonte atualmente usados no catálogo com suas contagens de consultas. Ajuda o agente a descobrir valores válidos para o filtro listQueries sourceKind

Resolução. Durante a indexação, qualificadores de tabela / coluna são resolvidos de forma barata através do mapa de aliases do analisador e colocados em maiúsculas para correspondência sem distinção de maiúsculas/minúsculas. Um esquema explícito no SQL (SCHEMA.TABLE) é preservado literalmente. Referências de tabela não qualificadas são resolvidas como parte da construção do índice contra o esquema JDBC ao vivo: exatamente uma correspondência preenche o esquema, múltiplas correspondências são marcadas como ambiguous, e zero correspondências permanecem como unresolved.

Administração do Catálogo

FerramentaDescrição
rebuildCatalogReconstrói o instantâneo de estrutura persistente e o índice de uso para schemas separados por vírgula (ou o escopo configurado/padrão), faz checkpoint do WAL do SQLite e retorna o caminho <catalog>.db distribuível e a conexão para a qual foi construído

Esta ferramenta escreve apenas no catálogo local. Ela não modifica o banco de dados inspecionado.

Conexões

FerramentaDescrição
listConnectionsLista os bancos de dados que este servidor atende: name (o valor a passar como connection), description, tipo de mecanismo, esquema padrão, se um arquivo de catálogo local já existe e se o pool foi construído neste processo

listConnections lê configuração e o sistema de arquivos local apenas — ele não abre conexão de banco de dados, então ainda responde quando alguns dos bancos de dados configurados estão fora do ar. Em uma instalação desconhecida, é a primeira chamada que vale a pena fazer.

Instantâneo de Estrutura Persistente

Metadados estruturais (colunas, chaves, índices, FKs, views, rotinas, triggers, sequências) são mantidos em um instantâneo de estrutura persistente armazenado no arquivo <catalog>.db SQLite local (o mesmo arquivo de banco de dados do catálogo de uso, em <data-dir>/<catalog>/). O SQLite roda em modo WAL, então Codex, Claude e outros processos de agente locais podem usar o mesmo catálogo simultaneamente. Isso acelera chamadas repetidas para tableContext, findJoinPaths, schemaLint, schemaGraph, queryContext, describeTable, searchObjects e o re-resolvedor do catálogo de uso. Ferramentas de estatísticas como tableStats, indexStats, columnStats e sampleRows não são armazenadas em cache; seus contadores são ao vivo.

O instantâneo é autoritativo ("cache para sempre") — não há TTL ou detecção de obsolescência. Ele é preenchido preguiçosamente (describeTable persiste cada tabela que carrega) e pode ser pré-carregado para esquemas inteiros com a ferramenta rebuildCatalog, que constrói o instantâneo de estrutura e o índice de uso em um <catalog>.db distribuível. rebuildCatalog faz checkpoint do WAL antes de retornar. Limpe o catálogo enquanto todos os processos do servidor estão parados excluindo <catalog>.db e quaisquer arquivos <catalog>.db-wal / <catalog>.db-shm adjacentes.

Arquivos H2 <catalog>.mv.db existentes não são convertidos ou excluídos. Na primeira inicialização do SQLite, o servidor cria um novo <catalog>.db, registra um aviso e deixa o arquivo legado intocado; execute rebuildCatalog para popular o novo catálogo.

Configuração:

  • structureSnapshotSchemas — esquemas para pré-carregar em uma reconstrução completa (vazio → o esquema padrão).
  • structureSnapshotOracleColumnQueryTimeoutSeconds — timeout apenas para Oracle para a consulta em massa de colunas/padrões baseada em DBMS_XMLGEN durante uma reconstrução completa (padrão 300; 0 desabilita). Ambos são campos por conexão em connections.json.

Exploração de Dados

FerramentaDescrição
sampleRowsRetorna algumas linhas de uma tabela ou visão (LIMIT / FETCH FIRST / TOP dependendo do banco de dados). Parâmetros: schema, table, limit (padrão 10, máximo 100)

Seletividade e Distribuição

FerramentaDescrição
columnStatsEstatísticas básicas de coluna: total_rows, non_null_rows, distinct_values, min, max. Uma agregação única e barata quando apenas extremos são necessários

columnStats apenas relata extremos. As outras ferramentas respondem "quão seletivo é este predicado?" e "quão assimétricos são os valores nesta coluna?", que é a informação que um LLM precisa para escolher um índice ou reescrever um JOIN de forma significativa.

FerramentaDescrição
columnDistributionTop-N valores mais frequentes de uma coluna mais sua participação. Revela assimetria, por exemplo 70% das linhas com status='OK', onde um índice em status sozinho não é útil. Parâmetros: schema, table, column, topN (padrão 20, máximo 1000)
columnHistogramPercentis P25 / P50 / P75 / P90 / P95 / P99 mais min, max e contagem de nulos. Usa SQL:2003 WITHIN GROUP: percentile_cont para tipos numéricos e percentile_disc para todos os outros, incluindo datas, timestamps e texto
nullRatioUma varredura para contagens de nulos / não nulos em todas as colunas da tabela. As colunas são ordenadas por null_ratio decrescente. sparse=true marca colunas onde mais de 50% das linhas são nulas, que podem ser candidatas a um índice parcial
estimateSelectivityEstima quantas linhas um predicado retornaria sem executar a consulta, usando EXPLAIN em SELECT 1 FROM t WHERE <predicate>. Retorna linhas estimadas, contagem de linhas base sem o filtro e seletividade. Útil para colocar o predicado mais seletivo primeiro em um índice composto
joinCardinalityEstima a contagem de linhas de saída de um JOIN sem executá-lo. Retorna a estimativa do planejador, contagens de linhas por lado e selectivity_vs_cartesian. Suporta INNER, LEFT, RIGHT e FULL

Estatísticas de Objetos

Estas ferramentas dão ao LLM sinais de escala e saúde dos objetos; sem isso, conselhos de otimização viram adivinhação. Os dados vêm de catálogos do sistema (pg_class, pg_stat_*, ALL_TABLES, ALL_INDEXES, DBA_SEGMENTS) e são agregados no lado Java.

FerramentaDescrição
tableStatsTamanhos de tabela e índice em bytes, contagem estimada de linhas, tuplas mortas no PostgreSQL, último vacuum/analyze e contadores de seq/idx scan. No Oracle, também inclui dados DBA_SEGMENTS de melhor esforço quando disponíveis
indexStatsTamanho por índice, contador de scan, colunas, flag único/primário e tipo de índice. Extras do PostgreSQL: idx_tup_read/fetch, pg_get_indexdef. Extras do Oracle: distinct_keys, clustering_factor, blevel, leaf_blocks, last_analyzed
unusedIndexesÍndices com zero scans no PostgreSQL (pg_stat_user_indexes). Índices PK e UNIQUE são excluídos. No Oracle, retorna uma nota de diagnóstico porque ALL_INDEXES não expõe contadores de uso; DBA_INDEX_USAGE 12.2+ ou V$OBJECT_USAGE com ALTER INDEX ... MONITORING USAGE é necessário
redundantIndexesÍndices cuja lista de colunas é um prefixo estrito de outro índice na mesma tabela. Índices únicos não são relatados porque removê-los removeria uma restrição. O tipo de índice deve corresponder
fkIndexCoverageChaves estrangeiras no lado filho que não possuem um índice de suporte, uma causa clássica de DELETE / UPDATE CASCADE lentos e JOINs lentos. O resultado inclui suggested_index_columns pronto para CREATE INDEX

Todas as ferramentas são somente leitura; os dados não são modificados.

Formato de Erro

Todas as ferramentas retornam erros no mesmo formato: JSON com campos error e kind.

{"error": "Only SELECT / WITH / EXPLAIN statements are allowed", "kind": "rejected"}
kindQuando
sqlO banco de dados retornou um SQLException para erro de sintaxe, objeto ausente, permissão ausente e casos semelhantes
argumentArgumento de ferramenta inválido
rejectedO guarda de somente leitura bloqueou a consulta antes que ela chegasse ao banco de dados
not_foundgetViewDefinition, getRoutineDefinition ou getTriggerDefinition não encontraram nada. O corpo da resposta também inclui missing e name
driver / unexpected / plan_parseFalha interna do driver, falha não tratada ou falha de análise do plano

validateQuery usa seu próprio formato, sem kind; valid é o discriminador.

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

Proteção de Somente Leitura

A proteção é em camadas e é projetada principalmente para declarações acidentais de DELETE / DROP de um LLM, não para um ator malicioso. Um ator malicioso já tem a URL do banco de dados, nome de usuário e senha — que também é o motivo pelo qual o servidor mantém credenciais fora do ambiente, para que um agente não as adquira casualmente.

  1. ReadOnlyGuard no código do projeto. Antes de enviar SQL ao banco de dados, o servidor primeiro o analisa com JSqlParser e verifica o AST. Apenas um único SELECT, WITH ou EXPLAIN é permitido. CTEs de escrita, SELECT INTO e cláusulas de bloqueio como FOR UPDATE são proibidos. Se o JSqlParser não conseguir analisar SQL específico do dialeto, o guarda recorre à verificação léxica mais antiga: primeiro token significativo, rejeição de múltiplas declarações, pulo de comentários e detecção de palavras-chave de escrita fora de strings e identificadores entre aspas.
  2. connection.setReadOnly(true). Definido pelo Hikari e novamente por este servidor em cada checkout.
  3. PostgreSQL: default_transaction_read_only=on. Adicionado à URL JDBC automaticamente, a menos que você já tenha fornecido seu próprio options=. Até DDL no lado do servidor é rejeitado.
  4. Oracle: dica JDBC de somente leitura. O JDBC do Oracle trata setReadOnly(true) principalmente como uma dica consultiva. O guarda no lado do cliente e um usuário de banco de dados dedicado somente leitura são as principais proteções do Oracle. O Oracle EXPLAIN PLAN escreve um plano estático em PLAN_TABLE; este servidor limita essas leituras com um STATEMENT_ID gerado.
  5. SQL Server: dica JDBC de somente leitura mais planos estimados SHOWPLAN. O SQL Server também trata setReadOnly(true) como uma dica. Use um login/usuário de privilégio mínimo para aplicação forte. explainQuery e analyzePlan usam SHOWPLAN_TEXT/XML, que retorna planos estimados sem executar a declaração.

Proteção Máxima: Use um Usuário de Banco de Dados Somente Leitura

Se você puder gastar cinco minutos, crie um usuário dedicado com permissões somente leitura. Esta é a garantia mais forte, mesmo que o guarda seja acidentalmente desabilitado.

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

Desabilitando o Guarda

Se você precisar chamar, por exemplo, um procedimento armazenado com semântica somente leitura que o guarda não permite, você pode desabilitar a validação no lado do cliente:

"readonlyGuard": "off"

Proteções no nível da conexão (setReadOnly e, no PostgreSQL, default_transaction_read_only) permanecem habilitadas. No Oracle e SQL Server, setReadOnly é de melhor esforço; use um usuário de banco de dados somente leitura para a garantia mais forte.

Stack

  • Java 21, Spring Boot 4.0, Spring AI MCP 2.0.0-M6 (transporte stdio)
  • HikariCP através do 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) contendo o índice de uso e o snapshot de estrutura persistente
  • Gradle 9.3.1 com catálogo de versões

Licença

Este projeto é licenciado sob a Apache License, Versão 2.0. Veja LICENSE.

Dependências de runtime e teste são licenciadas por seus respectivos proprietários. Veja THIRD_PARTY_NOTICES.md, especialmente se você distribuir um fat jar compilado contendo drivers JDBC empacotados.

Build

# 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 (inclui drivers PostgreSQL, Oracle e SQL Server).

Testes de Integração

Os testes de integração iniciam instâncias reais de PostgreSQL, Oracle Free e SQL Server através de Testcontainers, então Docker é necessário. Eles são excluídos do build regular e executados separadamente:

./gradlew integrationTest

Para executar apenas a suíte SQL Server Testcontainers:

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

As primeiras execuções do Oracle Free e SQL Server baixam imagens grandes e podem levar vários minutos para iniciar.

Testes de Fumaça Contra um Banco de Dados Oracle Real

Se você tiver acesso a um banco de dados Oracle existente, você pode executar testes de fumaça somente leitura (LiveOracleIntegrationTest) diretamente contra ele. Os testes executam apenas consultas SELECT contra o dicionário (DUAL, ALL_TABLES) e o esquema do usuário; não há declarações CREATE / INSERT / UPDATE.

Nome de usuário e senha não são armazenados no repositório; eles são passados através de variáveis de ambiente. Se não forem definidos, os testes são pulados silenciosamente e não quebram o build 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 em .gitignore; se desejado, armazene variáveis lá e carregue-as antes de executar testes, por exemplo com direnv, dotenv-cli ou set -a; . ./.env; set +a no bash. O Gradle não analisa .env por si só; as variáveis já devem estar presentes no ambiente quando o Gradle inicia.

Configuração

Bancos de dados, credenciais e tudo que varia por banco de dados vivem em connections.json — deliberadamente não no ambiente. O ambiente configura apenas o processo do servidor em si:

VariávelObrigatóriaDescrição
JDBC_MCP_CONNECTIONS_FILEnãoCaminho do arquivo JSON que descreve as conexões nomeadas que este servidor atende; padrão <data-dir>/connections.json. Um arquivo ausente ou vazio inicia o servidor sem conexões (aviso registrado); um malformado é um erro de inicialização
JDBC_MCP_DATA_DIRnãoDiretório raiz para dados locais do servidor, padrão ~/.jdbc-mcp-server. Cada conexão recebe seu próprio subdiretório sob ele
JDBC_MCP_RESOURCES_ENABLEDnãoExpõe o manifesto qualificado por catálogo mais recursos de tabela concretos e modelos de recursos de tabela/coluna; padrão false
JDBC_MCP_TOOLS_*nãoAlternâncias de ferramentas por grupo que controlam quais ferramentas aparecem em tools/list. Todos os grupos padrão true; defina um grupo como false para ocultá-lo (útil para modelos de contexto pequeno). Veja Grupos de Ferramentas

As configurações de uma conexão — URL, credenciais, esquema padrão, timeouts, limites de linhas, tamanhos de pool, o guarda de somente leitura, opções de snapshot e uso — são campos de sua entrada connections.json; veja Campos de Conexão.

Executando

Com connections.json no lugar:

java -jar jdbc-mcp-server.jar

(Use build/libs/jdbc-mcp-server.jar se você compilou localmente, ou o arquivo baixado de Releases.)

O servidor imediatamente começa a ouvir MCP sobre stdin/stdout. Logs são escritos em stderr. Chamadas de ferramentas endereçam um banco de dados pelo nome que ele tem no arquivo: "connection": "myapp".

Docker

A imagem é publicada no GHCR a cada release. Monte o diretório que contém connections.json em /data — é também onde o servidor mantém seus catálogos e logs locais:

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

O mesmo comando é o que um cliente MCP deve iniciar (-i mantém stdin aberto para o transporte stdio). URLs JDBC em connections.json devem ser alcançáveis de dentro do contêiner: use o nome do host do banco de dados, não localhost, ou adicione --network host no Linux. Para compilar a imagem localmente:

docker build -t jdbc-mcp-server .

Conectando um Cliente de IA

Adicione este servidor à configuração do cliente:

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

Não há nada para colocar em env: os bancos de dados vêm de connections.json, e manter as credenciais fora da configuração do cliente é o ponto. Adicione JDBC_MCP_CONNECTIONS_FILE somente se você mantiver o arquivo em algum lugar diferente do caminho padrão.

Onde Configurar

ClienteMétodo de conexão
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 adiciona o servidor apenas ao projeto atual. Verifique a conexão com claude mcp list. Reinicie o cliente após adicionar o servidor.

Servindo Vários Bancos de Dados a partir de um Único Servidor

Um único processo de servidor pode atender qualquer número de bancos de dados nomeados. O manifesto de ferramentas permanece um único conjunto de 49 ferramentas, não importa quantas estejam configuradas — cada ferramenta recebe connection como seu primeiro argumento — e o pool de um banco de dados, o catálogo local e os serviços são criados na primeira vez que algo realmente solicita essa conexão.

Isso importa em escala: registrar quinze instâncias de servidor MCP coloca quinze manifestos de ferramentas no contexto do agente e quinze JVMs na memória, quando a sessão pode acabar tocando em dois dos bancos de dados.

Cada banco de dados é uma entrada em connections.json; adicionar um banco de dados significa adicionar uma entrada e reiniciar o servidor.

Escolhendo uma conexão

Não há conexão padrão: cada chamada de ferramenta nomeia o banco de dados que deseja em seu primeiro argumento. Um nome ausente ou desconhecido retorna um erro argument listando os nomes disponíveis. Chame listConnections para ver o que existe — ele lê apenas a configuração, então funciona mesmo quando alguns dos bancos de dados configurados estão fora do ar.

Um único banco de dados

Nada muda para um único banco de dados: um connections.json com uma única entrada, e seu nome passado como connection. Não há atalho de variável de ambiente — um arquivo é toda a configuração.

Isolamento

  • Configurar uma conexão não custa nada até que seja usada: sem pool, sem arquivo de catálogo, sem conexão.
  • Alcançar o banco de dados X abre pools apenas para X.
  • Um banco de dados que está fora do ar, ou uma entrada cuja URL não é uma URL JDBC suportada, falha nas chamadas feitas contra ele e deixa as outras conexões funcionando. listConnections relata o motivo em configError.
  • Cada conexão mantém seu próprio catálogo local em <data-dir>/<name>/<name>.db, então snapshots de estrutura e índices de uso nunca se misturam.
  • Recursos MCP (quando JDBC_MCP_RESOURCES_ENABLED=true) são publicados para cada conexão configurada que já possui um arquivo de catálogo local; URIs já eram qualificados por catálogo.

O processo único do servidor mantém seu log rotativo compartilhado em <data-dir>/logs/jdbc-mcp-server.log. Entradas de log emitidas durante o tratamento de uma chamada de ferramenta incluem seu nome connection; entradas de nível de processo usam connection=server.

Uma instância por banco de dados (a abordagem anterior)

Registrar uma instância de servidor por banco de dados ainda funciona e continua sendo uma escolha razoável para um ou dois bancos de dados. O cliente define namespaces para as ferramentas pela chave do servidor, ao custo de um manifesto de ferramentas e uma JVM por banco de dados:

{
  "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"}
    }
  }
}

Não dê a dois bancos de dados o mesmo nome de conexão, em qualquer configuração: seu índice de uso e snapshot de estrutura compartilhariam um único arquivo <catalog>.db.

Estrutura do Projeto

+-- 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

Solução de Problemas

  • "Cannot find a Java installation ... matching languageVersion=21" - instale JDK 21+ e defina JAVA_HOME. As toolchains do Gradle não podem baixá-lo sem acesso à internet.
  • Conexão recusada / ORA-01017 / FATAL / falha de login no SQL Server - verifique o url, username e password da conexão em connections.json. Para PostgreSQL, teste a URL com psql; para Oracle, use sqlplus user/password@...; para SQL Server, teste com sqlcmd -S host,1433 -d database -U user -P password.
  • {"kind":"rejected","error":"Only SELECT / WITH / EXPLAIN statements are allowed"} - a proteção funcionou. Isso é esperado para qualquer operação de escrita. Se a consulta for verdadeiramente somente leitura, por exemplo, uma chamada de função somente leitura via SELECT func(...), ela passará. Para casos totalmente não triviais, você pode desabilitar a proteção com "readonlyGuard": "off" nessa conexão.
  • Tentativa de escrita no Oracle alcançou o banco de dados - isso normalmente deve ser bloqueado pela proteção primeiro. Se readonlyGuard for off, confie em um usuário Oracle somente leitura; setReadOnly(true) do JDBC é apenas uma dica de melhor esforço para Oracle.
  • Resultado vazio de describeTable / listTables no Oracle - o Oracle armazena nomes de objetos em maiúsculas. Passe CUSTOMERS, não customers.
  • Erros de certificado no SQL Server - defina as opções de criptografia da URL JDBC explicitamente, por exemplo, encrypt=true;trustServerCertificate=false com um certificado confiável, ou trustServerCertificate=true apenas para uso local/desenvolvimento.
  • unusedIndexes não suportado no SQL Server - esta ferramenta evita intencionalmente sys.dm_db_index_usage_stats porque geralmente requer permissões elevadas de visualização de estado. Use indexStats, fkIndexCoverage e redundantIndexes para auditorias de baixo privilégio no SQL Server.