DBHub — servidor MCP para TRIVASADB3, en vm-playground

Instancia self-hosted de DBHub (bytebase/dbhub), un servidor MCP universal para bases SQL, publicada en dbhub.estebanalcocer.cloud para que Claude (en claude.ai chat, no solo Claude Code) pueda consultar 192.168.117.200/TRIVASADB3 directamente vía un custom connector — sin pasar por dlt/Metabase/Lightdash. Stack en ~/stacks/dbhub/docker-compose.yml.

Excepción al rol de workstation de vm-playground

Desde el rediseño del 2026-08-10 (ver hosts.md), vm-playground es workstation pura, sin servicios de producción. DBHub es la primera excepción: corre acá en vez de vm-main porque este host ya tenía NetBird conectado a la malla de Conkafecito (alcance directo a 192.168.117.0/24, confirmado con un test TCP a 192.168.117.207:1433 antes de levantar el stack) y conservaba el cert.pem de un túnel Cloudflare anterior ya borrado — condiciones que vm-main no tenía sin trabajo extra. Mismo patrón de excepción documentado ya para Home Assistant (corre en pop-os por la LAN de casa) e Infisical (comparte host con Conkafecito pero publica en la cuenta personal).

Por qué TRIVASADB3 y no la copia de producción

Hay tres/cuatro copias de TRIVASADB con distinto rol (detalle en Conexiones a TRIVASADB). Se eligió 192.168.117.200/TRIVASADB3 — copia viva pero no productiva — en vez de 192.168.117.207/TRIVASADB (producción) a propósito: un query mal armado o pesado desde un cliente de IA no le pega a la base transaccional real.

Guardrails de solo lectura

DBHub soporta modo solo-lectura vía TOML (no hay flag CLI para esto), configurado en ~/stacks/dbhub/config.toml:

[[sources]]
id = "trivasadb3"
type = "sqlserver"
host = "192.168.117.200"
port = 1433
database = "TRIVASADB3"
user = "${DB_USER}"
password = "${DB_PASSWORD}"
query_timeout = 30
 
[[tools]]
name = "execute_sql"
source = "trivasadb3"
readonly = true
max_rows = 1000

Verificado en vivo tras levantar el stack: un SELECT normal contra INFORMATION_SCHEMA.TABLES regresa filas; un CREATE TABLE de prueba se rechaza con {"success": false, "error": "Read-only mode is enabled...", "code": "READONLY_VIOLATION"} antes de tocar la base.

Gotcha: env_file de Compose también escapa $$

El password de SQL Server contiene $$ (dos signos de pesos literales). Docker Compose aplica su regla de escapado ($$ → un solo $ literal) no solo a interpolación directa en el YAML, sino también al parsear valores de env_file — algo no evidente porque env_file se documenta como “carga variables tal cual”. El primer intento dejó DB_PASSWORD=p4$w0rd (un solo $) llegando al contenedor y el login a SQL Server fallaba con ELOGIN. Se confirmó con docker inspect dbhub --format '{{range .Config.Env}}...' comparando el valor real recibido contra el escrito en .env. Fix: escribir el doble de signos de pesos en .env (p4$$$$w0rd) para que, tras el escapado de Compose, lleguen los dos $ reales. Cualquier secreto con $ en este tipo de stack necesita este mismo ajuste.

Publicación: túnel Cloudflare nuevo en vm-playground

vm-playground se quedó sin túnel propio el 2026-08-10 (ver Túneles de Cloudflare), pero conservaba el cert.pem de la cuenta estebanalcocer.cloud en ~/.cloudflared/ de un túnel anterior ya borrado, así que crear uno nuevo no requirió volver a autenticar la cuenta:

  • cloudflared tunnel create vm-playground → id b645f9f8-2cc9-4af9-95fd-73039c58d11f
  • cloudflared tunnel route dns vm-playground dbhub.estebanalcocer.cloud
  • Config propio, /etc/cloudflared/config-estebanalcocer.yml, ingress único hacia http://localhost:8080 (el contenedor publica en 127.0.0.1:8080:8080, nunca en todas las interfaces — mismo patrón que el resto de servicios detrás de túnel).
  • Servicio systemd dedicado cloudflared-estebanalcocer.service (mismo nombre que el de vm-main, por convención de nombrarlo por zona/cuenta, no por host) — nunca se reusa el cloudflared.service genérico, porque un solo proceso cloudflared tunnel run solo atiende un túnel/config a la vez.

Gotcha: --allowed-hosts bloquea el hostname público por default

DBHub trae protección anti DNS-rebinding: por default solo acepta requests con header Host: localhost / 127.0.0.1 / la IP interna del contenedor. El túnel de Cloudflare reenvía el Host original de la petición (dbhub.estebanalcocer.cloud), así que sin ajuste el primer request público devolvía 401/rechazo aunque el bearer token fuera correcto. Fix: agregar el hostname público a la lista (se suma a los defaults, no los reemplaza):

--allowed-hosts dbhub.estebanalcocer.cloud,localhost,127.0.0.1

Auth del endpoint

--auth-token / DBHUB_AUTH_TOKEN (bearer token) es la única barrera de auth delante del endpoint — se consideró suficiente dado que además está limitado a solo-lectura y a una copia no productiva de la base, mismo razonamiento que llevó a no ponerle Cloudflare Access a Infisical (ver esa página, sección homónima). Token generado con openssl rand -hex 32 (mismo patrón que las demás credenciales de este tipo de stack) y guardado en Infisical bajo la clave DBHUB_AUTH_TOKEN — el .env local del stack (chmod 600, no versionado) sigue siendo la fuente que efectivamente usa el contenedor.

Registrar el conector en claude.ai

Settings → Connectors → Add custom connector:

  • URL: https://dbhub.estebanalcocer.cloud/mcp
  • Advanced → Request headers: Authorization: Bearer <DBHUB_AUTH_TOKEN> (valor en Infisical)

Véase también

  • Túneles de Cloudflare — túnel vm-playground nuevo y su patrón de config/systemd
  • Gestión de secretos — dónde vive DBHUB_AUTH_TOKEN
  • Conexiones a TRIVASADB — por qué .200/TRIVASADB3 y no .207 (producción)
  • Infisical — mismo razonamiento de “auth propio del servicio es suficiente, sin Cloudflare Access encima”
  • NetBird — malla que le da a vm-playground alcance directo a 192.168.117.0/24