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
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
envem logs e relatórios de bugs. - Não é uma sandbox. Um agente com acesso ao shell executando como você pode ler o arquivo;
chmod 600manté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:
| Campo | Padrão | Significado |
|---|---|---|
url | obrigatório | URL JDBC; também seleciona o mecanismo |
username, password | nenhum | Credenciais do banco de dados |
description | nenhum | Texto livre retornado por listConnections |
defaultSchema | o esquema da sessão | Esquema usado quando uma chamada de ferramenta de metadados omite um |
queryTimeoutSeconds | 30 | Timeout por consulta; 0 desativa |
maxRows | 1000 | Limite de linhas para uma resposta; truncated: true quando atingido |
fetchSize | 500 | Dica JDBC fetchSize |
readonlyGuard | strict | off desativa a verificação somente-SELECT no lado do cliente |
poolMaximumSize | 40 | Tamanho máximo do pool Hikari |
poolMinimumIdle | 0 | Mínimo ocioso Hikari; 0 mantém o pool preguiçoso |
poolConnectionTimeoutMs | 10000 | Timeout de checkout de conexão Hikari |
poolValidationTimeoutMs | 5000 | Timeout de validação Hikari |
poolIdleTimeoutMs | 60000 | Conexões ociosas acima de poolMinimumIdle são fechadas após isso |
structureSnapshotSchemas | o esquema padrão | Esquemas capturados por rebuildCatalog |
structureSnapshotOracleColumnQueryTimeoutSeconds | 300 | Timeout somente Oracle para a consulta em massa de colunas durante rebuildCatalog; 0 desativa |
usageCatalogEnabled | true | Quando false, ferramentas de uso relatam o estado desativado |
usageCatalogPaths | nenhum | Diretórios extras, arquivos JSON ou arquivos zip com registros QueryUsage |
usageNativeSchemas | o esquema padrão | Esquemas varridos para uso nativo |
usageNativeIncludeViews, usageNativeIncludeRoutines, usageNativeIncludeTriggers | true | O que a varredura de uso nativo cobre |
usageNativeMaxObjects | 10000 | Má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
DELETEouTRUNCATEenquanto "raciocina."
Com este servidor, o LLM pode:
- chamar
schemaBriefpara descobrir o mapa do esquema, ouqueryContextpara obter contexto detalhado pronto para uso: tabelas, colunas, relacionamentos e restrições; - refinar o contexto com
tableContextem torno de uma tabela específica oufindJoinPathspara descoberta de caminhos JOIN; - escrever uma consulta e opcionalmente chamar
inspectQuery,queryLintouresolveQueryLineagepara verificações de AST, metadados e linhagem de visões/rotinas; - chamar
validateQuerycom o mesmoparamsounamedParamsque será usado para execução, validando a sintaxe sem executar a consulta; - chamar
explainQueryquando um plano for necessário; - chamar
executeQuerypara 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.
| Grupo | Flag | Padrão | Ferramentas |
|---|---|---|---|
| Metadados | JDBC_MCP_TOOLS_METADATA | ativado | listSchemas, listTables, describeTable, getTriggerDefinition, getViewDefinition, listRoutines, getRoutineDefinition, listSequences, searchObjects |
| Consulta | JDBC_MCP_TOOLS_QUERY | ativado | executeQuery |
| Administração | JDBC_MCP_TOOLS_ADMIN | ativado | rebuildCatalog |
| Amostra | JDBC_MCP_TOOLS_SAMPLE | ativado | sampleRows |
| Análise de consulta | JDBC_MCP_TOOLS_ANALYSIS | ativado | explainQuery, analyzePlan, validateQuery, inspectQuery, queryLint, resolveQueryLineage |
| Distribuição | JDBC_MCP_TOOLS_DISTRIBUTION | ativado | columnStats, columnDistribution, columnHistogram, nullRatio, estimateSelectivity, joinCardinality |
| Estatísticas | JDBC_MCP_TOOLS_STATS | ativado | tableStats, indexStats, unusedIndexes, redundantIndexes, fkIndexCoverage |
| Benchmark | JDBC_MCP_TOOLS_BENCHMARK | ativado | benchmarkQuery, timedQuery |
| Catálogo de uso | JDBC_MCP_TOOLS_USAGE | ativado | usageCatalogStatus, getQuery, listQueries, findQueriesByTable, findQueriesByColumn, observedRelationships, listKnownTags, listKnownDomains, listKnownKinds, invalidateUsageCatalogCache |
| Contexto de esquema | JDBC_MCP_TOOLS_SCHEMA_CONTEXT | ativado | tableContext, findJoinPaths, schemaLint, schemaBrief, schemaGraph, queryContext, schemaGraphDot |
| Conexões | JDBC_MCP_TOOLS_CONNECTIONS | ativado | listConnections |
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
| Ferramenta | Descrição |
|---|---|
executeQuery | Executa 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 |
explainQuery | Retorna 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 |
analyzePlan | Resumo 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) |
validateQuery | Valida 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 |
inspectQuery | Analisa 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 |
queryLint | Analisa 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 |
resolveQueryLineage | Resolve 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.
| Ferramenta | Descrição |
|---|---|
benchmarkQuery | Executa 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 |
timedQuery | executeQuery 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
| Ferramenta | Descrição |
|---|---|
listSchemas | Lista esquemas. Esquemas de sistema são ocultados por padrão; use includeSystem=true para mostrar todos |
listTables | Lista tabelas e views em um esquema. Parâmetros: schema, namePattern (com % / _), types (separados por vírgula, por exemplo TABLE,VIEW,MATERIALIZED VIEW) |
describeTable | Descriçã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 |
getTriggerDefinition | Corpo do trigger para um trigger nomeado. Parâmetros: schema, table, trigger |
getViewDefinition | Definição SQL de uma view |
listRoutines | Funções, procedimentos e pacotes em um esquema |
getRoutineDefinition | Código-fonte de uma função ou procedimento. No Oracle, todas as linhas ALL_SOURCE são concatenadas em ordem |
listSequences | Sequências em um esquema, ou entre esquemas quando schema é omitido |
searchObjects | Busca 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.
| Ferramenta | Descrição |
|---|---|
tableContext | Contexto 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 |
findJoinPaths | Encontra 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 |
schemaBrief | Mapa 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) |
schemaGraph | Mé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 |
schemaLint | Auditoria 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 |
queryContext | Constró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) |
schemaGraphDot | Representaçã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. CarregajoinSupport(número de consultas distintas) equeryUids(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"ousource.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.
| Ferramenta | Descrição |
|---|---|
usageCatalogStatus | Estado atual do catálogo (not_started, indexing, ready, failed ou invalidated), flag habilitado e fontes configuradas |
invalidateUsageCatalogCache | Descarta 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 |
getQuery | Registro 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 |
listQueries | Listagem paginada com filtros opcionais: sourcePath (LIKE — % / _ permitidos), sourceKind, businessDomain, tag, parseStatus, searchText, limit, offset |
findQueriesByTable | Todas 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 |
findQueriesByColumn | Todas 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 |
observedRelationships | Agrega 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 |
listKnownTags | Tags atualmente usadas no catálogo, com contagens de consultas. Permite que o agente reutilize um vocabulário estável entre chamadas de ingestão |
listKnownDomains | O mesmo para valores businessDomain |
listKnownKinds | Tipos 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
| Ferramenta | Descrição |
|---|---|
rebuildCatalog | Reconstró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
| Ferramenta | Descrição |
|---|---|
listConnections | Lista 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 emDBMS_XMLGENdurante uma reconstrução completa (padrão300;0desabilita). Ambos são campos por conexão emconnections.json.
Exploração de Dados
| Ferramenta | Descrição |
|---|---|
sampleRows | Retorna 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
| Ferramenta | Descrição |
|---|---|
columnStats | Estatí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.
| Ferramenta | Descrição |
|---|---|
columnDistribution | Top-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) |
columnHistogram | Percentis 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 |
nullRatio | Uma 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 |
estimateSelectivity | Estima 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 |
joinCardinality | Estima 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.
| Ferramenta | Descrição |
|---|---|
tableStats | Tamanhos 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 |
indexStats | Tamanho 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 |
fkIndexCoverage | Chaves 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"}
kind | Quando |
|---|---|
sql | O banco de dados retornou um SQLException para erro de sintaxe, objeto ausente, permissão ausente e casos semelhantes |
argument | Argumento de ferramenta inválido |
rejected | O guarda de somente leitura bloqueou a consulta antes que ela chegasse ao banco de dados |
not_found | getViewDefinition, getRoutineDefinition ou getTriggerDefinition não encontraram nada. O corpo da resposta também inclui missing e name |
driver / unexpected / plan_parse | Falha 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.
- 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,WITHouEXPLAINé permitido. CTEs de escrita,SELECT INTOe cláusulas de bloqueio comoFOR UPDATEsã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. connection.setReadOnly(true). Definido pelo Hikari e novamente por este servidor em cada checkout.- PostgreSQL:
default_transaction_read_only=on. Adicionado à URL JDBC automaticamente, a menos que você já tenha fornecido seu própriooptions=. Até DDL no lado do servidor é rejeitado. - 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 OracleEXPLAIN PLANescreve um plano estático emPLAN_TABLE; este servidor limita essas leituras com umSTATEMENT_IDgerado. - 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.explainQueryeanalyzePlanusamSHOWPLAN_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
ojdbc1123.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ável | Obrigatória | Descrição |
|---|---|---|
JDBC_MCP_CONNECTIONS_FILE | não | Caminho 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_DIR | não | Diretó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_ENABLED | não | Expõ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ão | Alternâ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
| Cliente | Método de conexão |
|---|---|
| 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 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
Xabre pools apenas paraX. - 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.
listConnectionsrelata o motivo emconfigError. - 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,usernameepasswordda conexão emconnections.json. Para PostgreSQL, teste a URL compsql; para Oracle, usesqlplus user/password@...; para SQL Server, teste comsqlcmd -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 viaSELECT 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
readonlyGuardforoff, 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/listTablesno Oracle - o Oracle armazena nomes de objetos em maiúsculas. PasseCUSTOMERS, nãocustomers. - Erros de certificado no SQL Server - defina as opções de criptografia da URL JDBC explicitamente, por exemplo,
encrypt=true;trustServerCertificate=falsecom um certificado confiável, outrustServerCertificate=trueapenas para uso local/desenvolvimento. unusedIndexesnão suportado no SQL Server - esta ferramenta evita intencionalmentesys.dm_db_index_usage_statsporque geralmente requer permissões elevadas de visualização de estado. UseindexStats,fkIndexCoverageeredundantIndexespara auditorias de baixo privilégio no SQL Server.