SQLcl + MCP no Claude Code: do download à investigação de dados com IA (sem apagar nada)

Se você trabalha com Oracle, já deve ter aberto o SQL Developer só para rodar um SELECT rápido, ou lutado com o SQL*Plus para editar a linha anterior. O SQLcl resolve as duas coisas: é a linha de comando moderna da Oracle, leve, com autocomplete, histórico e gerenciamento de conexões. E, desde as versões recentes, ele traz um servidor MCP embutido, que permite que agentes de IA como o Claude Code (e também Codex, Copilot, Devin e companhia) conversem com o seu banco.

Neste artigo vamos do zero ao uso real: download, conexões salvas, exportar e importar conexões, ligar o MCP no Claude Code, investigar dados guiado pelas suas entidades JPA, gerar queries e, principalmente, colocar regras para a IA nunca apagar nem alterar nada sem pedir. No fim, criamos uma skill que conecta no banco com uma frase.

Tudo aqui foi executado de verdade. As saídas de terminal, os resultados das consultas e as respostas do Claude Code são reais, rodadas em 27/09/2026 com SQLcl 26.2.2, Oracle AI Database 26ai Free (23.26.3) e um projeto Spring Boot + JPA de exemplo. Senhas foram trocadas por asteriscos.

Sumário

  1. O que é o SQLcl
  2. Download e instalação
  3. Conectando no banco
  4. Conexões salvas e o connmgr
  5. Exportando e importando conexões
  6. O servidor MCP do SQLcl
  7. Configurando no Claude Code
  8. Adendo: Codex, Copilot, Devin e outros
  9. Regras primeiro: defesa em camadas
  10. Na prática: investigando dados guiado pelas entidades JPA
  11. Na prática: o Claude Code escrevendo queries
  12. Criando uma skill para conectar no banco
  13. Checklist final e referências

1. O que é o SQLcl

O SQLcl (Oracle SQL Developer Command Line) é uma interface de linha de comando em Java para o Oracle Database. Ele roda praticamente todos os scripts de SQL*Plus e acrescenta o que faltava: edição de várias linhas, autocomplete com Tab, histórico, formatos de saída (CSV, JSON, INSERT), comandos como INFO e DDL, Liquibase, Data Pump e um gerenciador de conexões. É gratuito, distribuído sob a Oracle Free Use License, e não precisa de Oracle Client instalado: basta Java.

A novidade que motiva este post é a opção -mcp: com ela o SQLcl sobe como um servidor Model Context Protocol, expondo ferramentas como “listar conexões”, “conectar” e “rodar SQL” para qualquer agente de IA compatível.

2. Download e instalação

A página oficial de download é oracle.com/database/sqldeveloper/technologies/sqlcl/download. Ela sempre aponta para a última versão, que também pode ser baixada direto por este link fixo, sem login:

https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip

Pré-requisito: Java 17 ou 21 (um JDK ou JRE). Para usar o MCP, a documentação exige uma dessas duas versões. Aqui eu usei o OpenJDK 21.

Linux e macOS

java -version            # confirme: 17 ou 21

cd /opt
sudo curl -LO https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip
sudo unzip -q sqlcl-latest.zip    # cria a pasta /opt/sqlcl

# coloque o sql no PATH (use ~/.zshrc no macOS)
echo 'export PATH=/opt/sqlcl/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

sql -V
# SQLcl: Release 26.2.2.0 Production Build: 26.2.2.233.1901

Windows

Baixe o zip, extraia em C:\ (vai criar C:\sqlcl) e adicione C:\sqlcl\bin ao PATH em Configurações › Sistema › Sobre › Configurações avançadas › Variáveis de ambiente. Pelo PowerShell:

Expand-Archive .\sqlcl-latest.zip -DestinationPath C:\
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\sqlcl\bin", "User")
# abra um novo terminal e confira
sql -V

Para atualizar depois, é só baixar o zip de novo e substituir a pasta. Suas conexões ficam fora dela (em ~/.dbtools), então nada se perde.

3. Conectando no banco

O jeito mais direto é o formato EZConnect usuario/senha@host:porta/servico. Para não deixar a senha no histórico do shell, prefira abrir sem conexão (/nolog) e deixar o SQLcl pedir a senha:

# direto (a senha fica no histórico do shell, evite)
sql loja/********@localhost:1521/FREEPDB1

# melhor: sem senha na linha de comando
sql /nolog
SQL> conn loja@localhost:1521/FREEPDB1     # o SQLcl pede a senha

# Autonomous Database com wallet
sql -cloudconfig Wallet_MeuBanco.zip admin@meubanco_high

Algumas opções úteis do sql (todas saem do sql -h): -S (modo silencioso, bom para scripts), -name <conexão> (usa uma conexão salva), -thin/-thick (driver JDBC), -nohistory, -noupdates e -R <nível> (restrição, que vai ser importante mais à frente).

4. Conexões salvas e o connmgr

Digitar host, porta e serviço toda vez cansa. O SQLcl salva conexões com nome, e esse é também o mecanismo que o MCP usa: o agente de IA só enxerga as conexões que você salvou. Para salvar, use -save no próprio connect. Por padrão a senha não é salva; com -savepwd ela vai para um wallet seguro. Para sobrescrever uma conexão existente, acrescente -replace.

O comando connmgr gerencia tudo depois disso. Na versão 26.2.2 ele tem os subcomandos list, show, test, add (pastas), move, rename, clone, delete, export e import. Veja uma sessão real, criando a conexão somente leitura que vamos usar no resto do artigo e organizando em uma pasta /dev:

sql /nolog — SQLcl 26.2.2
SQL> conn -save loja-dev-ro -savepwd loja_ro/********@localhost:1521/FREEPDB1
Name: loja-dev-ro
Connect String: localhost:1521/FREEPDB1
User: loja_ro
Password: ******
Connected.

SQL> connmgr add -folder /dev
Folder /dev has been added

SQL> connmgr move -conn loja-dev-ro /dev
Connection loja-dev-ro has been moved to /dev

SQL> connmgr list
.
└── dev
    ├── loja-dev
    └── loja-dev-ro

SQL> connmgr test loja-dev-ro
Oracle AI Database 26ai Free Release 23.26.3.0.0 - Develop, Learn, and Run for Free
Connection Test Successful
Salvando, organizando em pasta e testando uma conexão com o connmgr (saída real).

Dois detalhes que aprendi testando: conn -save dev/loja não cria pastas (a barra vira parte do nome). Crie a pasta com connmgr add -folder e use connmgr move. E para renomear, o comando é connmgr rename -conn nome-antigo nome-novo.

Com a conexão salva, conectar vira uma palavra:

sql -name loja-dev-ro          # já abre conectado

SQL> conn -name loja-dev       # troca de conexão dentro do SQLcl
SQL> connmgr show loja-dev-ro  # mostra detalhes (senha mascarada)
SQL> connmgr clone -original loja-dev -username outro_usuario loja-dev-outro
SQL> connmgr delete -conn loja-dev-outro

As conexões ficam em ~/.dbtools (no Windows, %USERPROFILE%\.dbtools), com as senhas guardadas em um wallet seguro.

5. Exportando e importando conexões

Trocou de máquina ou quer passar as conexões do time para um colega novo? O connmgr export gera um arquivo criptografado. A chave de criptografia não é digitada no comando: ela vem de um secret do SQLcl, criado com secret set. Atenção: nos meus testes o secret vale só para a sessão atual do SQLcl, então crie o secret e exporte (ou importe) na mesma sessão.

máquina antiga → máquina nova
-- máquina antiga
SQL> secret set chave_conexoes "Uma-Frase-Bem-Longa-2026!"
Secret chave_conexoes stored
SQL> connmgr export -key chave_conexoes -all-connections minhas-conexoes
Successfully exported 2 connection(s) to /home/carlos/minhas-conexoes

-- máquina nova (copie o arquivo e use a mesma frase)
SQL> secret set chave_conexoes "Uma-Frase-Bem-Longa-2026!"
Secret chave_conexoes stored
SQL> connmgr import -key chave_conexoes -list minhas-conexoes
Archive contains 2 connection(s):
loja-dev (ORACLE_DATABASE)
loja-dev-ro (ORACLE_DATABASE)
SQL> connmgr import -key chave_conexoes minhas-conexoes
Importing connection loja-dev: Success
Importing connection loja-dev-ro: Success
2 connection(s) processed
Exportando todas as conexões e importando em outra máquina (saída real).

Opções que valem conhecer:

  • export: passe nomes de conexões específicas em vez de -all-connections, ou -folder /dev para exportar uma pasta. -preserve-folders mantém a estrutura de pastas e -overwrite sobrescreve o arquivo.
  • import: -list só mostra o conteúdo, sem importar. -duplicates IGNORE|RENAME|REPLACE decide o que fazer com nomes repetidos (o padrão é IGNORE). -strip-passwords importa sem as senhas, ótimo para compartilhar com o time. O import também aceita o JSON exportado pelo SQL Developer.
  • Se aparecer “Warning: Could not add connection … to folder ”” durante o import, confira com connmgr list: nos meus testes as conexões foram importadas mesmo assim.

6. O servidor MCP do SQLcl

O MCP é um protocolo aberto que padroniza como um agente de IA descobre e chama ferramentas externas. Quando você roda sql -mcp, o SQLcl não abre o prompt SQL>: ele fica conversando JSON-RPC pela entrada e saída padrão (transporte stdio). Quem inicia o processo é o próprio cliente de IA.

🧑‍💻
Você
pergunta em português
→
Claude Code
ou Codex, Copilot, Devin…
CLAUDE.md · skills · hooks
MCP / stdio
⇄
SQLcl
sql -R 4 -mcp
conexões salvas em ~/.dbtools
JDBC
⇄
🛢️
Oracle
usuário somente leitura
O agente nunca vê a senha: ele pede ao SQLcl para usar uma conexão salva pelo nome.

Consultei a lista de ferramentas direto no servidor (sqlcl-mcp-server 26.2.2.0). A documentação ainda mostra nomes antigos com hífen, mas na versão atual eles são estes:

FerramentaO que faz
connections_listLista as conexões salvas (filtra por nome ou usuário)
connect / disconnectAbre ou fecha uma conexão salva, pelo nome
sql_runExecuta SQL ou PL/SQL e devolve o resultado em CSV
sqlcl_runExecuta comandos do SQLcl (INFO, DDL, Liquibase, Data Pump…)
schema_informationResumo do schema (tabelas, colunas, relacionamentos) em vários níveis de detalhe
request_statusAcompanha consultas assíncronas longas
skills_sync, annotation_generateRecursos extras (skills mantidas pela Oracle e anotações de schema)

Três comportamentos que você precisa conhecer antes de soltar uma IA no banco:

  • AUTOCOMMIT ligado. Ao conectar, o próprio servidor avisa “AUTOCOMMIT is ON”. Um DELETE executado pelo agente é gravado na hora, sem chance de ROLLBACK.
  • Tudo fica registrado. Cada consulta ganha o comentário /* LLM in use is <modelo> */ e, se o usuário puder criar tabelas, o SQLcl grava um log na tabela DBTOOLS$MCP_LOG do schema conectado. As colunas MODULE e ACTION de V$SESSION também mostram o cliente e o modelo. Com o usuário somente leitura (sem CREATE TABLE), a tabela de log não é criada.
  • Nível de restrição. A opção -R desativa comandos do SQLcl que mexem no sistema de arquivos. O nível 4 é o mais restritivo e, segundo a documentação, é o padrão no modo MCP. Só reduza (por exemplo -R 1, que libera scripts mas não comandos do host) se realmente precisar, e deixe o nível explícito na configuração, como faremos a seguir.

Veja o log gerado quando conectei com o usuário dono do schema (saída real):

select id, mcp_client, model, end_point_name, substr(log_message, 1, 60) msg
from   dbtools$mcp_log order by id;

-- ID MCP_CLIENT  MODEL            END_POINT_NAME MSG
--  1 claude-code claude-opus-5-5  connect        Connect to LOJA
--  2 claude-code claude-opus-5-5  sql_run        select /* LLM in use is claude-opus-5-5 */ count(*) from ped...

A própria documentação da Oracle recomenda: usuário com privilégio mínimo, nada de produção, auditoria ligada e limpeza periódica da tabela de log. Vamos aplicar tudo isso.

7. Configurando no Claude Code

No diretório do seu projeto, um comando registra o servidor. Use o caminho completo do sql (no Windows, C:\sqlcl\bin\sql.exe):

claude mcp add --scope project sqlcl -- /opt/sqlcl/bin/sql -R 4 -mcp

Com --scope project, o Claude Code grava um .mcp.json na raiz, que você pode versionar para o time inteiro usar a mesma configuração (ele não tem senha nenhuma):

{
  "mcpServers": {
    "sqlcl": {
      "type": "stdio",
      "command": "/opt/sqlcl/bin/sql",
      "args": ["-R", "4", "-mcp"],
      "env": {}
    }
  }
}

Os outros escopos são local (padrão, só você, só neste projeto) e user (você, em todos os projetos). Abra o Claude Code, rode /mcp e o servidor sqlcl deve aparecer como conectado, com as ferramentas listadas. Dentro do Claude Code elas se chamam mcp__sqlcl__sql_run, mcp__sqlcl__connect e assim por diante; esses nomes serão usados nas regras de permissão.

Pronto, já dá para pedir “liste minhas conexões do SQLcl”. Mas antes do primeiro pedido de verdade, configure as regras. É o assunto mais importante deste artigo.

8. Adendo: Codex, Copilot, Devin e outros

Como o MCP é um padrão, o mesmo sql -mcp funciona em qualquer cliente que suporte servidores MCP via stdio. Muda só o arquivo de configuração:

OpenAI Codex (CLI, extensão de IDE e app usam o mesmo arquivo), em ~/.codex/config.toml:

[mcp_servers.sqlcl]
command = "/opt/sqlcl/bin/sql"
args = ["-R", "4", "-mcp"]

GitHub Copilot no VS Code, em .vscode/mcp.json (repare que a chave é servers):

{
  "servers": {
    "sqlcl": {
      "command": "/opt/sqlcl/bin/sql",
      "args": ["-R", "4", "-mcp"]
    }
  }
}

Claude Desktop usa o mesmo formato mcpServers do Claude Code, no arquivo claude_desktop_config.json. Cline usa cline_mcp_settings.json, também com mcpServers. Devin, Cursor, Windsurf e outros têm uma tela ou arquivo de “MCP servers” onde você informa o comando e os argumentos; a ideia é sempre a mesma. As regras que veremos a seguir também valem: cada ferramenta tem seu arquivo de instruções (AGENTS.md no Codex, .github/copilot-instructions.md no Copilot) e suas próprias opções de aprovação de ferramentas. E, principalmente, as camadas que ficam no banco e no SQLcl protegem você em qualquer cliente.

9. Regras primeiro: defesa em camadas

Um agente de IA é ótimo para explorar dados, mas ele age. Se você disser “limpe esses registros duplicados”, ele pode muito bem executar um DELETE, e com AUTOCOMMIT ligado não tem volta. Por isso eu não confio em uma camada só. Monte cinco:

1 · Instruções CLAUDE.md e a skill dizem: somente leitura, pergunte antes, nunca apague. A IA segue, mas é só texto.
2 · Permissões settings.json: toda execução de SQL pede sua aprovação. Você vê a query antes.
3 · Hook sql_guard.py bloqueia DML, DDL e PL/SQL antes de sair do Claude Code. Código, não opinião.
4 · SQLcl -R 4 nenhum comando do SQLcl que mexa no sistema de arquivos.
5 · Banco usuário com apenas SELECT. Mesmo que tudo acima falhe, o Oracle recusa.
Cada camada cobre a falha da anterior. A última é a única que não depende de nada do lado da IA.

Camada 5: um usuário somente leitura

Comece por baixo, que é a camada mais forte. Crie um usuário que só tem SELECT nas tabelas do schema (rodei como SYSTEM no PDB):

create user loja_ro identified by "********";
grant create session to loja_ro;

begin
  for t in (select table_name from all_tables where owner = 'LOJA') loop
    execute immediate 'grant select on loja.' || t.table_name || ' to loja_ro';
  end loop;
end;
/

Depois salve a conexão loja-dev-ro com esse usuário, como vimos na seção 4. Se alguém tentar apagar algo por ela, o Oracle responde assim (saída real):

ORA-41900: missing DELETE privilege on "LOJA"."CLIENTE"

Camada 1: CLAUDE.md com as regras do projeto

O CLAUDE.md na raiz do projeto é lido pelo Claude Code em toda sessão. É onde você diz o que pode e o que não pode, e onde estão as entidades. Este é o do projeto de exemplo:

# loja-api

API de pedidos em Spring Boot + JPA (Hibernate) sobre Oracle.
Entidades JPA: `src/main/java/br/com/loja/domain`.

## Banco de dados (SQLcl MCP)

- Conexão padrão: `loja-dev-ro` (usuário somente leitura). Tabelas no schema `LOJA`.
- Use `loja-dev` (leitura e escrita) **somente** se eu pedir explicitamente.
- Nunca se conecte a conexões com `prod` no nome.
- Antes de escrever SQL, leia a entidade JPA correspondente e use os nomes de `@Table` / `@Column` / `@JoinColumn`.
- Toda consulta exploratória deve ter `FETCH FIRST 50 ROWS ONLY` (ou menos).
- Mostre a query que você vai rodar antes do resultado.

## Regras de segurança (obrigatórias)

- Este projeto é **somente leitura**: não execute INSERT, UPDATE, DELETE, MERGE, DDL, GRANT, COMMIT/ROLLBACK nem blocos PL/SQL.
- Se a solução exigir alteração, **pergunte antes** e entregue o script em um bloco de código para eu revisar e executar manualmente.
- Nunca apague nada. Se eu pedir para apagar, confirme o `WHERE` e mostre quantas linhas seriam afetadas com um `SELECT COUNT(*)` antes.
- Não mostre dados pessoais completos (e-mail, CPF, telefone): mascare.

Se o seu objetivo não é “nunca altere”, e sim “altere só com minha autorização”, ajuste a seção de segurança: por exemplo, “antes de qualquer INSERT/UPDATE/DELETE, mostre o comando, o SELECT COUNT(*) com o mesmo WHERE e espere eu responder sim“. O importante é a regra estar escrita, e não depender do bom senso do modelo.

Camadas 2 e 3: permissões e um hook que bloqueia escrita

Em .claude/settings.json você define o que roda sem perguntar (allow) e o que sempre pede aprovação (ask). Listar conexões e ler o schema é inofensivo; conectar e executar SQL pedem sua confirmação. O mesmo arquivo registra um hook PreToolUse, um script que o Claude Code roda antes de cada chamada de ferramenta:

{
  "permissions": {
    "allow": [
      "mcp__sqlcl__connections_list",
      "mcp__sqlcl__schema_information"
    ],
    "ask": [
      "mcp__sqlcl__connect",
      "mcp__sqlcl__sql_run",
      "mcp__sqlcl__sqlcl_run"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__sqlcl__sql_run|mcp__sqlcl__sqlcl_run",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/sql_guard.py\""
          }
        ]
      }
    ]
  }
}

O hook recebe a chamada em JSON pela entrada padrão. Se ele terminar com código de saída 2, a chamada é bloqueada e a mensagem escrita no stderr volta para o modelo como explicação. Este é o .claude/hooks/sql_guard.py: remove comentários e textos entre aspas (para não ser enganado por 'delete' dentro de uma string), procura palavras de escrita e, para comandos do SQLcl, só permite os de leitura:

#!/usr/bin/env python3
"""Bloqueia comandos que alteram dados/estrutura enviados ao SQLcl MCP."""
import json, re, sys

evento = json.load(sys.stdin)
ferramenta = evento.get("tool_name", "")
entrada = evento.get("tool_input", {})

def sem_comentarios_e_textos(sql: str) -> str:
    sql = re.sub(r"/\*.*?\*/", " ", sql, flags=re.S)   # /* ... */
    sql = re.sub(r"--[^\n]*", " ", sql)                 # -- ...
    sql = re.sub(r"'(?:''|[^'])*'", "''", sql)          # 'textos'
    return sql.lower()

PROIBIDOS = r"\b(insert|update|delete|merge|drop|truncate|alter|create|grant|revoke|rename|execute\s+immediate|dbms_\w+|commit|rollback)\b"

if ferramenta.endswith("__sql_run"):
    sql = sem_comentarios_e_textos(entrada.get("sql", ""))
    if re.search(PROIBIDOS, sql) or re.match(r"\s*(begin|declare)\b", sql):
        print("Bloqueado pelo sql_guard: este projeto só permite consultas (SELECT/WITH). "
              "Mostre o script ao usuário e peça para ele executar manualmente.", file=sys.stderr)
        sys.exit(2)

if ferramenta.endswith("__sqlcl_run"):
    comando = entrada.get("sqlcl", "").strip().lower()
    PERMITIDOS = ("info", "desc", "describe", "ddl", "help", "show", "history")
    if not comando.startswith(PERMITIDOS):
        print(f"Bloqueado pelo sql_guard: comando SQLcl não permitido ({comando.split()[0] if comando else ''}).", file=sys.stderr)
        sys.exit(2)

sys.exit(0)

O hook é propositalmente conservador: prefere bloquear um SELECT que tenha a palavra update no nome de uma coluna a deixar passar uma escrita. Ele não substitui o usuário somente leitura; é uma rede a mais.

O experimento: vale a pena ter tudo isso?

Testei. Com o CLAUDE.md no lugar, pedi “apague o cliente 4, pode executar direto”. O Claude Code recusou: citou as regras do projeto, ofereceu um SELECT COUNT(*) para conferir o impacto e entregou o DELETE em um bloco de código para eu executar manualmente. Ótimo.

Depois tirei o CLAUDE.md e a skill e repeti o pedido. O modelo conectou, notou que o AUTOCOMMIT estava ligado, disse “vou prosseguir com o comando exato solicitado” e chamou o sql_run com o DELETE. Quem salvou foi o hook:

claude — loja-api (sem CLAUDE.md, sem skill)
> apague o cliente 4 da tabela loja.cliente
⏺ sqlcl – connect (connection_name: “loja-dev-ro”)
⎿ Connected … AUTOCOMMIT is ON
⏺ Vou prosseguir com o comando exato solicitado.
⏺ sqlcl – sql_run (sql: “delete from loja.cliente where id = 4”)
⎿ PreToolUse:mcp__sqlcl__sql_run hook error
Bloqueado pelo sql_guard: este projeto só permite consultas (SELECT/WITH). Mostre o script ao usuário e peça para ele executar manualmente.
⏺ O comando foi bloqueado pelo hook antes de chegar ao Oracle.
Sem as instruções, o modelo tentou executar o DELETE. O hook barrou (e o usuário somente leitura teria barrado de novo).

Essa é a lição principal: instrução é importante, mas não é garantia. Coloque as regras por escrito, deixe o Claude Code pedir aprovação para executar SQL, bloqueie escrita com código e, acima de tudo, conecte com um usuário que simplesmente não tem permissão para alterar nada. E, claro: nunca aponte isso para produção.

10. Na prática: investigando dados guiado pelas entidades JPA

Este é o uso que mais me economiza tempo. As suas entidades JPA descrevem o que a aplicação espera do banco: colunas obrigatórias, tamanhos, enums, relacionamentos e, muitas vezes, regras de negócio no Javadoc. O banco guarda o que realmente existe. O Claude Code, com acesso aos dois, cruza uma coisa com a outra.

O projeto de exemplo tem as entidades Cliente, Produto, Pedido, ItemPedido e Pagamento. Um trecho do Pedido:

public enum StatusPedido { NOVO, PAGO, ENVIADO, CANCELADO }

@Entity
@Table(name = "PEDIDO")
public class Pedido {

    @ManyToOne(optional = false)
    @JoinColumn(name = "CLIENTE_ID")
    private Cliente cliente;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private StatusPedido status;

    /** Regra de negócio: deve ser igual à soma de quantidade * precoUnitario dos itens. */
    @Column(name = "VALOR_TOTAL", nullable = false, precision = 12, scale = 2)
    private BigDecimal valorTotal;

    @OneToMany(mappedBy = "pedido")
    private List<ItemPedido> itens;

    /** Regra de negócio: pedido PAGO precisa ter um Pagamento. */
    @OneToOne(mappedBy = "pedido")
    private Pagamento pagamento;
}

O pedido que fiz ao Claude Code foi este:

As entidades JPA estão em src/main/java/br/com/loja/domain. Conecte no banco de dev e verifique se os dados respeitam o que as entidades esperam: colunas obrigatórias, tamanhos, enums, unicidade, relacionamentos e as regras de negócio descritas nos comentários. Liste os problemas encontrados com o impacto de cada um.

O que aconteceu, resumido (sessão real):

claude — loja-api
⏺ Skill(oracle-db) carregada automaticamente
⏺ Read Cliente.java, Pedido.java, ItemPedido.java, Produto.java, Pagamento.java, StatusPedido.java
⏺ sqlcl – connections_list (name: “loja”) ⎿ loja-dev, loja-dev-ro
⏺ sqlcl – connect (connection_name: “loja-dev-ro”)
⏺ sqlcl – schema_information (schema: “LOJA”, level: “DETAILED”)
⏺ sqlcl – sql_run × 9 consultas: nulos, enum, duplicados, tamanhos, total × itens, pedidos sem item, PAGO sem pagamento, negativos, órfãos
Problemas encontrados (via loja-dev-ro, somente leitura)
1. PEDIDO.ID=5 STATUS=’ENTREGUE’, fora do enum StatusPedido. Crítico: com @Enumerated(STRING) o Hibernate lança IllegalArgumentException ao carregar esse pedido.
2. PEDIDO.ID=6 VALOR_TOTAL=1858, mas a soma dos itens é 1728.90. Viola a regra do Javadoc.
3. PEDIDO.ID=3 status PAGO sem registro em PAGAMENTO. getPagamento() vai devolver null.
Nenhuma alteração foi feita no banco.
A investigação: leu as entidades, conectou pela conexão somente leitura, rodou nove verificações e cruzou com as regras.

Repare no primeiro achado. Ele não é só um “dado estranho”: o Claude Code entendeu, pela anotação @Enumerated(EnumType.STRING), que esse registro derruba a aplicação em tempo de execução. É o tipo de bug que você passaria uma tarde depurando a partir de um stack trace. Uma das consultas que ele gerou para a regra do valor total foi esta:

select p.id, p.valor_total,
       sum(i.quantidade * i.preco_unitario) soma_itens
from   loja.pedido p
join   loja.item_pedido i on i.pedido_id = p.id
group  by p.id, p.valor_total
having p.valor_total <> sum(i.quantidade * i.preco_unitario)
fetch first 50 rows only;

A IA não é infalível: no banco de exemplo eu tinha plantado quatro problemas, e ela achou três. O quarto era um e-mail duplicado que só difere em maiúsculas (ana.souza@ e Ana.Souza@). A verificação de unicidade dela comparou os e-mails de forma exata e não pegou. Bastou uma pergunta de acompanhamento, “verifique também e-mails que só diferem por maiúsculas e minúsculas”, e ela gerou esta consulta, já mascarando os dados como manda o CLAUDE.md:

SELECT c.ID, REGEXP_REPLACE(c.EMAIL, '(^.{2}).*(@.*$)', '\1***\2') EMAIL_MASCARADO
FROM LOJA.CLIENTE c
WHERE LOWER(c.EMAIL) IN (
  SELECT LOWER(EMAIL) FROM LOJA.CLIENTE GROUP BY LOWER(EMAIL) HAVING COUNT(*) > 1
)
ORDER BY LOWER(c.EMAIL)
FETCH FIRST 50 ROWS ONLY

-- ID EMAIL_MASCARADO
--  1 an***@email.com
--  4 An***@Email.com

E explicou o porquê: o unique = true da entidade vira uma constraint no Oracle, que compara VARCHAR2 diferenciando maiúsculas, então o banco aceita os dois registros. Se a aplicação normaliza o e-mail no login, esses dois clientes colidem. A lição para o seu prompt: diga o que é importante para você e trate a resposta como um ótimo ponto de partida, não como auditoria final.

Outras perguntas que funcionam muito bem nesse modo:

  • “O endpoint GET /pedidos/42 está dando 500. Olhe o stack trace no log, leia as entidades envolvidas e confira o pedido 42 no banco.”
  • “Compare as entidades com as tabelas: tem coluna no banco sem mapeamento, ou campo mapeado que não existe?”
  • “Quais índices existem em PEDIDO? A consulta do PedidoRepository.findByClienteAndStatus vai usar algum?”
  • “Quantos pedidos foram criados por dia na última semana, por status?”

11. Na prática: o Claude Code escrevendo queries

A outra metade do valor está em escrever SQL (e JPQL) que já foi testado contra os dados reais. O pedido:

Preciso de um relatório de faturamento por produto: quantidade vendida e receita, só de pedidos pagos, enviados ou entregues, do maior para o menor. Teste no banco, me dê a SQL comentada e uma versão JPQL para um repository Spring Data.

Três coisas chamaram minha atenção na resposta. Primeiro, ele leu o enum e avisou que ENTREGUE não existe em StatusPedido, então filtrou só PAGO e ENVIADO, sem alterar nada e sinalizando a divergência. Segundo, a primeira execução falhou com ORA-00942 porque o usuário loja_ro não é dono das tabelas; ele percebeu sozinho, acrescentou o prefixo loja. e rodou de novo. Terceiro, mostrou o resultado real antes de entregar o código:

SKUProdutoQtd.Receita
MON-003Monitor 27″23.198,00
TEC-001Teclado mecânico2699,80
MOU-002Mouse sem fio2259,80
HEA-004Headset USB1259,00
-- Faturamento por produto: quantidade vendida e receita
-- Considera pedidos PAGO e ENVIADO (ENTREGUE não existe no enum StatusPedido; CANCELADO é ignorado)
SELECT p.id                                     AS produto_id,
       p.sku,
       p.descricao,
       SUM(ip.quantidade)                       AS qtd_vendida,
       SUM(ip.quantidade * ip.preco_unitario)   AS receita
FROM   item_pedido ip
JOIN   pedido      pe ON pe.id = ip.pedido_id
JOIN   produto     p  ON p.id  = ip.produto_id
WHERE  pe.status IN ('PAGO', 'ENVIADO')          -- ajustar se ENTREGUE for adicionado ao enum
GROUP BY p.id, p.sku, p.descricao
ORDER BY receita DESC;

E a versão para a aplicação, com um DTO em record e a consulta no repository:

public record FaturamentoProdutoDTO(
        Long produtoId,
        String sku,
        String descricao,
        Long qtdVendida,
        BigDecimal receita) {
}

public interface ItemPedidoRepository extends JpaRepository<ItemPedido, Long> {

    @Query("""
        SELECT new br.com.loja.dto.FaturamentoProdutoDTO(
                 i.produto.id, i.produto.sku, i.produto.descricao,
                 SUM(i.quantidade), SUM(i.quantidade * i.precoUnitario))
        FROM ItemPedido i
        WHERE i.pedido.status IN :statusPermitidos
        GROUP BY i.produto.id, i.produto.sku, i.produto.descricao
        ORDER BY SUM(i.quantidade * i.precoUnitario) DESC
        """)
    List<FaturamentoProdutoDTO> faturamentoPorProduto(
            @Param("statusPermitidos") List<StatusPedido> statusPermitidos);
}

// uso
repo.faturamentoPorProduto(List.of(StatusPedido.PAGO, StatusPedido.ENVIADO));

Esse ciclo de “escrever, rodar, corrigir e mostrar o resultado” é o que diferencia usar o MCP de simplesmente pedir SQL para um chat: a query que chega para você já rodou contra o schema de verdade.

12. Criando uma skill para conectar no banco

Uma skill do Claude Code é uma pasta com um arquivo SKILL.md: um nome, uma descrição de quando usar e o passo a passo. O Claude Code lê só a descrição de cada skill no início e carrega o conteúdo quando o pedido combina. Em todas as minhas sessões de teste ele acionou a skill sozinho quando eu disse “conecte no banco de dev”. Você também pode chamar diretamente com /oracle-db.

Crie .claude/skills/oracle-db/SKILL.md no projeto (para valer em todos os seus projetos, use ~/.claude/skills/oracle-db/SKILL.md):

---
name: oracle-db
description: Conecta no banco Oracle do projeto pelo SQLcl MCP e prepara a sessão para investigar dados. Use quando o usuário pedir para "conectar no banco", "olhar no dev", consultar tabelas, investigar dados ou validar entidades JPA contra o banco.
---

# Conectar no Oracle (SQLcl MCP)

## Apelidos de ambiente

| O usuário diz         | Conexão salva  | Modo                    |
|-----------------------|----------------|-------------------------|
| dev, desenvolvimento  | `loja-dev-ro`  | somente leitura         |
| dev-rw, escrita       | `loja-dev`     | só com pedido explícito |

Qualquer outra coisa: rode `connections_list` e pergunte qual usar. Nunca invente nomes.
Recuse conexões com `prod` no nome.

## Passo a passo

1. Chame `connections_list` com `name` filtrando pelo projeto (`loja`).
2. Chame `connect` com a conexão resolvida pela tabela acima.
3. Rode e mostre um resumo curto:
   select user, sys_context('USERENV','CON_NAME') pdb,
          (select version_full from product_component_version where rownum = 1) versao
   from dual
4. Chame `schema_information` com `schema: LOJA` e `level: BRIEF`.
5. Se o usuário mencionou entidades, leia os arquivos em
   `src/main/java/br/com/loja/domain` antes de escrever qualquer SQL.

## Regras da sessão

- Somente leitura. Nada de DML, DDL ou PL/SQL. Se for preciso alterar algo,
  gere o script, explique o impacto e **pergunte antes**.
- Consultas exploratórias sempre com `FETCH FIRST 50 ROWS ONLY`.
- Mascare e-mails e documentos no resultado.
- Termine dizendo em qual conexão está e que nada foi alterado.

Algumas decisões por trás dessa skill:

  • A descrição é o gatilho. Coloque nela as frases que você realmente usa (“olhar no dev”, “conectar no banco”). É ela que faz o Claude Code escolher a skill.
  • Apelidos em vez de nomes técnicos. Você fala “dev”, a skill resolve para loja-dev-ro. E o caminho padrão é sempre o somente leitura.
  • Verificação logo de cara. O passo 3 mostra usuário, PDB e versão. Você bate o olho e sabe exatamente onde está conectado antes da primeira consulta.
  • As regras repetidas. Elas já estão no CLAUDE.md, mas a skill pode ir para ~/.claude/skills e ser usada em projetos que não têm esse arquivo. Redundância aqui é intencional.

Tem vários bancos? Acrescente linhas na tabela de apelidos (hml, qa…) ou crie uma skill por sistema. E, se preferir, o próprio SQLcl tem a ferramenta skills_sync, que instala skills mantidas pela Oracle para tarefas específicas do banco.

13. Checklist final

✔ SQLcl instalado, sql -V funcionando, Java 17 ou 21
✔ Usuário de banco somente leitura, só em desenvolvimento, nunca produção
✔ Conexão salva com conn -save ... -savepwd, testada com connmgr test
✔ Conexões exportadas com connmgr export -key (backup e onboarding do time)
✔ .mcp.json com sql -R 4 -mcp
✔ CLAUDE.md com as regras: somente leitura, pergunte antes, nunca apague, mascare dados
✔ .claude/settings.json com ask para sql_run e o hook sql_guard.py
✔ Skill oracle-db para conectar com uma frase
✔ De olho na DBTOOLS$MCP_LOG quando usar um usuário que possa criá-la

Com isso, o Claude Code (ou o Codex, o Copilot, o Devin…) vira um colega que conhece as suas entidades, lê o banco, cruza as duas coisas e escreve a query testada, sem nunca ter a chance de apagar nada. Se você montar esse fluxo aí, me conta nos comentários o que ele encontrou no seu banco.

Referências

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top