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

Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy --dry-run
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 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-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 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 /mcp para o servidor MCP upstream

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

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

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

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 manipulador de metadados do servidor de autorização)
POST /registerDynamic Client Registration
GET /authorizeAutorização (redireciona para a UI Hospedada do Cognito)
POST /oauth/tokenTroca de token (injeta o segredo do App Client)
ANY /mcpProxy para o servidor MCP upstream

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

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.

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

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

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.

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:

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

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

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.

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