DCR Proxy
O gerador DCR Proxy cria um proxy OAuth Dynamic Client Registration (DCR) na frente de um Amazon Cognito User Pool.
Clientes MCP (como Claude Code, Kiro CLI ou o MCP Inspector) esperam autenticar contra um servidor de autorização OAuth que suporta Dynamic Client Registration e descoberta de metadados. O Amazon Cognito não suporta DCR nativamente, e seu segredo de App Client nunca deve ser exposto a um cliente público. Este proxy preenche essa lacuna: ele mantém o fluxo do Cognito Hosted UI intacto, implementa DCR, injeta o segredo do App Client no lado do servidor durante a troca de token e encaminha o tráfego MCP para o seu servidor MCP upstream.
Gerar um proxy DCR
Seção intitulada “Gerar um proxy DCR”- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#dcr-proxy - Preencha os parâmetros obrigatórios
- Clique em
Generate
pnpm nx g @aws/nx-plugin:ts#dcr-proxyyarn nx g @aws/nx-plugin:ts#dcr-proxynpx nx g @aws/nx-plugin:ts#dcr-proxybunx nx g @aws/nx-plugin:ts#dcr-proxyVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:ts#dcr-proxy --dry-runyarn nx g @aws/nx-plugin:ts#dcr-proxy --dry-runnpx nx g @aws/nx-plugin:ts#dcr-proxy --dry-runbunx nx g @aws/nx-plugin:ts#dcr-proxy --dry-run| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| name | string | dcr-proxy | O nome do seu proxy DCR, usado para o projeto do handler TypeScript, o nome da classe construct/module e seu diretório em common/constructs ou common/terraform |
| directory | string | packages | O diretório onde armazenar o projeto do handler do proxy DCR. |
| subDirectory | string | - | O subdiretório onde o projeto do handler é colocado. Por padrão, este é o nome do projeto. |
| iac | inherit | cdk | terraform | inherit | O provedor IaC preferido (cdk ou terraform). Por padrão, este é herdado da sua seleção inicial. |
| preferInstallDependencies | boolean | true | Se deve preferir instalar dependências após a execução do gerador. Defina como false para adiar a instalação ao agrupar múltiplos geradores (uma instalação ainda é executada se necessário para que geradores subsequentes possam calcular o grafo de projetos Nx); instale uma vez no final. |
Saída do Gerador
Seção intitulada “Saída do Gerador”O gerador cria um projeto TypeScript autônomo contendo os handlers Lambda e infraestrutura para implantá-los com base no seu iac selecionado.
Directory<dcr-proxy-name>
Directorysrc/
Directoryhandlers/
- authorization-server-metadata.ts Serve
/.well-known/oauth-authorization-servere/.well-known/openid-configuration - protected-resource-metadata.ts Serve
/.well-known/oauth-protected-resource - register.ts RFC 7591 Dynamic Client Registration
- authorize.ts Redireciona para o Cognito Hosted UI
- token.ts Injeta o segredo do App Client e troca o token
- mcp-proxy.ts Faz proxy de requisições
/mcppara o servidor MCP upstream
- authorization-server-metadata.ts Serve
Os handlers são empacotados independentemente com Rolldown, e ambos os provedores IaC referenciam a saída do bundle resultante.
Infraestrutura
Seção intitulada “Infraestrutura”Como este gerador fornece infraestrutura como código com base no iac escolhido, ele criará um projeto em packages/common que inclui os constructs CDK ou módulos Terraform relevantes.
O projeto comum de infraestrutura como código está estruturado da seguinte forma:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructs para infraestrutura específica de um projeto/gerador
- …
Directorycore/ Constructs genéricos reutilizados pelos constructs em
app- …
- index.ts Ponto de entrada exportando os constructs de
app
- project.json Metas de build e configuração do projeto
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Módulos Terraform para infraestrutura específica de um projeto/gerador
- …
Directorycore/ Módulos genéricos reutilizados pelos módulos em
app- …
- project.json Metas de build e configuração do projeto
Para implantar o proxy, os seguintes arquivos são gerados:
Directorypackages/common/constructs/src
Directoryapp
Directorydcr-proxies
Directory<dcr-proxy-name>
- <dcr-proxy-name>.ts Construto CDK que implanta o proxy
Directorypackages/common/terraform/src
Directoryapp
Directorydcr-proxies
Directory<dcr-proxy-name>
- <dcr-proxy-name>.tf Módulo Terraform que implanta o proxy
A infraestrutura provisiona uma API Gateway HTTP API com as seguintes rotas:
| Rota | Descrição |
|---|---|
GET /.well-known/oauth-protected-resource | Metadados de recurso protegido |
GET /.well-known/oauth-authorization-server | Metadados do servidor de autorização |
GET /.well-known/openid-configuration | Configuração OpenID (servida pelo handler de metadados do servidor de autorização) |
POST /register | Dynamic Client Registration |
GET /authorize | Autorização (redireciona para o Cognito Hosted UI) |
POST /oauth/token | Troca de token (injeta o segredo do App Client) |
ANY /mcp | Proxy para o servidor MCP upstream |
Apenas o handler de token recebe acesso de leitura ao segredo do Cognito App Client no Secrets Manager.
Implantando o DCR Proxy
Seção intitulada “Implantando o DCR Proxy”O proxy não cria seus recursos Cognito ou seu servidor MCP. Em vez disso, você injeta os identificadores de recursos gerenciados em outro lugar (seja gerados por este plugin ou provisionados separadamente), mantendo o proxy desacoplado de como esses recursos são provisionados.
Você fornece:
- O id do Cognito User Pool e o id do App Client
- O ARN de um segredo do Secrets Manager contendo o segredo do App Client. O handler de token lê isso em tempo de execução; o valor nunca é exposto ao cliente.
- A URL base do domínio do Cognito Hosted UI
- A URL completa do seu servidor MCP upstream
Instancie o construto gerado em sua stack, passando as propriedades necessárias:
import { DcrProxy } from ':my-scope/common-constructs';
new DcrProxy(this, 'DcrProxy', { userPoolId: userPool.userPoolId, userPoolClientId: userPoolClient.userPoolClientId, cognitoClientSecretArn: clientSecret.secretArn, cognitoHostedUiBase: userPoolDomain.baseUrl(), upstreamUrl: 'https://my-agentcore-runtime-url/mcp',});O construto expõe os endpoints do proxy (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) como propriedades somente leitura.
Referencie o módulo gerado a partir da sua configuração Terraform, passando as variáveis necessárias:
module "dcr_proxy" { source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
user_pool_id = aws_cognito_user_pool.main.id user_pool_client_id = aws_cognito_user_pool_client.main.id cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn cognito_hosted_ui_base = "https://${aws_cognito_user_pool_domain.main.domain}.auth.${data.aws_region.current.region}.amazoncognito.com" upstream_url = "https://my-agentcore-runtime-url/mcp" asset_bucket_name = module.asset_bucket.bucket_name}O módulo expõe os endpoints do proxy (proxy_url, mcp_url, metadata_url, token_endpoint, registration_endpoint) como saídas.
Fronteando um Servidor MCP
Seção intitulada “Fronteando um Servidor MCP”Para frontar um servidor MCP gerado com o gerador ts#mcp-server (ou py#mcp-server) usando --auth cognito, passe o mesmo User Pool e App Client tanto para o servidor MCP quanto para o proxy, e use o invocationUrl do construto do servidor MCP como o upstreamUrl do proxy.
import { DcrProxy, MyProjectMcpServer, UserIdentity,} from ':my-scope/common-constructs';import { OAuthScope } from 'aws-cdk-lib/aws-cognito';import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
const identity = new UserIdentity(this, 'Identity');
// Um App Client confidencial que o proxy usa para a troca de token. Registre as// URLs de callback que seus clientes usam (veja abaixo).const proxyClient = identity.userPool.addClient('DcrProxyClient', { generateSecret: true, oAuth: { flows: { authorizationCodeGrant: true }, scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE], callbackUrls: [ 'http://localhost:41100/callback', // Callback usado pelo Claude Desktop 'https://claude.ai/api/mcp/auth_callback', ], },});
// Armazene o segredo do App Client no Secrets Manager para o handler de token lerconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// O servidor MCP, autorizando JWTs emitidos para o mesmo App Clientconst mcpServer = new MyProjectMcpServer(this, 'MyProjectMcpServer', { identity: { userPool: identity.userPool, userPoolClient: proxyClient, },});
new DcrProxy(this, 'DcrProxy', { userPoolId: identity.userPool.userPoolId, userPoolClientId: proxyClient.userPoolClientId, cognitoClientSecretArn: clientSecret.secretArn, cognitoHostedUiBase: identity.userPoolDomain.baseUrl(), // Use a URL de invocação do construto do servidor MCP em vez de codificá-la upstreamUrl: mcpServer.invocationUrl,});# Um App Client confidencial que o proxy usa para a troca de token. Registre as# URLs de callback que seus clientes usam (veja abaixo).resource "aws_cognito_user_pool_client" "dcr_proxy" { name = "dcr-proxy-client" user_pool_id = module.user_identity.user_pool_id generate_secret = true allowed_oauth_flows = ["code"] allowed_oauth_flows_user_pool_client = true allowed_oauth_scopes = ["openid", "email", "profile"] callback_urls = [ "http://localhost:41100/callback", # Callback usado pelo Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Armazene o segredo do App Client no Secrets Manager para o handler de token lerresource "aws_secretsmanager_secret" "client_secret" { name = "my-dcr-proxy-client-secret"}
resource "aws_secretsmanager_secret_version" "client_secret" { secret_id = aws_secretsmanager_secret.client_secret.id secret_string = aws_cognito_user_pool_client.dcr_proxy.client_secret}
# O servidor MCP, autorizando JWTs emitidos para o mesmo App Clientmodule "my_project_mcp_server" { source = "../../common/terraform/src/app/mcp-servers/my-project-mcp-server"
user_pool_id = module.user_identity.user_pool_id user_pool_client_ids = [aws_cognito_user_pool_client.dcr_proxy.id] appconfig_application_id = module.runtime_config.application_id appconfig_application_arn = module.runtime_config.application_arn}
module "dcr_proxy" { source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
user_pool_id = module.user_identity.user_pool_id user_pool_client_id = aws_cognito_user_pool_client.dcr_proxy.id cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn cognito_hosted_ui_base = "https://${module.user_identity.user_pool_domain}.auth.${data.aws_region.current.region}.amazoncognito.com" # Use a URL de invocação do módulo do servidor MCP em vez de codificá-la upstream_url = module.my_project_mcp_server.invocation_url asset_bucket_name = module.asset_bucket.bucket_name}Fronteando um AgentCore Gateway
Seção intitulada “Fronteando um AgentCore Gateway”Para frontar um AgentCore Gateway gerado com o gerador agentcore-gateway usando --auth cognito, passe o mesmo User Pool e App Client tanto para o gateway quanto para o proxy, e use o gatewayUrl do construto do gateway como o upstreamUrl do proxy.
import { DcrProxy, MyGateway, UserIdentity,} from ':my-scope/common-constructs';import { OAuthScope } from 'aws-cdk-lib/aws-cognito';import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
const identity = new UserIdentity(this, 'Identity');
// Um App Client confidencial que o proxy usa para a troca de token. Registre as// URLs de callback que seus clientes usam (veja abaixo).const proxyClient = identity.userPool.addClient('DcrProxyClient', { generateSecret: true, oAuth: { flows: { authorizationCodeGrant: true }, scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE], callbackUrls: [ 'http://localhost:41100/callback', // Callback usado pelo Claude Desktop 'https://claude.ai/api/mcp/auth_callback', ], },});
// Armazene o segredo do App Client no Secrets Manager para o handler de token lerconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// O gateway, autorizando JWTs emitidos para o mesmo App Clientconst gateway = new MyGateway(this, 'MyGateway', { identity: { userPool: identity.userPool, userPoolClient: proxyClient, },});
new DcrProxy(this, 'DcrProxy', { userPoolId: identity.userPool.userPoolId, userPoolClientId: proxyClient.userPoolClientId, cognitoClientSecretArn: clientSecret.secretArn, cognitoHostedUiBase: identity.userPoolDomain.baseUrl(), // Use a URL do construto do gateway em vez de codificá-la upstreamUrl: gateway.gateway.gatewayUrl,});# Um App Client confidencial que o proxy usa para a troca de token. Registre as# URLs de callback que seus clientes usam (veja abaixo).resource "aws_cognito_user_pool_client" "dcr_proxy" { name = "dcr-proxy-client" user_pool_id = module.user_identity.user_pool_id generate_secret = true allowed_oauth_flows = ["code"] allowed_oauth_flows_user_pool_client = true allowed_oauth_scopes = ["openid", "email", "profile"] callback_urls = [ "http://localhost:41100/callback", # Callback usado pelo Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Armazene o segredo do App Client no Secrets Manager para o handler de token lerresource "aws_secretsmanager_secret" "client_secret" { name = "my-dcr-proxy-client-secret"}
resource "aws_secretsmanager_secret_version" "client_secret" { secret_id = aws_secretsmanager_secret.client_secret.id secret_string = aws_cognito_user_pool_client.dcr_proxy.client_secret}
# O gateway, autorizando JWTs emitidos para o mesmo App Clientmodule "my_gateway" { source = "../../common/terraform/src/app/agentcore-gateway/my-gateway"
user_pool_id = module.user_identity.user_pool_id user_pool_client_ids = [aws_cognito_user_pool_client.dcr_proxy.id]}
module "dcr_proxy" { source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
user_pool_id = module.user_identity.user_pool_id user_pool_client_id = aws_cognito_user_pool_client.dcr_proxy.id cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn cognito_hosted_ui_base = "https://${module.user_identity.user_pool_domain}.auth.${data.aws_region.current.region}.amazoncognito.com" # Use a URL do módulo do gateway em vez de codificá-la upstream_url = module.my_gateway.gateway_url asset_bucket_name = module.asset_bucket.bucket_name}Permitindo URIs de Redirecionamento de Cliente
Seção intitulada “Permitindo URIs de Redirecionamento de Cliente”Como o proxy implementa Dynamic Client Registration virtualmente — não há um Cognito App Client por cliente — todo cliente MCP autentica através do único App Client que você passa para o proxy. Durante o fluxo OAuth, o proxy encaminha o redirect_uri do cliente para o Cognito Hosted UI inalterado, então o Cognito realiza a verificação autoritativa: o callback deve estar registrado como uma URL de callback naquele App Client, caso contrário o Cognito rejeita o login.
Este é um efeito colateral do design DCR virtual. Um cliente pode registrar qualquer redirect_uri com o proxy, mas o login só é bem-sucedido se essa URL exata for uma das URLs de callback do App Client. O Cognito corresponde URLs de callback exatamente, incluindo a porta, então clientes que escutam em uma porta efêmera aleatória não podem ser cobertos por um curinga — você deve fixar cada cliente a uma URL de callback fixa e registrar essa URL exata no App Client.
Adicione as URLs de callback que seus clientes usam quando você criar o App Client:
const userPoolClient = userPool.addClient('DcrProxyClient', { generateSecret: true, oAuth: { flows: { authorizationCodeGrant: true }, callbackUrls: [ // Clientes locais: fixe em uma porta fixa e incomum em vez de uma padrão 'http://localhost:41100/callback', // Callback usado pelo Claude Desktop 'https://claude.ai/api/mcp/auth_callback', ], },});resource "aws_cognito_user_pool_client" "dcr_proxy" { # ... generate_secret = true allowed_oauth_flows = ["code"] allowed_oauth_flows_user_pool_client = true callback_urls = [ # Clientes locais: fixe em uma porta fixa e incomum em vez de uma padrão "http://localhost:41100/callback", # Callback usado pelo Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}Consumindo um Servidor MCP com Proxy
Seção intitulada “Consumindo um Servidor MCP com Proxy”O proxy permite que clientes MCP autentiquem contra seu Cognito User Pool sem qualquer configuração específica do cliente além da URL do proxy. Quando um cliente se conecta ao endpoint /mcp, ele descobre os metadados OAuth (via /.well-known/oauth-protected-resource e /.well-known/oauth-authorization-server), se registra dinamicamente e conduz o usuário através do login do Cognito Hosted UI. O proxy injeta o segredo do App Client durante a troca de token, então o cliente nunca precisa dele.
Para conectar um cliente, aponte-o para o mcpUrl do proxy (ou seja, <proxyUrl>/mcp). Os exemplos abaixo assumem que seu proxy está implantado em https://my-proxy.example.com.
Claude Code
Seção intitulada “Claude Code”Adicione o servidor com proxy com o comando claude mcp add, usando o transporte HTTP. Por padrão, o Claude Code escuta em uma porta de callback aleatória; passe --callback-port para fixá-la na porta registrada no seu App Client (41100 nos exemplos acima). O Claude Code sempre usa o caminho /callback, então o URI de redirecionamento resultante é http://localhost:41100/callback:
claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcpQuando você invoca uma ferramenta do servidor pela primeira vez, o Claude Code abre o Cognito Hosted UI para autenticar antes que a requisição seja enviada via proxy upstream.
Kiro CLI
Seção intitulada “Kiro CLI”Adicione o servidor à sua configuração MCP do Kiro CLI, usando o transporte HTTP. Sem um oauth.redirectUri explícito, o Kiro escolhe uma porta de callback aleatória; defina-a para a URL registrada no seu App Client para que a porta e o caminho correspondam exatamente:
{ "mcpServers": { "my-proxied-server": { "type": "http", "url": "https://my-proxy.example.com/mcp", "oauth": { "redirectUri": "http://localhost:41100/callback" } } }}O Kiro CLI aciona o login do Cognito Hosted UI no primeiro uso e gerencia os tokens resultantes para requisições subsequentes.
Reutilizando um User Pool UserIdentity
Seção intitulada “Reutilizando um User Pool UserIdentity”Se você já tem um User Pool do gerador ts#website#auth (o construto UserIdentity), você pode frontar um servidor MCP para os mesmos usuários que fazem login no seu site. Reutilize seu userPool, mas adicione um App Client separado para o proxy: o cliente do site é um cliente público sem segredo, enquanto o proxy DCR requer um cliente confidencial (generateSecret: true) cujo segredo o handler de token injeta durante a troca de token.
UserIdentity configura o domínio do User Pool com Managed Login (versão 2). Managed Login requer um estilo de branding por App Client, então você deve criar um para o novo cliente do proxy — caso contrário, sua página de login hospedada retorna 403.
import { DcrProxy, MyProjectMcpServer, UserIdentity,} from ':my-scope/common-constructs';import { OAuthScope, CfnManagedLoginBranding } from 'aws-cdk-lib/aws-cognito';import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
// O user pool criado por ts#website#auth para os usuários do seu siteconst identity = new UserIdentity(this, 'Identity');
// Um App Client confidencial no MESMO user pool para o proxy DCRconst proxyClient = identity.userPool.addClient('DcrProxyClient', { generateSecret: true, oAuth: { flows: { authorizationCodeGrant: true }, scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE], callbackUrls: ['http://localhost:41100/callback'], },});
// Managed Login precisa de um estilo de branding para o novo clientenew CfnManagedLoginBranding(this, 'DcrProxyClientBranding', { userPoolId: identity.userPool.userPoolId, clientId: proxyClient.userPoolClientId, useCognitoProvidedValues: true,});
const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
const mcpServer = new MyProjectMcpServer(this, 'MyProjectMcpServer', { identity: { userPool: identity.userPool, userPoolClient: proxyClient, },});
new DcrProxy(this, 'DcrProxy', { userPoolId: identity.userPool.userPoolId, userPoolClientId: proxyClient.userPoolClientId, cognitoClientSecretArn: clientSecret.secretArn, // O construto UserIdentity sempre cria um domínio cognitoHostedUiBase: identity.userPoolDomain.baseUrl(), upstreamUrl: mcpServer.invocationUrl,});# O módulo user pool criado por ts#website#auth para os usuários do seu sitemodule "user_identity" { source = "../../common/terraform/src/core/user-identity"}
# Um App Client confidencial no MESMO user pool para o proxy DCRresource "aws_cognito_user_pool_client" "dcr_proxy" { name = "dcr-proxy-client" user_pool_id = module.user_identity.user_pool_id generate_secret = true allowed_oauth_flows = ["code"] allowed_oauth_flows_user_pool_client = true allowed_oauth_scopes = ["openid", "email", "profile"] callback_urls = ["http://localhost:41100/callback"]}
# Managed Login precisa de um estilo de branding para o novo clienteresource "aws_cognito_managed_login_branding" "dcr_proxy" { user_pool_id = module.user_identity.user_pool_id client_id = aws_cognito_user_pool_client.dcr_proxy.id use_cognito_provided_values = true}
resource "aws_secretsmanager_secret" "client_secret" { name = "my-dcr-proxy-client-secret"}
resource "aws_secretsmanager_secret_version" "client_secret" { secret_id = aws_secretsmanager_secret.client_secret.id secret_string = aws_cognito_user_pool_client.dcr_proxy.client_secret}
module "my_project_mcp_server" { source = "../../common/terraform/src/app/mcp-servers/my-project-mcp-server"
user_pool_id = module.user_identity.user_pool_id user_pool_client_ids = [aws_cognito_user_pool_client.dcr_proxy.id] appconfig_application_id = module.runtime_config.application_id appconfig_application_arn = module.runtime_config.application_arn}
module "dcr_proxy" { source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
user_pool_id = module.user_identity.user_pool_id user_pool_client_id = aws_cognito_user_pool_client.dcr_proxy.id cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn cognito_hosted_ui_base = "https://${module.user_identity.user_pool_domain}.auth.${data.aws_region.current.region}.amazoncognito.com" upstream_url = module.my_project_mcp_server.invocation_url asset_bucket_name = module.asset_bucket.bucket_name}