O que foi preciso para construir um servidor MCP seguro para o DynamoDB
O torna trivial entregar seu banco de dados a um . Esse é o problema. A maioria dos servidores MCP para DynamoDB é um processo fino segurando suas credenciais AWS brutas com um caminho de escrita e sem etapa de revisão — já escrevemos sobre por que esse padrão é arriscado e como escolher com segurança do lado do usuário.
Este post é o outro lado: o que de fato foi preciso para construir a versão segura. O DynoTable é um cliente desktop para DynamoDB cujo servidor MCP expõe seu conjunto de ferramentas de IA restrito a agentes externos — Claude Code, Cursor, Codex — sem que o agente jamais toque nas credenciais AWS. Chegar lá significou lançar um servidor de autorização dentro de um app Electron, projetar um tipo de identidade que torna misturas de credenciais irrepresentáveis e decidir, mais de uma vez, que o padrão conveniente era o errado.
Se você está construindo um servidor MCP em torno de qualquer coisa sensível, estas são as decisões que você também vai encontrar.
HTTP em loopback não é automaticamente seguro
O servidor faz bind em 127.0.0.1, o que soa seguro e não é suficiente. Qualquer
página web que você visita pode tentar requisições em direção ao localhost, e o DNS
rebinding permite que uma página hostil as disfarce com um Host plausível. Então
a primeira camada é um origin guard: rejeitar qualquer requisição que carregue um Origin
real de página web, rejeitar cabeçalhos Host que não sejam de loopback — mas permitir
requisições sem nenhum Origin, porque clientes de CLI legítimos como o
Claude Code não enviam um.
Essa última cláusula é a parte honesta: um origin guard só bloqueia a classe de ataque via navegador. Ele não consegue autenticar ninguém. O bearer token é o verdadeiro portão, o que levanta a questão de onde os tokens vêm.
Lançamos um servidor de autorização OAuth 2.1 dentro do app
A história de autenticação de servidor remoto do MCP é OAuth 2.1, e para um servidor desktop local não há provedor de identidade upstream em que se apoiar. Então o app é o servidor de autorização: registro dinâmico de clientes, (S256), códigos de autorização, refresh tokens, revogação, e os documentos de metadados RFC 8414 / RFC 9728 que permitem aos clientes descobrir tudo isso.
Regras que definimos cedo e mantivemos:
- Não implemente o protocolo à mão. O SDK do MCP entrega toda a superfície do AS como um router; nós implementamos apenas o provedor de armazenamento/política por baixo dele. Uma consequência que aceitamos: nosso framework MCP fala uma stack HTTP internamente e o router do SDK fala outra, então o AS roda como um segundo listener de loopback, com o resource server apontando os clientes para ele via metadados de protected-resource. Dois listeners é um custo delimitado e explícito; rederivar PKCE e a gramática de tokens à mão é um custo ilimitado.
- Redirects são apenas de loopback (conforme a RFC 8252 para apps nativos), e um redirect inválido nunca recebe um redirect de erro — ele recebe um 400 direto, então a URI de um atacante nunca é um destino.
- Códigos de autorização são de uso único — um código repetido é uma concessão inválida, ponto final.
- O refresh pode estreitar o escopo, mas nunca ampliá-lo, e reemite o mesmo payload de identidade verbatim — um token renovado não pode adquirir silenciosamente uma região diferente.
- A revogação é reverificada a cada requisição, não em cache no momento da conexão — clicar em "Revogar" nas Configurações mata a próxima chamada do cliente.
Builds de desenvolvimento usam, em vez disso, um caminho de autenticação mais simples, construído para não poder existir em um binário de release — um token de escopo completo não autenticado em software lançado seria um backdoor local por trás de um origin guard que deliberadamente admite requisições sem Origin.
Uma conexão, uma identidade AWS — carregada inteira, nunca adivinhada
A propriedade central de segurança: um agente externo se conecta a um perfil, não ao "DynoTable". Cada conexão é vinculada a um perfil AWS e uma região, respaldada por seu próprio cliente DynamoDB não compartilhado, de modo que dois agentes em perfis diferentes não podem vazar um para o outro.
O binding começou a vida como campos soltos — awsProfile, region,
profileId, uma porta local opcional — redeclarados em cada registro do
caminho. Agora é um único tipo de identidade em união discriminada com dois
braços: online (perfil + região + id) e offline (porta local + id,
para perfis do DynamoDB Local). O fato de ser offline é
o próprio discriminante, então uma identidade online sem credenciais ou uma
offline carregando uma região é um erro de tipo, não uma surpresa em tempo de execução.
Esse único tipo viaja intacto por consentimento → token → concessão → cada
requisição.
Duas consequências que consideramos estruturais:
- A região é congelada no consentimento. O usuário aprova "perfil X na
região Y", e essa tupla é fixada no token — sobrevivendo ao refresh,
nunca reresolvida a partir de estado mutável no caminho crítico. Editar a
região do perfil exige novo consentimento. A alternativa — resolver a
região por requisição ou recorrer a um padrão — é como um agente acaba
silenciosamente consultando
us-east-1. - Tokens malformados são um 401, nunca uma coerção. Se os claims de identidade
de um token não fazem parse em um braço válido, a requisição é não autorizada. Nenhum
perfil
'default', nenhuma região padrão, nenhuma string vazia. Todo lugar em que o código antigo teria adivinhado agora é uma falha rígida.
O consentimento é uma superfície de produto, não uma caixa de diálogo
O primeiro prompt de consentimento era a message box nativa da plataforma. Ela congelava o processo main do Electron enquanto aberta, não conseguia combinar com o design system do app e nem sequer podia ser capturada em screenshot para a documentação. Agora é um modal dentro do app: o nome do cliente que está se conectando, um seletor de perfil sobre exatamente os perfis que o usuário escolheu expor, e três escopos oferecidos do mais restrito ao mais amplo — somente leitura, leitura + stage, completo. Pedidos de consentimento expirados ou duplicados se resolvem como negados em vez de travar o fluxo OAuth.

O seletor tem uma pequena fronteira de confiança própria: um cliente pode sugerir um perfil na URL de autorização, mas a concessão é cunhada a partir do que o usuário de fato selecionou, validada contra o snapshot que lhe foi mostrado — uma sugestão forjada não pode vincular um perfil que nunca esteve na tela.
Somos igualmente explícitos sobre o que o consentimento não dá a você: a autorização é somente de escopo, aprovada uma vez por conexão. Os prompts de permissão por chamada do assistente dentro do app não se aplicam ao tráfego MCP — um token de escopo completo vazado pode escrever (no staging) sem que um humano veja cada chamada. Tempos de vida curtos para tokens, revogação por cliente, uma trilha de auditoria sempre ativa marcando cada ação originada no MCP, e somente leitura como a oferta padrão são as mitigações. Escrever isso como um risco residual foi mais útil do que fingir que o modal de consentimento o fechava.
Guardrails para trabalho headless
Um agente externo não tem renderer, nem popups de confirmação, nem usuário observando. Tudo que era interativo precisou de um equivalente headless:
- Clamping de licença ao vivo, por chamada. A concessão armazena o escopo que o usuário aprovou; o escopo efetivo é derivado a cada chamada de ferramenta a partir do estado atual de licença do app — uma licença somente leitura reduz uma concessão completa, um estado desconectado rejeita com 401 antes de qualquer ramo de token (para que o token de dev não possa contorná-lo), e uma reativação no meio da sessão amplia novamente sem novo consentimento. Uma assimetria deliberada: a exportação dentro do app permanece disponível sob uma licença somente leitura (seus dados são seus dados), mas a exportação em massa headless via MCP é reduzida — um direito interativo de portabilidade e um canal de saída conduzido por agente são superfícies de confiança diferentes.
- Orçamentos de scan em vez de popups de confirmação. No app, uma leitura de tabela inteira pergunta primeiro. Headless, um orçamento de itens/bytes/duração limita cada scan, e o SQL sobre tabelas transmite por um mecanismo respaldado em disco em vez de um limite em memória.
- A desconexão aborta de forma honesta. Se o cliente cai no meio de um scan, o servidor aborta na próxima fronteira de página — nunca no meio de uma instrução — e descarta o estado temporário.
- O é concluído pela conexão. Quando as credenciais AWS precisam de um código MFA e o cliente suporta elicitation do MCP, o servidor solicita o código de 6 dígitos pela própria UI do agente — o Claude Code pede ao humano — e faz o retry. O SSO é deliberadamente excluído desse caminho: ele precisa de um device flow no navegador, e fingir o contrário seria apenas uma mensagem de erro pior. Chamadas concorrentes que precisam das mesmas credenciais se agrupam em um único prompt.
Expor um banco de dados via MCP é seguro afinal?
Nossa resposta honesta: só se o servidor for projetado em torno da suposição de que o agente está, na melhor das hipóteses, confuso, e, na pior, é adversário. A torna a intenção de um agente não confiável, e é por isso que a arquitetura nunca depende de o agente se comportar: as credenciais ficam no app, as escritas só chegam a uma área de staging revisável, os escopos limitam o que uma chamada pode fazer independentemente do que o modelo pede, e cada ação é auditada. O guia do lado da conexão cobre o que isso significa na prática para cada cliente.
O que exigir de qualquer servidor MCP que toca seu banco de dados
- Onde ficam as credenciais — no processo do agente, ou por trás do servidor?
- Existe consentimento por conexão com escopos reais, ou um único arquivo de configuração concede tudo?
- Uma escrita pode ser executada sem uma etapa de revisão humana?
- A identidade (perfil, região) é fixada no consentimento, ou resolvida a partir de padrões no momento da chamada?
- O que acontece com um token malformado ou revogado — falha rígida, ou um fallback?
- Existe uma trilha de auditoria que distingue ações originadas por agentes?
- O que limita um scan descontrolado, e o que acontece quando o cliente desconecta no meio de uma operação?
Se um servidor não consegue responder a isso, a integração conveniente é a arriscada. Para dimensionar quanto um scan sem limites custaria de fato a você, a calculadora de preços é reveladora — e se você prefere ver a versão segura rodando a ler sobre ela, baixe o DynoTable, ative o servidor MCP e conecte um cliente com escopo somente leitura em menos de um minuto.


