Pular para o conteúdo

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.

  1. Instale o Nx Console VSCode Plugin se ainda não o fez
  2. Abra o console Nx no VSCode
  3. Clique em Generate (UI) na seção "Common Nx Commands"
  4. Procure por @aws/nx-plugin - ts#dcr-proxy
  5. Preencha os parâmetros obrigatórios
    • Clique em Generate
    ParâmetroTipoPadrãoDescrição
    name stringdcr-proxyO 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 stringpackagesO 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 | terraforminheritO provedor IaC preferido (cdk ou terraform). Por padrão, este é herdado da sua seleção inicial.
    preferInstallDependencies booleantrueSe 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.

    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-server e /.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 /mcp para o servidor MCP upstream

    Os handlers são empacotados independentemente com Rolldown, e ambos os provedores IaC referenciam a saída do bundle resultante.

    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

    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

    A infraestrutura provisiona uma API Gateway HTTP API com as seguintes rotas:

    RotaDescrição
    GET /.well-known/oauth-protected-resourceMetadados de recurso protegido
    GET /.well-known/oauth-authorization-serverMetadados do servidor de autorização
    GET /.well-known/openid-configurationConfiguração OpenID (servida pelo handler de metadados do servidor de autorização)
    POST /registerDynamic Client Registration
    GET /authorizeAutorização (redireciona para o Cognito Hosted UI)
    POST /oauth/tokenTroca de token (injeta o segredo do App Client)
    ANY /mcpProxy para o servidor MCP upstream

    Apenas o handler de token recebe acesso de leitura ao segredo do Cognito App Client no Secrets Manager.

    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.

    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 ler
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // O servidor MCP, autorizando JWTs emitidos para o mesmo 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 a URL de invocação do construto do servidor MCP em vez de codificá-la
    upstreamUrl: mcpServer.invocationUrl,
    });

    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 ler
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // O gateway, autorizando JWTs emitidos para o mesmo 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 a URL do construto do gateway em vez de codificá-la
    upstreamUrl: gateway.gateway.gatewayUrl,
    });

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

    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.

    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:

    Terminal window
    claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcp

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

    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.

    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 site
    const identity = new UserIdentity(this, 'Identity');
    // Um App Client confidencial no MESMO user pool para o 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 precisa de um estilo de branding para o novo 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,
    // O construto UserIdentity sempre cria um domínio
    cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
    upstreamUrl: mcpServer.invocationUrl,
    });