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 suporte 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 da UI Hospedada do Cognito 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 seu servidor MCP upstream.
Gerar um proxy DCR
Seção intitulada “Gerar um proxy DCR”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- 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
| 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 manipuladores 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 a UI Hospedada do Cognito
- 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 manipuladores são empacotados independentemente com Rolldown, e ambos os provedores de IaC referenciam a saída do pacote resultante.
Infraestrutura
Seção intitulada “Infraestrutura”Como este gerador fornece infraestrutura como código baseada no seu iac escolhido, ele criará um projeto em packages/common que inclui os constructs CDK relevantes ou módulos Terraform.
O projeto comum de infraestrutura como código é estruturado da seguinte forma:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructs for infrastructure specific to a project/generator
- …
Directorycore/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directorycore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
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 CDK construct which deploys the proxy
Directorypackages/common/terraform/src
Directoryapp
Directorydcr-proxies
Directory<dcr-proxy-name>
- <dcr-proxy-name>.tf Terraform module which deploys the 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 manipulador de metadados do servidor de autorização) |
POST /register | Dynamic Client Registration |
GET /authorize | Autorização (redireciona para a UI Hospedada do Cognito) |
POST /oauth/token | Troca de token (injeta o segredo do App Client) |
ANY /mcp | Proxy para o servidor MCP upstream |
Apenas o manipulador de token recebe acesso de leitura ao segredo do App Client do Cognito no Secrets Manager.
Implantando o DCR Proxy
Seção intitulada “Implantando o DCR Proxy”O proxy não cria seus recursos do 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 manipulador de token lê isso em tempo de execução; o valor nunca é exposto ao cliente.
- A URL base do domínio da UI Hospedada do Cognito
- A URL completa do seu servidor MCP upstream
Instancie o construct 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 construct expõe os endpoints do proxy (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) como propriedades somente leitura.
Referencie o módulo gerado a partir de 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.
Colocando um Servidor MCP na Frente
Seção intitulada “Colocando um Servidor MCP na Frente”Para colocar um servidor MCP gerado com o gerador ts#mcp-server (ou py#mcp-server) usando --auth cognito na frente, passe o mesmo User Pool e App Client tanto para o servidor MCP quanto para o proxy, e use o invocationUrl do construct 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');
// A confidential App Client the proxy uses for the token exchange. Register the// callback URLs your clients use (see below).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 used by Claude Desktop 'https://claude.ai/api/mcp/auth_callback', ], },});
// Store the App Client secret in Secrets Manager for the token handler to readconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// The MCP server, authorizing JWTs issued for the same 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 the MCP server construct's invocation URL rather than hardcoding it upstreamUrl: mcpServer.invocationUrl,});# A confidential App Client the proxy uses for the token exchange. Register the# callback URLs your clients use (see below).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 used by Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Store the App Client secret in Secrets Manager for the token handler to readresource "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}
# The MCP server, authorizing JWTs issued for the same 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 the MCP server module's invocation URL rather than hardcoding it upstream_url = module.my_project_mcp_server.invocation_url asset_bucket_name = module.asset_bucket.bucket_name}Colocando um AgentCore Gateway na Frente
Seção intitulada “Colocando um AgentCore Gateway na Frente”Para colocar um AgentCore Gateway gerado com o gerador agentcore-gateway usando --auth cognito na frente, passe o mesmo User Pool e App Client tanto para o gateway quanto para o proxy, e use o gatewayUrl do construct 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');
// A confidential App Client the proxy uses for the token exchange. Register the// callback URLs your clients use (see below).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 used by Claude Desktop 'https://claude.ai/api/mcp/auth_callback', ], },});
// Store the App Client secret in Secrets Manager for the token handler to readconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// The gateway, authorizing JWTs issued for the same 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 the gateway construct's URL rather than hardcoding it upstreamUrl: gateway.gateway.gatewayUrl,});# A confidential App Client the proxy uses for the token exchange. Register the# callback URLs your clients use (see below).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 used by Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Store the App Client secret in Secrets Manager for the token handler to readresource "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}
# The gateway, authorizing JWTs issued for the same 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 the gateway module's URL rather than hardcoding it upstream_url = module.my_gateway.gateway_url asset_bucket_name = module.asset_bucket.bucket_name}Permitindo URIs de Redirecionamento do Cliente
Seção intitulada “Permitindo URIs de Redirecionamento do Cliente”Como o proxy implementa Dynamic Client Registration virtualmente — não há um App Client do Cognito por cliente — cada 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 a UI Hospedada do Cognito sem alterações, então o Cognito realiza a verificação autoritativa: o callback deve ser registrado como uma URL de callback nesse 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: [ // Local clients: pin to a fixed, uncommon port rather than a default one 'http://localhost:41100/callback', // Callback used by 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 = [ # Local clients: pin to a fixed, uncommon port rather than a default one "http://localhost:41100/callback", # Callback used by 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 nenhuma 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 da UI Hospedada do Cognito. 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, Claude Code escuta em uma porta de callback aleatória; passe --callback-port para fixá-lo à porta registrada no seu App Client (41100 nos exemplos acima). 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 pela primeira vez uma ferramenta do servidor, Claude Code abre a UI Hospedada do Cognito para autenticar antes que a requisição seja enviada por proxy para 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, 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" } } }}Kiro CLI aciona o login da UI Hospedada do Cognito 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 construct UserIdentity), você pode colocar um servidor MCP na frente 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 manipulador 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 marca 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';
// The user pool created by ts#website#auth for your website usersconst identity = new UserIdentity(this, 'Identity');
// A confidential App Client on the SAME user pool for the DCR proxyconst 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 needs a branding style for the new clientnew 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, // The UserIdentity construct always creates a domain cognitoHostedUiBase: identity.userPoolDomain.baseUrl(), upstreamUrl: mcpServer.invocationUrl,});# The user pool module created by ts#website#auth for your website usersmodule "user_identity" { source = "../../common/terraform/src/core/user-identity"}
# A confidential App Client on the SAME user pool for the DCR proxyresource "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 needs a branding style for the new clientresource "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}