Ir al contenido

DCR Proxy

El generador DCR Proxy crea un proxy OAuth Dynamic Client Registration (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 Dynamic Client Registration y descubrimiento de metadatos. Amazon Cognito no soporta DCR de forma nativa, y su secreto de 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 de App Client del lado del servidor durante el intercambio de tokens y reenvía el tráfico MCP a tu servidor MCP upstream.

  1. Instale el Nx Console VSCode Plugin si aún no lo ha hecho
  2. Abra la consola Nx en VSCode
  3. Haga clic en Generate (UI) en la sección "Common Nx Commands"
  4. Busque @aws/nx-plugin - ts#dcr-proxy
  5. Complete los parámetros requeridos
    • Haga clic en Generate
    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 basándose en tu iac seleccionado.

    • Directorio<dcr-proxy-name>
      • Directoriosrc/
        • Directoriohandlers/
          • authorization-server-metadata.ts Sirve /.well-known/oauth-authorization-server y /.well-known/openid-configuration
          • protected-resource-metadata.ts Sirve /.well-known/oauth-protected-resource
          • register.ts RFC 7591 Dynamic Client Registration
          • authorize.ts Redirige a la Cognito Hosted UI
          • token.ts Inyecta el secreto de App Client e intercambia el token
          • mcp-proxy.ts Redirige las solicitudes /mcp al servidor MCP upstream

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

    Dado que este generador proporciona infraestructura como código basada en tu iac seleccionado, creará un proyecto en packages/common que incluye los constructos CDK o módulos de Terraform correspondientes.

    El proyecto común de infraestructura como código tiene la siguiente estructura:

    • Directoriopackages/common/constructs
      • Directoriosrc
        • Directorioapp/ Constructos para infraestructura específica de un proyecto/generador
        • Directoriocore/ Constructos genéricos reutilizados por los constructos en app
        • index.ts Punto de entrada que exporta los constructos de app
      • project.json Objetivos de compilación y configuración del proyecto

    Para desplegar el proxy, se generan los siguientes archivos:

    • Directoriopackages/common/constructs/src
      • Directorioapp
        • Directoriodcr-proxies
          • Directorio<dcr-proxy-name>
            • <dcr-proxy-name>.ts Construcción CDK que despliega el proxy

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

    RutaDescripción
    GET /.well-known/oauth-protected-resourceMetadatos de 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 /registerDynamic Client Registration
    GET /authorizeAutorización (redirige a la Cognito Hosted UI)
    POST /oauth/tokenIntercambio de tokens (inyecta el secreto de App Client)
    ANY /mcpProxy al servidor MCP upstream

    Solo el manejador de tokens tiene acceso de lectura al secreto de Cognito App Client 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 sean 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 lo lee 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 la construcción generada 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',
    });

    La construcción 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 de la construcción 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');
    // Un App Client confidencial que el proxy usa para el intercambio de tokens. Registra las
    // URLs de callback que usan tus clientes (ver abajo).
    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 por Claude Desktop
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });
    // Almacena el secreto de App Client en Secrets Manager para que el manejador de tokens lo lea
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // El servidor MCP, autorizando JWTs emitidos para el mismo 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(),
    // Usa la URL de invocación de la construcción del servidor MCP en lugar de codificarla
    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 de la construcción 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');
    // Un App Client confidencial que el proxy usa para el intercambio de tokens. Registra las
    // URLs de callback que usan tus clientes (ver abajo).
    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 por Claude Desktop
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });
    // Almacena el secreto de App Client en Secrets Manager para que el manejador de tokens lo lea
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // El gateway, autorizando JWTs emitidos para el mismo 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(),
    // Usa la URL de la construcción del gateway en lugar de codificarla
    upstreamUrl: gateway.gateway.gatewayUrl,
    });

    Debido a que el proxy implementa Dynamic Client Registration de forma virtual — no hay un Cognito App Client 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 la Cognito Hosted UI sin cambios, por lo que Cognito realiza la verificación autoritativa: el callback debe estar registrado como una URL de callback 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 callback del App Client. Cognito coincide las URLs de callback 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 callback fija y registrar esa URL exacta en el App Client.

    Agrega las URLs de callback que usan tus clientes cuando creas el App Client:

    const userPoolClient = userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    callbackUrls: [
    // Clientes locales: fija a un puerto fijo y poco común en lugar de uno predeterminado
    'http://localhost:41100/callback',
    // Callback usado por Claude Desktop
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });

    El proxy permite a los clientes MCP autenticarse 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 de 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 callback 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 el 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 la Cognito Hosted UI para autenticarse antes de que la solicitud se redirija 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 callback 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 (la construcción 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';
    // El user pool creado por ts#website#auth para los usuarios de tu sitio web
    const identity = new UserIdentity(this, 'Identity');
    // Un App Client confidencial en el MISMO user pool para el proxy DCR
    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 necesita un estilo de marca para el nuevo cliente
    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,
    // La construcción UserIdentity siempre crea un dominio
    cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
    upstreamUrl: mcpServer.invocationUrl,
    });