Gabriel Herencia

~/insights / Herramientas de Desarrollo

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.

·5 min de lectura
Herramientas de DesarrolloDockerClaude CodeBases de DatosSeguridadMCP
Portada: Cómo construí mi propio MCP de PostgreSQL

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.

@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.

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, 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: 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 — 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. Si trabajas con IA y bases de datos, construye el tuyo — con barandas.