· 9 min de lectura

Lo que costó construir un servidor MCP de DynamoDB seguro

El hace que sea trivial entregarle tu base de datos a un . Ese es el problema. La mayoría de los servidores MCP de DynamoDB son un proceso ligero que sostiene tus credenciales de AWS en crudo con una ruta de escritura y sin paso de revisión: ya hemos escrito sobre por qué ese patrón es peligroso y cómo elegir de forma segura desde el lado del usuario.

Esta entrada es la otra cara: lo que realmente costó construir la versión segura. DynoTable es un cliente de escritorio de DynamoDB cuyo servidor MCP expone su conjunto de herramientas de IA controladas a agentes externos —Claude Code, Cursor, Codex— sin que el agente llegue a tocar nunca las credenciales de AWS. Llegar hasta ahí implicó lanzar un servidor de autorización dentro de una app de Electron, diseñar un tipo de identidad que hace que las confusiones de credenciales sean irrepresentables y decidir, más de una vez, que la opción por defecto cómoda era la equivocada.

Si estás construyendo un servidor MCP en torno a cualquier cosa sensible, estas son las decisiones que también te encontrarás.

HTTP por loopback no es automáticamente seguro

El servidor se enlaza a 127.0.0.1, lo que suena seguro pero no es suficiente. Cualquier página web que visites puede intentar peticiones hacia localhost, y el rebinding de DNS permite que una página hostil las disfrace con un Host verosímil. Así que la primera capa es una protección de origen: rechaza cualquier petición que lleve un Origin real de página web, rechaza cabeceras Host que no sean de loopback, pero permite peticiones sin ningún Origin, porque los clientes de CLI legítimos como Claude Code no envían ninguno.

Esa última cláusula es la parte honesta: una protección de origen solo bloquea la clase de ataque desde el navegador. No puede autenticar a nadie. El token de portador es la verdadera puerta, lo que plantea la pregunta de dónde salen los tokens.

Lanzamos un servidor de autorización OAuth 2.1 dentro de la app

La historia de autenticación de servidor remoto de MCP es OAuth 2.1, y para un servidor de escritorio local no hay ningún proveedor de identidad externo en el que apoyarse. Así que la app es el servidor de autorización: registro dinámico de clientes, (S256), códigos de autorización, tokens de refresco, revocación y los documentos de metadatos de RFC 8414 / RFC 9728 que permiten a los clientes descubrirlo todo.

Reglas que fijamos pronto y mantuvimos:

  • No implementes el protocolo a mano. El SDK de MCP incluye toda la superficie del AS como un router; nosotros implementamos solo el proveedor de almacenamiento/política que hay debajo. Una consecuencia que aceptamos: nuestro framework de MCP habla una pila HTTP internamente y el router del SDK habla otra, así que el AS se ejecuta como un segundo listener de loopback, con el servidor de recursos apuntando a él a los clientes mediante metadatos de recurso protegido. Dos listeners es un coste acotado y explícito; re-derivar PKCE y la gramática de tokens a mano es uno ilimitado.
  • Las redirecciones son solo de loopback (según RFC 8252 para apps nativas), y una redirección inválida nunca recibe una redirección de error: recibe un 400 directo, así que la URI de un atacante nunca es un destino.
  • Los códigos de autorización son de un solo uso: un código reproducido es una concesión inválida, punto.
  • El refresco puede reducir el alcance pero nunca ampliarlo, y reemite la misma carga de identidad al pie de la letra: un token refrescado no puede adquirir discretamente una región distinta.
  • La revocación se vuelve a comprobar en cada petición, no se cachea al conectar: pulsar «Revocar» en Ajustes mata la siguiente llamada del cliente.

Las compilaciones de desarrollo usan en su lugar una vía de autenticación más simple, construida para que no pueda existir en un binario de lanzamiento: un token de alcance completo sin autenticar en software distribuido sería una puerta trasera local detrás de una protección de origen que deliberadamente admite peticiones sin Origin.

Una conexión, una identidad de AWS — transportada entera, nunca adivinada

La propiedad de seguridad central: un agente externo se conecta a un perfil, no a «DynoTable». Cada conexión está enlazada a un perfil de AWS y una región, respaldada por su propio cliente de DynamoDB no compartido, de modo que dos agentes en perfiles distintos no pueden mezclarse entre sí.

El enlace empezó su vida como campos sueltos —awsProfile, region, profileId, un puerto local opcional— redeclarados en cada registro de la ruta. Ahora es un único tipo de identidad de unión discriminada con dos brazos: online (perfil + región + id) y offline (puerto local + id, para perfiles de DynamoDB Local). El ser offline es el propio discriminante, así que una identidad online sin credenciales o una offline que lleve una región es un error de tipo, no una sorpresa en tiempo de ejecución. Ese único tipo viaja intacto a través de consentimiento → token → concesión → cada petición.

MCP server (tools)User (consent modal)In-app OAuth ASClaude Code / CursorMCP server (tools)User (consent modal)In-app OAuth ASClaude Code / Cursorregister + authorize (PKCE)which profile? which scope?profile P, read-onlycode → token (identity {P, region}pinned)tool call + bearer tokenverify, re-check revocation,clamp scope to license, resolve P's credsresult (agent never sees AWS keys)

Dos consecuencias que consideramos fundamentales:

  • La región se congela en el consentimiento. El usuario aprueba «perfil X en la región Y», y esa tupla se fija en el token, sobreviviendo al refresco y sin volver a resolverse a partir de estado mutable en la ruta caliente. Editar la región del perfil requiere volver a dar el consentimiento. La alternativa —resolver la región por petición o recurrir a un valor por defecto— es cómo un agente acaba consultando discretamente us-east-1.
  • Los tokens malformados son un 401, nunca una coacción. Si las reclamaciones de identidad de un token no se parsean en un brazo válido, la petición no está autorizada. Sin perfil 'default', sin región por defecto, sin cadena vacía. Cada sitio donde el código antiguo habría adivinado es ahora un fallo tajante.

El primer aviso de consentimiento era el cuadro de mensaje nativo de la plataforma. Congelaba el proceso principal de Electron mientras estaba abierto, no podía coincidir con el sistema de diseño de la app y ni siquiera se podía capturar para la documentación. Ahora es un modal dentro de la app: el nombre del cliente que se conecta, un selector de perfiles sobre exactamente los perfiles que el usuario eligió exponer y tres alcances ofrecidos del más estrecho primero: solo lectura, lectura + preparación, completo. Las peticiones de consentimiento caducadas o duplicadas se resuelven como denegadas en lugar de dejar colgado el flujo de OAuth.

El modal de consentimiento de MCP dentro de la app: el cliente que se conecta, el selector de perfiles y los tres alcances del más estrecho primero: solo lectura, lectura + preparación y acceso completo.
El modal de consentimiento de MCP dentro de la app: el cliente que se conecta, el selector de perfiles y los tres alcances del más estrecho primero: solo lectura, lectura + preparación y acceso completo.

El selector tiene un pequeño límite de confianza propio: un cliente puede sugerir un perfil en la URL de autorización, pero la concesión se acuña a partir de lo que el usuario seleccionó realmente, validado contra la instantánea que se le mostró: una sugerencia falsificada no puede enlazar un perfil que nunca estuvo en pantalla.

Somos igual de explícitos sobre lo que el consentimiento no te da: la autorización es solo de alcance, aprobada una vez por conexión. Los avisos de permiso por llamada del asistente dentro de la app no se aplican al tráfico de MCP: un token de alcance completo filtrado puede escribir (en el área de preparación) sin que un humano vea cada llamada. Vidas útiles cortas de los tokens, revocación por cliente, un registro de auditoría siempre activo que marca cada acción originada por MCP y el solo lectura como oferta por defecto son las mitigaciones. Escribir eso como riesgo residual fue más útil que fingir que el modal de consentimiento lo cerraba.

Barreras de protección para el trabajo sin interfaz

Un agente externo no tiene renderizador, ni ventanas emergentes de confirmación, ni un usuario observando. Todo lo interactivo necesitaba un equivalente sin interfaz:

  • Restricción de licencia en vivo, por llamada. La concesión almacena el alcance que el usuario aprobó; el alcance efectivo se deriva en cada llamada de herramienta a partir del estado de licencia actual de la app: una licencia de solo lectura restringe una concesión completa, un estado de sesión cerrada rechaza con 401 antes de cualquier rama de token (para que el token de desarrollo no pueda saltárselo) y una reactivación a mitad de sesión vuelve a ampliar sin volver a dar el consentimiento. Una asimetría deliberada: la exportación dentro de la app sigue disponible con una licencia de solo lectura (tus datos son tuyos), pero la exportación masiva sin interfaz sobre MCP se restringe: un derecho de portabilidad interactivo y un canal de salida dirigido por un agente son superficies de confianza distintas.
  • Presupuestos de escaneo en lugar de ventanas emergentes de confirmación. En la app, una lectura de tabla completa pregunta primero. Sin interfaz, un presupuesto de elementos/bytes/duración acota cada Scan, y el SQL sobre tablas fluye a través de un motor respaldado en disco en lugar de un tope en memoria.
  • La desconexión aborta honestamente. Si el cliente se cae a mitad de un Scan, el servidor aborta en el siguiente límite de página, nunca a mitad de sentencia, y descarta el estado temporal.
  • La se completa por el cable. Cuando las credenciales de AWS necesitan un código de MFA y el cliente admite elicitation de MCP, el servidor elicita el código de 6 dígitos a través de la propia interfaz del agente —Claude Code pregunta al humano— y reintenta. El SSO se excluye deliberadamente de esta ruta: necesita un flujo de dispositivo del navegador, y fingir lo contrario sería solo un mensaje de error peor. Las llamadas concurrentes que necesitan las mismas credenciales se fusionan en un solo aviso.

¿Es siquiera seguro exponer una base de datos sobre MCP?

Nuestra respuesta honesta: solo si el servidor se diseña en torno a la suposición de que el agente está confundido en el mejor caso y es adversario en el peor. La hace que la intención de un agente no sea de fiar, por lo que la arquitectura nunca depende de que el agente se comporte: las credenciales se quedan en la app, las escrituras solo aterrizan en un área de preparación revisable, los alcances restringen lo que una llamada puede hacer independientemente de lo que el modelo pida, y cada acción se audita. La guía del lado de la conexión cubre lo que eso significa en la práctica para cada cliente.

Qué exigir a cualquier servidor MCP que toque tu base de datos

  • ¿Dónde viven las credenciales: en el proceso del agente o detrás del servidor?
  • ¿Hay consentimiento por conexión con alcances reales, o un solo archivo de configuración lo concede todo?
  • ¿Puede ejecutarse una escritura sin un paso de revisión humana?
  • ¿La identidad (perfil, región) se fija en el consentimiento, o se resuelve a partir de valores por defecto en el momento de la llamada?
  • ¿Qué pasa con un token malformado o revocado: fallo tajante o un recurso alternativo?
  • ¿Hay un registro de auditoría que distinga las acciones originadas por el agente?
  • ¿Qué acota un Scan desbocado, y qué pasa cuando el cliente se desconecta a mitad de operación?

Si un servidor no puede responder a eso, la integración cómoda es la peligrosa. Para dimensionar lo que un Scan sin límite te costaría realmente, la calculadora de precios es aleccionadora, y si prefieres ver la versión segura en marcha en lugar de leer sobre ella, descarga DynoTable, activa el servidor MCP y conecta un cliente con alcance de solo lectura en menos de un minuto.

Trabaja con DynamoDB sin la Consola

DynoTable es un cliente de escritorio rápido para DynamoDB: explora tablas, ejecuta consultas estilo SQL y edita Items localmente.