DCR Proxy
El generador DCR Proxy crea un proxy OAuth de Registro Dinámico de Clientes (DCR) frente a un Amazon Cognito User Pool.
Los clientes MCP (como Claude Code, Kiro CLI o el MCP Inspector) esperan autenticarse contra un servidor de autorización OAuth que soporte Registro Dinámico de Clientes y descubrimiento de metadatos. Amazon Cognito no soporta DCR de forma nativa, y el secreto de su App Client nunca debe exponerse a un cliente público. Este proxy cierra esa brecha: mantiene intacto el flujo de Cognito Hosted UI, implementa DCR, inyecta el secreto del App Client del lado del servidor durante el intercambio de tokens, y reenvía el tráfico MCP a tu servidor MCP upstream.
Generar un proxy DCR
Sección titulada «Generar un 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-proxyTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
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 el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#dcr-proxy - Complete los parámetros requeridos
- Haga clic en
Generate
Opciones
Sección titulada «Opciones»| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
| name | string | dcr-proxy | El nombre de tu proxy DCR, utilizado para su proyecto de handler TypeScript, el nombre de la clase del construct/módulo, y su directorio bajo common/constructs o common/terraform |
| directory | string | packages | El directorio donde almacenar el proyecto de handler del proxy DCR. |
| subDirectory | string | - | El subdirectorio donde se coloca el proyecto de handler. Por defecto, este es el nombre del proyecto. |
| iac | inherit | cdk | terraform | inherit | El proveedor IaC preferido (cdk o terraform). Por defecto, se hereda de tu selección inicial. |
| preferInstallDependencies | boolean | true | Si se prefiere instalar las dependencias después de que se ejecute el generador. Establece en false para diferir la instalación al agrupar múltiples generadores (la instalación aún se ejecuta si es necesaria para que los generadores subsiguientes puedan calcular el grafo de proyectos Nx); instala una vez al final. |
Salida del Generador
Sección titulada «Salida del Generador»El generador crea un proyecto TypeScript independiente que contiene los manejadores Lambda, e infraestructura para desplegarlos según tu iac seleccionado.
Directorio<dcr-proxy-name>
Directoriosrc/
Directoriohandlers/
- authorization-server-metadata.ts Serves
/.well-known/oauth-authorization-serverand/.well-known/openid-configuration - protected-resource-metadata.ts Serves
/.well-known/oauth-protected-resource - register.ts RFC 7591 Dynamic Client Registration
- authorize.ts Redirects to the Cognito Hosted UI
- token.ts Injects the App Client secret and exchanges the token
- mcp-proxy.ts Proxies
/mcprequests to the upstream MCP server
- authorization-server-metadata.ts Serves
Los manejadores se empaquetan de forma independiente con Rolldown, y ambos proveedores de IaC hacen referencia a la salida del paquete resultante.
Infraestructura
Sección titulada «Infraestructura»Dado que este generador proporciona infraestructura como código basada en tu iac elegido, creará un proyecto en packages/common que incluye las construcciones CDK o módulos Terraform relevantes.
El proyecto común de infraestructura como código está estructurado de la siguiente manera:
Directoriopackages/common/constructs
Directoriosrc
Directorioapp/ Constructs for infrastructure specific to a project/generator
- …
Directoriocore/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Directoriopackages/common/terraform
Directoriosrc
Directorioapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directoriocore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Para desplegar el proxy, se generan los siguientes archivos:
Directoriopackages/common/constructs/src
Directorioapp
Directoriodcr-proxies
Directorio<dcr-proxy-name>
- <dcr-proxy-name>.ts CDK construct which deploys the proxy
Directoriopackages/common/terraform/src
Directorioapp
Directoriodcr-proxies
Directorio<dcr-proxy-name>
- <dcr-proxy-name>.tf Terraform module which deploys the proxy
La infraestructura aprovisiona una API Gateway HTTP API con las siguientes rutas:
| Ruta | Descripción |
|---|---|
GET /.well-known/oauth-protected-resource | Metadatos del recurso protegido |
GET /.well-known/oauth-authorization-server | Metadatos del servidor de autorización |
GET /.well-known/openid-configuration | Configuración OpenID (servida por el manejador de metadatos del servidor de autorización) |
POST /register | Registro Dinámico de Clientes |
GET /authorize | Autorización (redirige a Cognito Hosted UI) |
POST /oauth/token | Intercambio de tokens (inyecta el secreto del App Client) |
ANY /mcp | Proxy al servidor MCP upstream |
Solo el manejador de tokens tiene acceso de lectura al secreto del App Client de Cognito en Secrets Manager.
Desplegar el DCR Proxy
Sección titulada «Desplegar el DCR Proxy»El proxy no crea tus recursos de Cognito ni tu servidor MCP. En su lugar, inyectas los identificadores de recursos gestionados en otro lugar (ya sea generados por este plugin o aprovisionados por separado), manteniendo el proxy desacoplado de cómo se aprovisionan esos recursos.
Proporcionas:
- El id del Cognito User Pool y el id del App Client
- El ARN de un secreto de Secrets Manager que contiene el secreto del App Client. El manejador de tokens lee esto en tiempo de ejecución; el valor nunca se expone al cliente.
- La URL base del dominio de Cognito Hosted UI
- La URL completa de tu servidor MCP upstream
Instancia el constructo generado en tu stack, pasando las propiedades requeridas:
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',});El constructo expone los endpoints del proxy (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) como propiedades de solo lectura.
Referencia el módulo generado desde tu configuración de Terraform, pasando las variables requeridas:
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}El módulo expone los endpoints del proxy (proxy_url, mcp_url, metadata_url, token_endpoint, registration_endpoint) como salidas.
Poner un Servidor MCP al Frente
Sección titulada «Poner un Servidor MCP al Frente»Para poner un servidor MCP generado con el generador ts#mcp-server (o py#mcp-server) usando --auth cognito al frente, pasa el mismo User Pool y App Client tanto al servidor MCP como al proxy, y usa el invocationUrl del constructo del servidor MCP como el upstreamUrl del 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}Poner un AgentCore Gateway al Frente
Sección titulada «Poner un AgentCore Gateway al Frente»Para poner un AgentCore Gateway generado con el generador agentcore-gateway usando --auth cognito al frente, pasa el mismo User Pool y App Client tanto al gateway como al proxy, y usa el gatewayUrl del constructo del gateway como el upstreamUrl del 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}Permitir URIs de Redirección del Cliente
Sección titulada «Permitir URIs de Redirección del Cliente»Debido a que el proxy implementa el Registro Dinámico de Clientes de forma virtual — no hay un App Client de Cognito por cliente — cada cliente MCP se autentica a través del único App Client que pasas al proxy. Durante el flujo OAuth, el proxy reenvía el redirect_uri del cliente a Cognito Hosted UI sin cambios, por lo que Cognito realiza la verificación autoritativa: la devolución de llamada debe estar registrada como una URL de devolución de llamada en ese App Client, de lo contrario Cognito rechaza el inicio de sesión.
Este es un efecto secundario del diseño DCR virtual. Un cliente puede registrar cualquier redirect_uri con el proxy, pero el inicio de sesión solo tiene éxito si esa URL exacta es una de las URLs de devolución de llamada del App Client. Cognito coincide las URLs de devolución de llamada exactamente, incluido el puerto, por lo que los clientes que escuchan en un puerto efímero aleatorio no pueden cubrirse con un comodín — debes fijar cada cliente a una URL de devolución de llamada fija y registrar esa URL exacta en el App Client.
Agrega las URLs de devolución de llamada que usan tus clientes cuando creas el 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", ]}Consumir un Servidor MCP con Proxy
Sección titulada «Consumir un Servidor MCP con Proxy»El proxy permite que los clientes MCP se autentiquen contra tu Cognito User Pool sin ninguna configuración específica del cliente más allá de la URL del proxy. Cuando un cliente se conecta al endpoint /mcp, descubre los metadatos OAuth (a través de /.well-known/oauth-protected-resource y /.well-known/oauth-authorization-server), se registra dinámicamente y guía al usuario a través del inicio de sesión de Cognito Hosted UI. El proxy inyecta el secreto del App Client durante el intercambio de tokens, por lo que el cliente nunca lo necesita.
Para conectar un cliente, apúntalo al mcpUrl del proxy (es decir, <proxyUrl>/mcp). Los ejemplos a continuación asumen que tu proxy está desplegado en https://my-proxy.example.com.
Claude Code
Sección titulada «Claude Code»Agrega el servidor con proxy con el comando claude mcp add, usando el transporte HTTP. Por defecto, Claude Code escucha en un puerto de devolución de llamada aleatorio; pasa --callback-port para fijarlo al puerto registrado en tu App Client (41100 en los ejemplos anteriores). Claude Code siempre usa la ruta /callback, por lo que la URI de redirección resultante es http://localhost:41100/callback:
claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcpCuando invocas por primera vez una herramienta del servidor, Claude Code abre Cognito Hosted UI para autenticarse antes de que la solicitud se envíe al upstream.
Kiro CLI
Sección titulada «Kiro CLI»Agrega el servidor a tu configuración MCP de Kiro CLI, usando el transporte HTTP. Sin un oauth.redirectUri explícito, Kiro elige un puerto de devolución de llamada aleatorio; configúralo a la URL registrada en tu App Client para que el puerto y la ruta coincidan exactamente:
{ "mcpServers": { "my-proxied-server": { "type": "http", "url": "https://my-proxy.example.com/mcp", "oauth": { "redirectUri": "http://localhost:41100/callback" } } }}Kiro CLI activa el inicio de sesión de Cognito Hosted UI en el primer uso y gestiona los tokens resultantes para solicitudes posteriores.
Reutilizar un User Pool de UserIdentity
Sección titulada «Reutilizar un User Pool de UserIdentity»Si ya tienes un User Pool del generador ts#website#auth (el constructo UserIdentity), puedes poner un servidor MCP frente a los mismos usuarios que inician sesión en tu sitio web. Reutiliza su userPool, pero agrega un App Client separado para el proxy: el cliente del sitio web es un cliente público sin secreto, mientras que el proxy DCR requiere un cliente confidencial (generateSecret: true) cuyo secreto el manejador de tokens inyecta durante el intercambio de tokens.
UserIdentity configura el dominio del User Pool con Managed Login (versión 2). Managed Login requiere un estilo de marca por App Client, por lo que debes crear uno para el nuevo cliente del proxy — de lo contrario, su página de inicio de sesión alojada devuelve 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}