dynamo-auth
Levantar el MCP de Dynamo Auth
Este MCP corre local en tu máquina, transporte
stdio — no hay deploy a Azure, no hay URL pública.
Te da tools para consultar y administrar el sistema de
acceso de Dynamo (proyectos, usuarios, miembros, apps
habilitadas, quién tiene acceso a qué, invitaciones y bajas)
actuando siempre como tu propio usuario — con tus
permisos, nunca con una clave de servicio. Al final vas a poder
llamar auth_session_status desde Claude Code y ver una
respuesta real: quién sos y contra qué ambiente estás.
Este manual usa comandos de Claude Code. Para
Claude Desktop el flujo de instalar/loguear/
compilar (§1-3) es idéntico — solo cambia dónde pegás el bloque
JSON final. Para Claude Mobile: no es posible,
stdio exige un proceso local que el celular no puede
correr. Detalle de los tres clientes:
Conectar desde Claude.
La trampa más importante de este MCP en particular:
varias tools de lectura no te dicen que no tenés permiso —
te devuelven una lista vacía. El gate está escrito dentro
del WHERE de la función de base de datos
(AND is_global_admin(auth.uid())), así que un usuario
sin permisos ve cero filas, exactamente igual que si el sistema
estuviera vacío. Si algo te devuelve vacío y esperabas
datos, lo primero que hay que mirar es
auth_session_status, no la base. Cada tool
afectada te lo recuerda en el campo note de su
respuesta.
0. Qué necesitás antes de empezar
-
Node.js 20+ instalado — corré
node --version, debe imprimirv20.xo más. Con Node 18 nada de esto funciona. -
El repo
auth-dynamoclonado en tu máquina. Si no lo tenés:git clone git@github.com-dynamo:DynamoTechTVT/auth-dynamo.git. -
gh(GitHub CLI) autenticado con la cuenta de Dynamo. El paquete depende de@dynamotechtvt/mcp-core, que vive en GitHub Packages (privado) — sin token no instala. Verificá congh auth statusque aparezca la organizaciónDynamoTechTVT. -
Un usuario real de Supabase Auth. Para que las tools devuelvan algo
útil necesitás ser global-admin (la mayoría) o al
menos project-admin del proyecto que consultás
(
auth_list_project_membersyauth_list_project_app_access). Si no lo sos, vas a ver listas vacías — no errores. -
El ambiente contra el que vas a apuntar. Este
manual usa QA (
bxogqotctuurtcgjakfz.supabase.co) porque es el ambiente seguro para probar — este MCP tiene 6 tools de escritura sobre el sistema de permisos, así que no repitas estos pasos contra PROD sin saber lo que hacés.
1. Instalar dependencias del paquete mcp/
Andá a la carpeta mcp/ del repo (NO la raíz, son
paquetes Node distintos — mcp/ tiene su propio
package-lock.json y no está en los workspaces del root):
cd auth-dynamo/mcp
NODE_AUTH_TOKEN=$(gh auth token) npm ci
Qué tenés que ver: una línea final del estilo
added 252 packages in 11s.
El NODE_AUTH_TOKEN=$(gh auth token) no es opcional y
no se reemplaza pegando un token a mano: saca el token de tu
sesión de gh ya autenticada y lo pasa
solo para esa invocación (no queda en el
historial ni en ningún archivo). En esta máquina la cuenta de
gh se resuelve por carpeta — tenés
que estar dentro de ~/dev/Dynamo/** para que agarre
la cuenta de Dynamo.
401 Unauthorized = tu gh no está
autenticado con la cuenta de Dynamo.
403 Forbidden = la cuenta está bien pero al repo le
falta el permiso de lectura sobre el paquete; eso lo otorga Daniel
en Package settings → Manage Actions access →
auth-dynamo: Read.
2. Generar tu sesión (npm run login)
El MCP necesita un JWT de sesión guardado en tu disco
(~/.dynamo/dynamo-qa-session.json). Exportá estas 4
variables en la misma terminal donde vas a correr
el comando (no las guardes en ningún archivo commiteado):
export SUPABASE_URL="https://bxogqotctuurtcgjakfz.supabase.co"
export ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4b2dxb3RjdHV1cnRjZ2pha2Z6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODIxODY5OTcsImV4cCI6MjA5Nzc2Mjk5N30.IL2_oF_UWFuOMZfvD6rJ4g4f87q4f9ioROJrxGss0qg"
export DYNAMO_LOGIN_EMAIL="tu-email@ejemplo.com"
export DYNAMO_LOGIN_PASSWORD="tu-password-de-supabase-auth"
La key de arriba es la anon de QA —
es pública por diseño (la misma que va en el
cliente web). En el Dashboard de Supabase, al lado, hay una
service_role marcada como "secret":
esa NO. El MCP no puede distinguirlas: si le
pasás la service_role, el modelo de seguridad entero
se cae y no te vas a enterar. Para DEV/PROD el ref y su anon key
salen del Dashboard del proyecto (Settings → API Keys) — no están
en este manual.
Corré:
npm run login
Qué tenés que ver: primero una línea
Ambiente: QA — la sesión se escribe en
/Users/<vos>/.dynamo/dynamo-qa-session.json, y después
la confirmación de que la sesión quedó guardada.
Si en cambio el login falla con credenciales inválidas, tu usuario probablemente no tiene password configurado (entrás siempre por magic-link). En ese caso usá el fallback OTP, que son dos corridas:
unset DYNAMO_LOGIN_PASSWORD
export DYNAMO_LOGIN_OTP=1
npm run login
# Te llega un código de 6 dígitos al email. Corré de nuevo con el código:
export DYNAMO_LOGIN_OTP_CODE="123456"
npm run login
Verificación de este paso: corré
ls -l ~/.dynamo/dynamo-qa-session.json — la línea
tiene que empezar con -rw-------. Esos permisos
(600) son a propósito: el archivo tiene tus tokens. Si
el archivo no existe, no sigas hasta que exista.
Un login sirve para todos los MCPs de Dynamo del mismo
ambiente. El archivo se llama
dynamo-qa-session.json, no auth-…,
porque auth, forms y analytics comparten el mismo proyecto
Supabase por ambiente. Si ya hiciste login para
Forms o
Analytics contra QA, este paso ya
está hecho.
3. Compilar el server
npm run build
Qué tenés que ver: nada. Cero salida es éxito
(tsc solo habla cuando hay errores). Confirmá que
generó el ejecutable con ls dist/server.js.
Cada vez que hagas git pull y traigas cambios de
mcp/, volvé a correr
npm run build. Si no, Claude Code sigue
ejecutando el dist/ viejo y vas a ver herramientas
desactualizadas sin ningún aviso.
auth_session_status reporta la versión del paquete y
el mtime del archivo de sesión justamente para que
puedas detectar un dist/ rancio.
4. Configurar Claude Code (transporte stdio)
Claude Code necesita la ruta absoluta al
dist/server.js. Conseguila con pwd desde
mcp/ y reemplazala abajo:
claude mcp add dynamo-auth \
--env SUPABASE_URL=https://bxogqotctuurtcgjakfz.supabase.co \
--env ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4b2dxb3RjdHV1cnRjZ2pha2Z6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODIxODY5OTcsImV4cCI6MjA5Nzc2Mjk5N30.IL2_oF_UWFuOMZfvD6rJ4g4f87q4f9ioROJrxGss0qg" \
-- node /RUTA/ABSOLUTA/A/auth-dynamo/mcp/dist/server.js
Qué tenés que ver:
Added stdio MCP server dynamo-auth to local config.
Verificá con claude mcp list — tiene que aparecer
dynamo-auth con ✓ Connected a la
derecha. El server no necesita tokens acá: lee la sesión directo
del archivo del paso 2.
Equivalente en JSON (editar a mano)
{
"mcpServers": {
"dynamo-auth": {
"command": "node",
"args": ["/RUTA/ABSOLUTA/A/auth-dynamo/mcp/dist/server.js"],
"env": {
"SUPABASE_URL": "https://bxogqotctuurtcgjakfz.supabase.co",
"ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4b2dxb3RjdHV1cnRjZ2pha2Z6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODIxODY5OTcsImV4cCI6MjA5Nzc2Mjk5N30.IL2_oF_UWFuOMZfvD6rJ4g4f87q4f9ioROJrxGss0qg"
}
}
}
}
Claude Desktop: el mismo bloque JSON, pegado en
claude_desktop_config.json (macOS:
~/Library/Application Support/Claude/claude_desktop_config.json).
Reiniciá Claude Desktop después de guardar.
Si dice ✗ Failed to connect: casi
siempre es una de tres — la ruta del server.js está
mal, no corriste npm run build, o falta una de las dos
variables de entorno. Arrancalo a mano para ver el error real:
SUPABASE_URL="https://bxogqotctuurtcgjakfz.supabase.co" ANON_KEY="<ANON_KEY>" node dist/server.js
Tiene que quedar colgado esperando (es correcto: habla por
stdin/stdout) y escribir
[dynamo-auth-mcp] Listo — v0.1.0, 12 tools registradas,
transport stdio.. Cortalo con Ctrl+C.
5. Verificar que responde
Reiniciá la sesión de Claude Code (o abrí una nueva) y pedile:
Usá auth_session_status para decirme contra qué ambiente estoy corriendo y quién soy.
Qué tenés que ver: algo así.
{
"authenticated": true,
"email": "tu.email@dynamotech.co",
"user_id": "…",
"env": "QA",
"version": "0.1.0",
"is_global_admin": true,
"admin_project_ids": [],
"multi_client": false
}
Si ves eso, terminaste. Si ves
"authenticated": false, la sesión no se hidrató:
repetí el paso 2 y reiniciá Claude Code — el MCP
lee la sesión al arrancar, no en cada llamada.
6. auth_session_status — el diagnóstico antes de operar
Es la única tool sin requisitos, siempre corrible primero. Te dice
quién sos (email/user_id),
contra qué ambiente estás apuntando
(DEV/QA/PROD, resuelto del SUPABASE_URL), si sos
global-admin, de qué proyectos sos project-admin
(admin_project_ids) y si tenés la flag
multi_client. Nunca devuelve el JWT, solo su expiry.
PROD ⚠️. Cuando el ambiente resuelto es
producción, auth_session_status lo marca
explícitamente con ⚠️. Correla antes de
cualquier escritura: acá hay 6 tools que escriben sobre
el sistema de permisos, y nada te impide invitar, desactivar o
deshabilitar apps de un cliente real si tenés permisos para
hacerlo. Es el mismo mecanismo de anon-key/ref en los tres
ambientes — solo cambia la URL, y es fácil creer que estás en QA
cuando apuntás a PROD. Si no era tu intención, cambiá
SUPABASE_URL a QA en la config del MCP y reiniciá
Claude Code.
UNKNOWN ⚠️. Aparece cuando la
SUPABASE_URL que configuraste no corresponde a
ninguno de los tres ambientes conocidos. Se marca con ⚠️ a
propósito: un ambiente que no reconocemos se trata como
riesgo, no como "seguro por defecto". Revisá la
URL.
7. Inventario de tools (12 total)
6 de lectura + 6 de escritura. Cada una valida su input y traduce cualquier error de la base a un mensaje legible en español — nunca se escapa una excepción cruda. No hace falta llamarlas por nombre: pedile a Claude lo que querés en castellano ("dale acceso a analytics a juan@cliente.com en el proyecto Acme") y elige la herramienta sola.
Lectura (6)
| Tool | Qué hace |
|---|---|
auth_session_status | Quién sos, contra qué ambiente, si sos global-admin y de qué proyectos sos project-admin (§6). Correla antes de cualquier cosa importante, y siempre que algo te devuelva vacío. |
auth_list_projects | Todos los proyectos de la plataforma, con cuántos miembros y cuántas apps tiene cada uno. Es la que usás para pasar de un nombre de proyecto a su project_id. Global-admin only, gate por filtro. |
auth_list_users | Busca usuarios por email o nombre (coincidencia parcial). Devuelve su rol, sus proyectos y si está desactivado. |
auth_list_project_members | Quiénes son los miembros de un proyecto y cuáles de ellos son administradores de ese proyecto. Acepta global-admin o project-admin de ese proyecto. |
auth_list_project_apps | Qué apps están habilitadas en un proyecto. Es la más estricta: global-admin y nadie más. |
auth_list_project_app_access | Quién tiene acceso a qué app dentro de un proyecto. Acepta global-admin o project-admin de ese proyecto. |
Escritura (6)
| Tool | Qué hace |
|---|---|
auth_invite_user | Invita a alguien nuevo: lo crea, le manda el correo, lo mete al proyecto y le da las apps. Si el email existía desactivado, lo reactiva. Va por la Edge Function admin-invite-user. Global-admin only. |
auth_add_project_member | Mete a un usuario que ya existe en un proyecto, opcionalmente con apps, en una sola transacción. Idempotente. |
auth_remove_project_member | Lo saca de un proyecto (no lo borra, no lo toca en otros proyectos). |
auth_set_app_access | Da o quita el acceso de una persona a una app en un proyecto. Idempotente en los dos sentidos. |
auth_set_project_app | Habilita o deshabilita una app en el proyecto. Solo global-admin. Deshabilitar exige confirm_cascade: true. |
auth_set_user_deactivated | Da de baja (o vuelve a habilitar) a un usuario en toda la plataforma. Va por la Edge Function admin-deactivate-user. Global-admin only. |
El modelo de acceso, en tres frases
Sin esto las respuestas no tienen sentido:
- Todo es por proyecto. No existe "tener acceso a forms" a secas: existe "tener acceso a forms en el proyecto X".
-
Son dos capas, y el orden importa. Primero la app
se habilita en el proyecto
(
auth_set_project_app/auth_list_project_apps), y recién después se le puede dar a un usuario (auth_set_app_access/auth_list_project_app_access). Dar acceso a una app que no está habilitada en el proyecto falla. -
Los global-admins no aparecen en la lista de accesos.
Pueden usar todo y son miembros automáticos de todos los
proyectos, pero por su rol — no por una fila de permiso. Que
alguien no figure en
auth_list_project_app_accessno quiere decir que no pueda entrar.
Cuatro cosas que hay que saber antes de escribir
-
Habilitar o deshabilitar una app arrastra a mucha gente de
golpe. Al habilitarla, la app se le da automáticamente a
todos los administradores del proyecto. Al
deshabilitarla, se borran todos los accesos de esa
app en ese proyecto — y volver a habilitarla no los
devuelve: hay que otorgarlos de nuevo uno por uno. Por eso
deshabilitar exige
confirm_cascade: true, y la tool se niega antes de tocar la base si no lo pasás. Antes de deshabilitar, pedile a Claude la lista actual de accesos y guardala: es la única forma de poder reconstruirla. -
La app
authno es una app: es el interruptor de "administrador del proyecto". Dársela a alguien lo promueve a admin de ese proyecto; quitársela lo degrada. Y promover a alguien le da automáticamente todas las apps habilitadas del proyecto, no solo las que pidas. Si lo que querés es darle una herramienta, no usesauth. -
Las apps que no estén habilitadas en el proyecto se
saltean en silencio. Si metés a alguien pidiendo
["forms", "crm"]y el proyecto no tienecrmhabilitada, la operación no falla: le daformsy se olvida decrm, sin avisar. La función de base no puede reportarlo (devuelvevoid), así que la tool te lo advierte en la respuesta. Verificá siempre el resultado real conauth_list_project_app_access. -
Desactivar a alguien no lo saca en el acto. Le
bloquea la renovación del token, pero su sesión abierta
sigue viva hasta que expire — hasta una hora. El campo
sessions_revokedviene siempre enfalse, y no es un bug: el corte inmediato no existe por esta vía.
Las dos operaciones privilegiadas van por Edge Function
auth_invite_user y auth_set_user_deactivated
necesitan la service_role key de Supabase — crear un
usuario y banearlo no se pueden hacer con una sesión de usuario
común. El MCP nunca ve esa clave. Esas dos
operaciones viven dentro de Edge Functions
(admin-invite-user y admin-deactivate-user):
el MCP las invoca como vos, y la función
vuelve a verificar tus permisos con tu propio JWT
(construye un cliente con tu bearer y llama
is_global_admin) antes de usar su clave
privilegiada. Hay un test automatizado que falla el CI si alguien
intenta meter esa clave en el MCP
(mcp/src/__tests__/no-service-role.test.ts).
Lo que este MCP deliberadamente NO puede hacer
No hay tools para promover a alguien a
global_admin, para cambiar por la vía directa
quién es administrador de un proyecto, ni para borrar un
proyecto. No es un olvido: las dos primeras son escalada de
privilegios y la tercera arrastra en cascada a miembros y apps de
forma irreversible. Se hacen desde el portal, con una persona
mirando. Si Claude te dice que no puede promover a alguien a
global-admin, está funcionando bien. Tampoco hay borrado de
usuarios, porque el sistema no borra usuarios: los desactiva
(auth_set_user_deactivated), que es reversible y queda
auditado. Y no es un bypass de permisos: usa tu sesión — lo que no
ves en el portal tampoco lo ves acá — ni toca infraestructura: un
write del MCP no es un deploy.
8. Cuando el refresh token expira
Si después de mucho tiempo sin usarlo las tools empiezan a fallar
con mensajes de sesión inválida/expirada, repetí el
paso 2 (npm run login) y reiniciá
Claude Code — no hace falta reconfigurar Claude Code de nuevo.
9. Troubleshooting rápido
-
Una lista vacía cuando esperabas datos — es lo
primero que hay que descartar, y casi nunca es un bug. El control
de permisos está dentro del filtro: si no sos global-admin no
recibís un error, recibís cero filas. Corré
auth_session_statusy miráis_global_admin. -
"No tenés permiso para esta operación"— ese sí es elforbiddenexplícito, y viene de la base, no del MCP. La mayoría de estas operaciones exigen global-admin; algunas aceptan además al project-admin de ese proyecto en particular. -
auth_list_usersdevuelve exactamente 100 resultados — no es casualidad: la función tiene un tope de 100 filas del lado del servidor, ordenadas por email, y no se puede paginar. Estás viendo los primeros 100 alfabéticamente y hay más que no ves. Usá el parámetro de búsqueda para acotar. -
"No se pudo identificar al usuario"al pasar un email — la resolución email→user_idtambién tiene el gate por filtro: si no sos global-admin devuelve "no encontrado" en lugar de "no tenés permiso". Un email que existe puede darte "no existe" simplemente porque no podés verlo. -
"La app no está habilitada en el proyecto"— te salteaste la capa de arriba. Primeroauth_set_project_app, despuésauth_set_app_access. -
"El usuario no es miembro del proyecto"— agregalo primero conauth_add_project_member; podés pasarle las apps en la misma llamada y te ahorrás el paso. -
"No se puede sacar a un global-admin de un proyecto"— correcto y es incondicional: ni otro global-admin puede. Un global-admin es miembro automático de todos los proyectos y esa membresía no se regenera sola. Si querés sacarle el acceso, la tool esauth_set_user_deactivated. -
"Es el último global-admin activo"— bloqueo anti-lockout: si lo desactivás, la plataforma queda sin nadie capaz de reactivar a nadie. Promové o reactivá otro global-admin primero. -
deactivated_atcon fecha en vez de un usuario que desapareció — el sistema no borra usuarios: los desactiva. Sigue existiendo, conserva su historia y puede reactivarse. -
403 Forbiddenen elnpm ci— falta el grant de lectura del paquete@dynamotechtvt/mcp-core(§1). -
Tools desactualizadas después de un
git pull— faltónpm run build(§3).
Tabla completa de errores y causas: Troubleshooting.