Ir al contenido

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.

Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy --dry-run
ParámetroTipoPredeterminadoDescripción
name stringdcr-proxyEl 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 stringpackagesEl 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 | terraforminheritEl proveedor IaC preferido (cdk o terraform). Por defecto, se hereda de tu selección inicial.
preferInstallDependencies booleantrueSi 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.

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-server and /.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 /mcp requests to the upstream MCP server

Los manejadores se empaquetan de forma independiente con Rolldown, y ambos proveedores de IaC hacen referencia a la salida del paquete resultante.

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

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

La infraestructura aprovisiona una API Gateway HTTP API con las siguientes rutas:

RutaDescripción
GET /.well-known/oauth-protected-resourceMetadatos del recurso protegido
GET /.well-known/oauth-authorization-serverMetadatos del servidor de autorización
GET /.well-known/openid-configurationConfiguración OpenID (servida por el manejador de metadatos del servidor de autorización)
POST /registerRegistro Dinámico de Clientes
GET /authorizeAutorización (redirige a Cognito Hosted UI)
POST /oauth/tokenIntercambio de tokens (inyecta el secreto del App Client)
ANY /mcpProxy al servidor MCP upstream

Solo el manejador de tokens tiene acceso de lectura al secreto del App Client de Cognito en Secrets Manager.

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.

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 read
const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
secretStringValue: proxyClient.userPoolClientSecret,
});
// The MCP server, authorizing JWTs issued for the same App Client
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,
cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
// Use the MCP server construct's invocation URL rather than hardcoding it
upstreamUrl: mcpServer.invocationUrl,
});

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 read
const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
secretStringValue: proxyClient.userPoolClientSecret,
});
// The gateway, authorizing JWTs issued for the same App Client
const 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,
});

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',
],
},
});

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.

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:

Ventana de terminal
claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcp

Cuando 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.

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.

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 users
const identity = new UserIdentity(this, 'Identity');
// A confidential App Client on the SAME user pool for the DCR proxy
const 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 client
new 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,
});