MCP PostgreSQL Server

Um servidor que permite que modelos de IA interajam com bancos de dados PostgreSQL por meio de uma interface padronizada.

Documentação

MCP PostgreSQL Server

npm version CI

Um servidor Model Context Protocol (MCP) para PostgreSQL: bancos de dados locais, Docker, RDS, Neon e Supabase.

O servidor é pequeno e auditável, com quatro dependências de runtime: o SDK MCP, pg, pg-connection-string e zod (além de ssh2, uma dependência opcional usada apenas para tunelamento SSH).

Requer Node.js 20 ou mais recente.

Início rápido

A forma preferida de configurar o servidor é um único DATABASE_URL:

{
  "mcpServers": {
    "postgres": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "mcp-postgres-server"],
      "env": {
        "DATABASE_URL": "postgres://user:password@localhost:5432/mydb",
        "PG_ALLOW_WRITE": "false"
      }
    }
  }
}

Com PG_ALLOW_WRITE definido como "false", o servidor tem acesso somente leitura ao banco de dados. Este é o padrão; defina-o como "true" apenas se o modelo precisar escrever.

O mesmo JSON funciona em qualquer cliente MCP que fale stdio: VS Code, Cursor, Claude Code, Codex, Windsurf.

Alternativamente, defina as variáveis PG_* individuais; elas são usadas quando DATABASE_URL não está definido:

{
  "mcpServers": {
    "postgres": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "mcp-postgres-server"],
      "env": {
        "PG_HOST": "your_host",
        "PG_PORT": "5432",
        "PG_USER": "your_user",
        "PG_PASSWORD": "your_password",
        "PG_DATABASE": "your_database",
        "PG_ALLOW_WRITE": "false"
      }
    }
  }
}

Instalação Manual

npm install mcp-postgres-server

Ou execute diretamente com:

npx mcp-postgres-server

Conecte-se ao seu banco de dados

Postgres local:

DATABASE_URL=postgres://mcp_readonly:secret@localhost:5432/mydb

Postgres no Docker: se o banco de dados estiver em um contêiner com uma porta publicada, conecte-se a localhost:<published-port> normalmente. Se o próprio servidor MCP estiver em um contêiner e o banco de dados estiver na sua máquina host, use host.docker.internal em vez de localhost:

DATABASE_URL=postgres://mcp_readonly:secret@host.docker.internal:5432/mydb

Amazon RDS:

DATABASE_URL=postgres://mcp_readonly:secret@mydb.xxxxxx.us-east-1.rds.amazonaws.com:5432/mydb?sslmode=require

Neon:

DATABASE_URL=postgres://mcp_readonly:secret@ep-xxx-xxx.us-east-2.aws.neon.tech/mydb?sslmode=require

Supabase:

DATABASE_URL=postgres://postgres.xxxxxxxx:secret@aws-0-us-east-1.pooler.supabase.com:5432/postgres?sslmode=require

Ferramentas

A disponibilidade das ferramentas depende da configuração:

FerramentaDisponível
query, list_schemas, list_tables, describe_tableSempre
executeSempre (recusa gravações a menos que PG_ALLOW_WRITE=true)
connect_dbSomente quando PG_ENABLE_RUNTIME_CONNECT=true

1. query

Executa uma instrução SQL somente leitura. Aceita SELECT, WITH ... SELECT, EXPLAIN e SHOW. Uma instrução por chamada — entrada com múltiplas instruções é rejeitada pelo protocolo de consulta estendida. No modo somente leitura (o padrão), a instrução é executada como BEGIN READ ONLY, a consulta e ROLLBACK — três comandos, aproximadamente duas viagens de ida e volta na rede com pipelining — então o próprio banco de dados recusa qualquer gravação. Com PG_ALLOW_WRITE=true, a instrução é enviada diretamente, sem esse wrapper, então uma gravação executada via query seria executada — use execute para gravações. Suporta parâmetros de instrução preparada no estilo PostgreSQL $1, $2; os valores são vinculados pelo driver e nunca interpolados no texto SQL.

use_mcp_tool({
  server_name: "postgres",
  tool_name: "query",
  arguments: {
    sql: "SELECT * FROM users WHERE id = $1",
    params: [1]
  }
});

Retorna JSON compacto: {"rows": [...], "rowCount": n, "returnedRows": n, "truncated": false}. Quando as linhas serializadas excedem PG_MAX_RESULT_BYTES, apenas as linhas que cabem são retornadas (returnedRows < rowCount), truncated é true e uma dica sugere adicionar LIMIT/WHERE ou selecionar menos colunas.

2. list_schemas

Lista todos os esquemas no banco de dados conectado.

use_mcp_tool({
  server_name: "postgres",
  tool_name: "list_schemas",
  arguments: {}
});

3. list_tables

Lista tabelas no banco de dados conectado. Aceita um parâmetro opcional de esquema (padrão: 'public').

// List tables in the 'public' schema (default)
use_mcp_tool({
  server_name: "postgres",
  tool_name: "list_tables",
  arguments: {}
});

// List tables in a specific schema
use_mcp_tool({
  server_name: "postgres",
  tool_name: "list_tables",
  arguments: {
    schema: "my_schema"
  }
});

4. describe_table

Obtém a estrutura de uma tabela específica (colunas, tipos, nulabilidade, padrões, chaves primárias). Aceita um parâmetro opcional de esquema (padrão: 'public').

use_mcp_tool({
  server_name: "postgres",
  tool_name: "describe_table",
  arguments: {
    table: "users",
    schema: "my_schema"  // optional
  }
});

5. execute — requer PG_ALLOW_WRITE=true

Executa uma instrução INSERT, UPDATE, DELETE ou DDL. Sempre registrada, mas no modo somente leitura (o padrão) ela recusa com um erro nomeando PG_ALLOW_WRITE e não altera nada — a instrução nunca chega ao banco de dados. Com PG_ALLOW_WRITE=true, ela executa: o mesmo tratamento de parâmetros $1, $2 que query, uma instrução completa por chamada, e o papel conectado determina o que ela pode fazer. Retorna {"rowCount": n, "command": "INSERT"}.

use_mcp_tool({
  server_name: "postgres",
  tool_name: "execute",
  arguments: {
    sql: "INSERT INTO users (name, email) VALUES ($1, $2)",
    params: ["John Doe", "john@example.com"]
  }
});

6. connect_db — requer PG_ENABLE_RUNTIME_CONNECT=true

Conecta-se a um banco de dados PostgreSQL diferente em tempo de execução usando as credenciais fornecidas. Não registrada por padrão — prefira configurar credenciais através do ambiente para que nunca passem por argumentos visíveis ao modelo. Os limites de sessão (statement_timeout, idle_in_transaction_session_timeout) são reaplicados após cada reconexão; leituras somente leitura aplicam somente leitura em sua própria transação BEGIN READ ONLY.

use_mcp_tool({
  server_name: "postgres",
  tool_name: "connect_db",
  arguments: {
    host: "localhost",
    port: 5432,
    user: "your_user",
    password: "your_password",
    database: "your_database"
  }
});

Referência de configuração

VariávelPadrãoDescrição
DATABASE_URL-String de conexão completa (preferida). Suporta ?sslmode= na URL.
PG_HOST-Host do banco de dados (fallback quando DATABASE_URL não está definido)
PG_PORT5432Porta do banco de dados
PG_USER-Usuário do banco de dados
PG_PASSWORD-Senha do banco de dados
PG_DATABASE-Nome do banco de dados
PG_ALLOW_WRITEfalseQuando true, execute executa gravações e leituras são enviadas diretamente. Desligado (padrão) é somente leitura: execute recusa gravações e cada leitura é executada em uma transação READ ONLY
PG_SSLMODE-disable | allow | prefer | require | verify-ca | verify-full. require/allow/prefer criptografam sem verificar o certificado; verify-ca/verify-full verificam (forneça uma CA via PG_SSL_CA). Valores não reconhecidos falham na inicialização. Limitação: ao contrário do libpq, allow/prefer não fazem fallback para texto simples (node-postgres não tem SSL oportunista), então um servidor sem TLS precisa de disable.
PG_SSL_CA-Caminho para um arquivo de certificado CA. Definir isso sozinho implica verify-full
PG_ENABLE_RUNTIME_CONNECTfalseRegistra a ferramenta connect_db (troca de credenciais em tempo de execução)
PG_MAX_RESULT_BYTES32768Orçamento de bytes para um resultado query enviado ao modelo. Linhas inteiras são mantidas enquanto couberem; acima do orçamento returnedRows < rowCount e truncated: true (se nem a primeira linha couber, returnedRows é 0 com uma dica). ~32 KiB ≈ 8 mil tokens; reduza para clientes estritos, aumente se seu cliente permitir mais.
PG_STATEMENT_TIMEOUT30000Tempo limite de instrução em milissegundos, aplicado a cada sessão
PG_CONNECT_TIMEOUT10000Tempo limite em milissegundos para uma única tentativa de conexão (aumente para links lentos ou túneis SSH)

Para alcançar um banco de dados acessível apenas através de um bastion, veja Tunelamento SSH (adiciona variáveis PG_SSH_*).

Recursos

  • Somente leitura por padrão; gravações são um opt-in explícito (PG_ALLOW_WRITE=true)
  • Somente leitura aplicado pelo mecanismo (BEGIN READ ONLY), nunca por análise SQL no lado do cliente
  • Acesso a dados atrás de uma pequena interface tipada; o driver pg nunca vaza além dela
  • Suporte a DATABASE_URL com SSL (sslmode=disable|allow|prefer|require|verify-ca|verify-full, CA personalizada)
  • Parâmetros de instrução preparada: placeholders no estilo $1, vinculados pelo driver
  • Limite de tamanho do resultado (orçamento de bytes) com um sinalizador explícito truncated em vez de inundar o contexto do modelo
  • Tempo limite de instrução da sessão mais um prazo do cliente; poolers de transação podem não preservar configurações de sessão
  • Erros retornados como resultados de ferramenta legíveis com dicas baseadas em SQLSTATE, para que o modelo possa se autocorrigir
  • Sobrevive a conexões perdidas — reconecta de forma preguiçosa em vez de travar
  • Tunelamento SSH opcional (PG_SSH_*) com verificação obrigatória de chave de host, carregado apenas quando configurado
  • Anotações de ferramenta MCP (dicas somente leitura / destrutivas) conforme especificação 2025-11-25
  • Suporte a múltiplos esquemas para operações de banco de dados

Segurança

Detalhes completos, incluindo o modelo de ameaças e o processo de divulgação, estão em SECURITY.md. A versão resumida:

  1. Um papel de banco de dados com privilégios mínimos é a fronteira real. O MCP trabalha com credenciais existentes; criar ou alterar papéis não é necessário. Um papel dedicado é o que realmente garante que gravações sejam impossíveis. No PostgreSQL 14+, o seguinte é um ponto de partida:

    CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
    GRANT CONNECT ON DATABASE your_database TO mcp_readonly;
    GRANT pg_read_all_data TO mcp_readonly;                          -- adds read privileges
    ALTER ROLE mcp_readonly SET default_transaction_read_only = on;  -- read-only by default
    

    (No PostgreSQL 13 ou mais antigo, conceda SELECT explicitamente em vez de pg_read_all_data — veja SECURITY.md.) O servidor avisa no stderr se você conectar como superusuário. Concessões de leitura não revogam privilégios existentes, e padrões permanecem mutáveis; funções disponíveis, propriedade e privilégios herdados também importam.

  2. O mecanismo aplica somente leitura. Não há análise SQL no lado do cliente. No modo somente leitura, cada leitura é executada em uma transação BEGIN READ ONLY revertida, então o próprio PostgreSQL — que sozinho sabe o que uma função, visão ou regra faz — recusa qualquer gravação com SQLSTATE 25006 e reverte qualquer mudança de sessão que a instrução tenha feito. O protocolo estendido rejeita strings com múltiplos comandos.

Enquadramento honesto: a transação somente leitura é defesa em profundidade sobre o papel, não um substituto para ele. O modo somente leitura impede que um modelo confuso ou injetado por prompt escreva no seu banco de dados; ele não impede injeção de prompt transportada nos dados das linhas que uma consulta retorna. Não aponte este servidor para produção — use uma réplica, um snapshot ou um papel com escopo restrito. Veja SECURITY.md.

Tunelamento SSH

Defina PG_SSH_HOST (além de autenticação e verificação de chave de host) para alcançar um banco de dados acessível apenas através de um bastion (um host de salto SSH). A string de conexão / campos PG_* descrevem então o banco de dados como visto a partir do bastion:

{
  "mcpServers": {
    "postgres": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "mcp-postgres-server"],
      "env": {
        "DATABASE_URL": "postgres://mcp_readonly:secret@db.internal:5432/mydb?sslmode=verify-full",
        "PG_SSH_HOST": "bastion.example.com",
        "PG_SSH_USER": "jump",
        "PG_SSH_PRIVATE_KEY": "/home/me/.ssh/id_ed25519",
        "PG_SSH_FINGERPRINT": "SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
VariávelPadrãoDescrição
PG_SSH_HOST-Host bastion SSH. Definir isso ativa o tunelamento: o servidor alcança o banco de dados apenas através de um túnel SSH para este host (veja abaixo). Recurso opcional; precisa da dependência opcional ssh2.
PG_SSH_PORT22Porta do bastion SSH
PG_SSH_USER-Nome de usuário SSH
PG_SSH_PRIVATE_KEY-Caminho para um arquivo de chave privada. Se não definido, a autenticação faz fallback como ssh: um agente em execução (SSH_AUTH_SOCK), depois uma chave padrão (~/.ssh/id_ed25519, id_rsa, id_ecdsa)
PG_SSH_PASSPHRASE-Frase secreta para a chave privada, se criptografada
PG_SSH_AGENT-true para usar o agente ambiente (SSH_AUTH_SOCK), ou um caminho de socket explícito / pipe nomeado do Windows (\\.\pipe\openssh-ssh-agent)
PG_SSH_PASSWORD-Senha de login SSH. Opt-in; uma chave ou agente tem precedência. Prefira chaves — um bastion frequentemente desativa autenticação por senha.
PG_SSH_FINGERPRINT-Impressão digital de chave de host fixada (SHA256:...). A verificação de chave de host é obrigatória e definida apenas desta forma: sem ela, o túnel recusa a conexão (falha fechada). Obtenha com ssh-keygen -lF host (lê seu known_hosts) ou ssh-keyscan host | ssh-keygen -lf - (veja a nota de confiança abaixo)
PG_SSH_KEEPALIVE_INTERVAL15000Intervalo de keepalive SSH em ms; o túnel cai após 3 keepalives sem resposta, e a próxima chamada reconecta
  • SSH altera apenas o transporte. A aplicação de somente leitura, o limite de tamanho do resultado, os timeouts e connect_db se comportam exatamente como em uma conexão direta, e nenhum SQL extra é enviado por consulta.
  • A verificação da chave do host é obrigatória por meio de um PG_SSH_FINGERPRINT fixado - o túnel não conectará sem ele, portanto, um bastião intermediário (man-in-the-middle) é recusado. Obtenha a impressão digital por um canal em que você confia, do mais confiável primeiro:
    • no próprio bastião, ou de seu administrador: ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub (sem envolvimento de rede);
    • do seu ~/.ssh/known_hosts existente, se você já alcança o host via ssh: ssh-keygen -lF bastion.example.com;
    • obtido do host: ssh-keyscan bastion.example.com | ssh-keygen -lf - (confie nisso apenas quando executado de uma posição de rede em que você confia - ele aceita o que o host retornar).
  • TLS valida o nome de host real do banco de dados. Com verify-full, o certificado é verificado contra o nome de host do próprio banco de dados (ex.: db.internal), não o loopback ao qual o túnel se liga localmente, e rejectUnauthorized é fixado para que um NODE_TLS_REJECT_UNAUTHORIZED=0 herdado não possa desativá-lo.
  • ssh2 é uma dependência opcional, carregada apenas quando PG_SSH_HOST está definido, então uma conexão direta nunca a inicializa. O npm instala dependências opcionais por padrão; execute npm install --omit=optional para ignorá-la completamente (uma conexão direta não precisa dela).

Uma conexão tunelada que falha relata um código SSH_* estável - veja Tratamento de Erros.

Tratamento de Erros

Falhas de SQL e de conexão são retornadas como resultados de ferramenta (isError: true) com uma mensagem, o código SQLSTATE e uma dica. A dica do próprio servidor PostgreSQL é usada quando presente; caso contrário, estes fallbacks se aplicam:

códigosignificadoprimeira coisa a verificar
28P01autenticação falhouPG_USER / PG_PASSWORD
3D000o banco de dados não existePG_DATABASE
42P01relação não encontradachame list_tables
42703coluna não encontradachame describe_table
57014a consulta foi cancelada (um timeout ou uma solicitação de cancelamento)se estiver expirando, adicione um LIMIT / simplifique-a, ou aumente PG_STATEMENT_TIMEOUT
25006a transação é somente leituraa origem pode ser um papel somente leitura, uma réplica, um padrão do servidor, ou (para query) o wrapper somente leitura; execute gravações precisam de PG_ALLOW_WRITE=true
ECONNREFUSED / ENOTFOUNDnão é possível alcançar ou resolver o host do banco de dadosPG_HOST / PG_PORT / DATABASE_URL

Através de um túnel SSH, uma falha carrega um code estável (e, onde a causa é determinada, uma dica nomeando a configuração a corrigir), então a fase com falha é inequívoca:

códigosignificadoprimeira coisa a verificar
SSH_CONFIG_INVALIDconfiguração SSH inválida, incl. um PG_SSH_FINGERPRINT malformadoos valores de PG_SSH_*
SSH_KEY_INVALIDchave ilegível, não analisável, uma chave pública, ou criptografada sem a frase secreta correta (uma chave criptografada com o PG_SSH_PASSPHRASE correto funciona)PG_SSH_PRIVATE_KEY, PG_SSH_PASSPHRASE
SSH_CONNECT_FAILEDo bastião está inacessível, ou a configuração SSH falhou por um motivo não classificadoPG_SSH_HOST, PG_SSH_PORT, acessibilidade
SSH_TIMEOUTo bastião não respondeu a temporede/firewall, PG_CONNECT_TIMEOUT
SSH_AUTH_FAILEDo bastião rejeitou a autenticaçãoPG_SSH_USER e a chave/agente/senha em uso
SSH_HOST_KEY_MISMATCHa chave do host não corresponde a PG_SSH_FINGERPRINT (valor desatualizado ou MITM)re-obtenha a impressão digital
SSH_FORWARD_FAILEDo túnel está ativo, mas o bastião não conseguiu alcançar o banco de dadoso host e a porta do DB vistos do bastião
SSH_CONNECTION_LOSTum túnel estabelecido caiu no meio da sessãotransitório; a próxima chamada reconecta

Um erro genuíno do PostgreSQL através de um túnel saudável mantém seu próprio código (ex.: 28P01 para credenciais de banco de dados erradas), não um código SSH.

Migrando de 0.1.x

Não é necessário para novas instalações. Duas mudanças de comportamento desde 0.1.x:

  1. Somente leitura por padrão. A ferramenta execute está sempre visível, mas recusa gravações (com um erro nomeando a flag) a menos que PG_ALLOW_WRITE=true, e cada leitura é executada dentro de uma transação READ ONLY imposta pelo mecanismo. Se seu fluxo de trabalho grava no banco de dados, defina "PG_ALLOW_WRITE": "true" para restaurar o comportamento de 0.1.x.
  2. connect_db está desabilitado por padrão. A troca de conexão em tempo de execução (passando credenciais por argumentos de ferramenta) requer PG_ENABLE_RUNTIME_CONNECT=true; caso contrário, os detalhes da conexão vêm apenas do ambiente.

Nomes de ferramentas, nomes de parâmetros e variáveis PG_* não mudaram. Os payloads de resultado agora são JSON compacto estruturado para cada ferramenta (ex.: query retorna {rows, rowCount, returnedRows, truncated} em vez de um array de linhas simples) - veja CHANGELOG.md para as formas exatas antes de atualizar qualquer coisa que analise a saída da ferramenta.

Licença

MIT