---
title: "Cómo construí mi propio MCP de PostgreSQL"
description: "El MCP de Postgres más conocido estaba deprecado y archivado, así que construí el mío. Esto aprendí: introspección completa, modos de acceso y cómo darle escritura a una IA sin romper la base."
url: https://gabrielherencia.com/insights/como-construi-mi-propio-mcp-de-postgresql
author: "Gabriel Herencia"
published: 2026-06-21
updated: 2026-07-14
category: "Herramientas de Desarrollo"
tags: ["Docker", "Claude Code", "Bases de Datos", "Seguridad", "MCP"]
reading_time_min: 5
---

# Cómo construí mi propio MCP de PostgreSQL

> El MCP de Postgres más conocido estaba deprecado y archivado, así que construí el mío. Esto aprendí: introspección completa, modos de acceso y cómo darle escritura a una IA sin romper la base.

![Portada: Cómo construí mi propio MCP de PostgreSQL](https://img.gabrielherencia.com/covers/como-construi-mi-propio-mcp-de-postgresql-c6fa20c5.jpg)

## Casi adopto un MCP deprecado

Necesitaba que un asistente de IA pudiera explorar una base **PostgreSQL** de verdad. El primer reflejo fue buscar un MCP ya hecho. El más conocido para Postgres estaba **archivado y deprecado** — y, pese al nombre, no era oficial de PostgreSQL, sino de un tercero.

La lección de partida fue clara: **antes de adoptar un MCP, verifica quién lo mantiene y si sigue vivo**. De ahí salió la decisión de construir el mío: control total, mantenible y extensible con tools a medida.

### ¿Qué es un MCP, en tres frases?

Un **MCP** (Model Context Protocol) es un servidor que expone *herramientas* que un asistente de IA puede invocar — un adaptador entre el modelo y un sistema externo. Se comunica por **stdio** (entrada/salida estándar) con mensajes JSON-RPC. Se conecta al cliente (por ejemplo Claude Code) declarándolo en un archivo `.mcp.json` con el comando que lo levanta y sus variables de entorno.

### Por qué construir el tuyo

- **Control**: tú decides qué tools existen y qué hace cada una.
- **Mantenibilidad**: no dependes de un repo que mañana se archiva.
- **Tools a medida**: puedes modelar exactamente tu flujo —introspección, escrituras con barandas, lo que necesites.

## El stack y por qué

Elegí **Python + uv + FastMCP + psycopg3**:

- El 90% del trabajo son consultas al catálogo de Postgres (`pg_catalog` / `information_schema`) que se mapean a JSON. Python es conciso para eso.
- **FastMCP** elimina boilerplate: una tool es una función con un decorador.
- **uv** da entornos reproducibles y arranques rápidos.

¿La alternativa? TypeScript estricto, que conviene **solo si** el objetivo es publicar en npm para terceros. La lección: **elige el stack según cómo vas a distribuirlo y mantenerlo**, no por moda.

```python
@mcp.tool()
def list_tables(schema: str = "public") -> list[dict]:
    """Lista las tablas de un esquema con su tamaño."""
    ...
```

## Qué expone: introspección completa, no solo tablas

Para que una IA **razone** sobre una base, necesita ver la estructura y las relaciones, no solo los datos. El servidor expone tools para:

- Esquemas, tablas y columnas (tipos, defaults, identity/generated).
- Claves primarias, constraints y **relaciones** (FKs entrantes y salientes).
- Índices, triggers, funciones y procedures (con su código fuente), vistas y enums.
- Estadísticas de tablas.

Con eso, el modelo puede responder «¿qué tablas referencian a `posts`?» o «¿qué hace este trigger?» sin que tenga que pegarle el esquema a mano.

## La parte interesante: darle escritura a una IA sin romper la base

Aquí está el corazón del proyecto. La idea de fondo: **darle permiso de escritura a una IA no tiene por qué ser peligroso si diseñas barandas**.

### Modos de acceso (menor privilegio)

El modo es un parámetro del servidor:

- `readonly` (por defecto): solo lectura; la sesión se fuerza a read-only.
- `readwrite`: habilita INSERT/UPDATE/DELETE/MERGE.
- `admin`: habilita además DDL (CREATE/ALTER/DROP).

El mismo servidor sirve a quien solo quiere leer y a quien necesita editar, sin sacrificar seguridad.

### Defensa en profundidad: tres capas independientes

1. **Rol de base con privilegios mínimos.** La seguridad empieza en la DB, idealmente con un usuario read-only. No en el código.
2. **Sesión endurecida.** En readonly la conexión va con `default_transaction_read_only=on`; en todos los modos hay `statement_timeout` e `idle_in_transaction_session_timeout`. Aunque algo se cuele, el motor rechaza la escritura o corta consultas eternas.
3. **Guardas a nivel de tool.** Las tools de escritura **ni se registran** en modos inferiores (no existen para el modelo); el runner de SELECT rechaza lo que no sea lectura y limita las filas.

### El patrón estrella: *dry-run + confirm*

Toda escritura corre primero como **simulación**: se ejecuta dentro de una transacción y se hace **ROLLBACK**, devolviendo cuántas filas se afectarían **sin persistir nada**. Solo cuando reviso el impacto, se vuelve a llamar con `confirm=true` para hacer COMMIT.

```text
1. execute_dml(sql, confirm=false)  -> DRY RUN, ROLLBACK, "afectaría 3 filas"
2. revisas el impacto
3. execute_dml(sql, confirm=true)   -> COMMIT
```

Reglas extra:

- Se **rechaza** un UPDATE/DELETE **sin WHERE** (salvo override explícito): afectaría toda la tabla.
- El cliente de IA pide aprobación humana en cada llamada → siempre hay **una persona en el bucle** antes de un cambio real.

Es la misma mentalidad de [select-only-seguridad-en-produccion](https://gabrielherencia.com/insights/select-only-seguridad-en-produccion), pero codificada dentro del propio servidor.

## Gotchas que aprendí en el camino

- **psycopg3 y los placeholders**: `IN %s` con una tupla falla con el binding del lado servidor. La forma correcta es `= ANY(%s)` / `<> ALL(%s)` con una lista. (Bug real que apareció y se corrigió.)
- **DDL no transaccional**: `CREATE INDEX CONCURRENTLY`, `CREATE/DROP DATABASE`, `VACUUM`… no corren dentro de una transacción, así que no admiten dry-run; hubo que darles un camino aparte y advertirlo.
- **TLS/SSL en la nube**: se resuelve agregando `?sslmode=require` a la cadena de conexión; psycopg3 lo maneja solo.
- **MCP sobre stdio en Docker**: el contenedor tiene que correr con `-i` (interactivo), porque la comunicación es por stdin/stdout.

## Cómo lo empaqueté para que cualquiera lo levante

- **Docker-first**: `docker run -i --rm`, con la conexión por variable de entorno. Los secretos **nunca** van en la imagen.
- **uvx desde Git**: `uvx --from git+<repo> postgres-mcp` lo corre sin clonar ni instalar nada.
- **Lockfile (`uv.lock`) versionado** para builds reproducibles.
- **Secretos fuera del repo**: `.env` en `.gitignore` y el `.mcp.json` con credenciales nunca se sube al repo. (Y si alguna credencial se expuso: rótala.)

Lo probé de punta a punta contra una Postgres desechable en Docker y contra una base gestionada en la nube.

## Cierre: barandas, no prohibiciones

Construir el MCP me dejó un principio que aplico a todo lo que automatizo: **el objetivo no es prohibirle cosas a la IA, es ponerle barandas**. Previsualizar el impacto antes de aplicar, forzar el menor privilegio y mantener a una persona en el bucle convierten «darle acceso de escritura a un modelo» en algo aburrido y seguro.

Esto es, en el fondo, [automatizar-las-barreras-mcp-hooks-skills](https://gabrielherencia.com/insights/automatizar-las-barreras-mcp-hooks-skills): que las reglas dejen de depender de mi memoria y vivan en infraestructura. Y se apoya en buscar siempre la [verdad-de-fondo-depurar-con-evidencia](https://gabrielherencia.com/insights/verdad-de-fondo-depurar-con-evidencia) — un MCP de introspección es justamente eso: ver la estructura real en vez de suponerla.

El repo es público y MIT, listo para clonar y probar: [github.com/gabriel-herencia/postgres-mcp](https://github.com/gabriel-herencia/postgres-mcp). Si trabajas con IA y bases de datos, construye el tuyo — con barandas.
