Skip to main content
Glama
sistemasLang

mcp-popey

by sistemasLang

MCP-Popey

Servidor MCP de consulta de solo lectura sobre la base de Popey ERP, organizada por circuitos de negocio (venta, compras, stock, etc.).

Estructura

config/circuits.yaml   # datos: qué circuitos existen, qué tablas tiene cada uno
src/circuits.py        # puerta de entrada a circuits.yaml (lo lee una vez, lo cachea)
src/db.py              # acceso a Postgres (pool, guardas de solo-lectura, chequeo de rol)
src/server.py          # servidor MCP: junta circuits.py + db.py en 3 tools
requirements.txt

circuits.py y db.py son independientes entre sí — ninguno de los dos sabe que el otro existe. server.py es el único módulo que conoce a ambos.

Related MCP server: mcp-sqlserver-readonly

Requisitos

  • Python 3.12+ (probado con esa versión; no se testeó en versiones anteriores).

  • Acceso de red a una instancia de Postgres con el esquema de Popey ERP.

  • Un rol de Postgres genuinamente read-only (ver Seguridad — el servidor se niega a arrancar si detecta que el rol tiene algún permiso de escritura).

Instalación

cd "MCP-Popey"
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Si python3 -m venv falla por falta de ensurepip/python3-venv (ModuleNotFoundError: No module named 'ensurepip'), instalá el paquete del sistema (sudo apt install python3.12-venv en Debian/Ubuntu) o bootstrapeá pip a mano dentro del venv con get-pip.py.

Configuración (variables de entorno)

Variable

Obligatoria

Default

Descripción

PGHOST

No

localhost

Host de Postgres

PGPORT

No

5432

Puerto de Postgres

PGDATABASE

Nombre de la base

PGUSER

Rol de conexión — debe ser read-only, ver abajo

PGPASSWORD

Contraseña del rol

DB_POOL_MIN

No

1

Conexiones mínimas del pool

DB_POOL_MAX

No

5

Conexiones máximas del pool

DB_STATEMENT_TIMEOUT_MS

No

30000

statement_timeout de sesión (ms), aplicado a cada conexión del pool al crearse

DB_DEFAULT_ROW_LIMIT

No

1000

LIMIT que se agrega automáticamente a una query que no trae uno

Si falta alguna de las 3 obligatorias, el servidor no arranca y lo dice explícitamente (no hay fallback silencioso a valores de psycopg2).

Historial: el 2026-08-08 existió brevemente un DB_ALLOW_WRITABLE_ROLE como escape hatch para poder arrancar contra dev antes de tener el rol read-only dedicado (langdev, el rol usado hasta entonces, es dueño de tablas y superuser). Se sacó el mismo día al crear mcp_popey_ro — ver creación del rol read-only más abajo. El chequeo de rol no tiene bypass hoy: si el rol conectado tiene cualquier permiso de escritura, el servidor aborta, sin excepciones.

Correr el servidor

export PGHOST=...
export PGDATABASE=...
export PGUSER=...
export PGPASSWORD=...

python src/server.py

(Equivalente: cd src && python server.py — la resolución de rutas internas no depende del directorio desde el que se invoque.)

Corre sobre stdio por default (mcp.run(), transporte "stdio"): queda esperando mensajes JSON-RPC por stdin/stdout — no es para ejecutar suelto en una terminal y esperar ver algo, es para que lo levante un cliente MCP. No usar print() en ningún cambio a este código: con stdio, stdout es el canal del protocolo.

Conectarlo a un cliente MCP

Ejemplo de configuración (Claude Desktop, Claude Code vía .mcp.json, u otro cliente que use el mismo formato mcpServers):

{
  "mcpServers": {
    "popey-erp": {
      "command": "/ruta/a/MCP-Popey/.venv/bin/python",
      "args": ["/ruta/a/MCP-Popey/src/server.py"],
      "env": {
        "PGHOST": "...",
        "PGDATABASE": "...",
        "PGUSER": "...",
        "PGPASSWORD": "..."
      }
    }
  }
}

Las 3 tools

list_circuits()

Sin parámetros. Devuelve [{name, description}, ...] — los circuitos de negocio definidos en circuits.yaml.

query_circuit(circuito, sql, params=None)

Ejecuta un único SELECT de solo lectura. circuito (ej. "venta") es contexto informativo, no una restricción de acceso — ver política abajo. params son los parámetros de la query (lista para %s, dict para %(nombre)s); nunca interpolar valores directo en sql.

Devuelve {"rows": [...], "warnings": [...]}.

get_circuit_schema(circuito)

Junta la metadata de negocio de circuits.yaml (role, key_columns, source, status, notes, pending_columns) con las columnas reales de Postgres (information_schema.columns, acotado a las tablas del circuito) — pensado para que el modelo sepa qué puede pedir antes de armar el sql de query_circuit.

Seguridad y política de acceso

Hay dos capas independientes, ninguna reemplaza a la otra:

1. El rol de Postgres (barrera primaria)

PGUSER tiene que ser un rol read-only real (GRANT SELECT únicamente). El servidor no confía ciegamente en eso: al arrancar, db.init_pool() verifica el rol contra Postgres (superusuario, GRANT de escritura propio o de PUBLIC, ownership de alguna tabla, o CREATE sobre algún schema) y, si encuentra cualquier permiso de escritura o DDL, loguea CRITICAL y aborta el arranque — no levanta el servidor con la barrera primaria comprometida. No hay forma de saltear este chequeo desde configuración (ver historial de DB_ALLOW_WRITABLE_ROLE arriba).

Creación del rol read-only (mcp_popey_ro)

En el entorno de dev actual, Postgres corre en un contenedor Docker (lang_docker_database_1) sin volumen persistente: cada vez que se recrea el contenedor, se pierde el cluster entero, rol incluido. Por eso el rol read-only se recrea con un script versionado en vez de dejarlo como comandos sueltos:

docker cp scripts/create_readonly_role.sql lang_docker_database_1:/tmp/
docker exec -it lang_docker_database_1 \
  psql -U langdev -d langdev -v ON_ERROR_STOP=1 \
  -v ro_password='<elegir una password>' \
  -f /tmp/create_readonly_role.sql

Crea (o actualiza la password de) el rol mcp_popey_ro con GRANT SELECT únicamente sobre los schemas que aparecen en config/circuits.yaml (administracion, compra, e_plataforma, mercado_libre, public, servicio_tecnico, stock, util, venta) — no sobre todos los schemas de la base. Si circuits.yaml suma un circuito en un schema nuevo, hay que agregar ese schema a scripts/create_readonly_role.sql y volver a correrlo (los GRANT son idempotentes). Después de correrlo, poner PGUSER=mcp_popey_ro y esa misma password en PGPASSWORD del .env.

2. La guarda de código (segunda capa, defensa en profundidad)

Independientemente del rol, db.py valida cada SQL antes de mandarlo a Postgres:

  • tiene que empezar con SELECT;

  • una sola sentencia (rechaza ; interno — bloquea múltiples sentencias);

  • sin palabras prohibidas en ningún lugar de la query (INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, pg_sleep, dblink, etc.).

Un DELETE/UPDATE mandado a query_circuit se rechaza acá, en Python, antes de llegar a la base — no depende de que Postgres tire un error de permisos.

circuits.yaml es informativo, no una barrera de acceso

Decisión explícita: el circuito pedido en query_circuit nunca bloquea qué se puede leer. Tres situaciones generan un aviso en warnings pero la query se ejecuta igual:

  • la tabla no pertenece al circuito pedido (pertenece a otro, o a ninguno);

  • la tabla está marcada status: needs_review en circuits.yaml;

  • la tabla está marcada status: legacy en circuits.yaml.

Única excepción bloqueante a esta política: si circuito no es uno de los circuitos existentes (list_circuits()), query_circuit y get_circuit_schema rechazan antes de ejecutar nada. No es un problema de status de una tabla — es la ausencia total de un circuito de referencia: sin eso no hay contra qué avisar, no hay "role/notes" que citar en un warning informativo. Por eso ahí sí se corta.

Auditoría

Cada llamada a query_circuit que llega a ejecutarse loguea (vía el logger popey-mcp.server, nivel WARNING si hubo algún aviso, INFO si no): el circuito pedido, el SQL final ejecutado (con el LIMIT ya aplicado) y las tablas que generaron warning. Limitación conocida: los intentos rechazados por la guarda de código (SELECT-only) o que fallan contra Postgres no quedan auditados — el log se emite justo antes del return, y una excepción corta el flujo antes de llegar ahí.

Troubleshooting

  • "Faltan variables de entorno obligatorias para conectar a Postgres" — falta PGDATABASE, PGUSER o PGPASSWORD.

  • El servidor no arranca y loguea CRITICAL sobre permisos de escritura/DDLPGUSER no es read-only; corregir los GRANT del rol en Postgres (no hay forma de saltear este chequeo desde acá a propósito).

  • ModuleNotFoundError al importar mcp, sqlglot o psycopg2 — faltó pip install -r requirements.txt (o el venv no está activado).

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    -
    quality
    F
    maintenance
    Read-only MCP server for SQL databases (SQL Server, Postgres, SQLite) with multi-server support and three-layer safety using AST validation and linting.
    MIT
  • A
    license
    -
    quality
    B
    maintenance
    Read-only MCP server for PostgreSQL, enabling schema discovery, table metadata, and safe SELECT queries via READ ONLY transactions.
    26
    MIT
  • F
    license
    -
    quality
    C
    maintenance
    A read-only MCP server for exploring and querying Oracle schemas safely. Provides tools for table listing, schema description, column search, and validated SELECT execution.
    2

View all related MCP servers

Related MCP Connectors

  • Read-only MCP server for wafergraph.com's semiconductor & AI supply-chain data: 30 tools, no auth.

  • Read-only MCP server for ClassQuill, a tutoring-business-management platform.

  • Read-only MCP server for Muovi, Argentina's trust-first local services marketplace (6 tools).

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sistemasLang/mcp-popey'

If you have feedback or need assistance with the MCP directory API, please join our Discord server