DCR Proxy
Le générateur DCR Proxy crée un proxy OAuth Dynamic Client Registration (DCR) devant un Amazon Cognito User Pool.
Les clients MCP (tels que Claude Code, Kiro CLI ou le MCP Inspector) s’attendent à s’authentifier auprès d’un serveur d’autorisation OAuth qui prend en charge Dynamic Client Registration et la découverte de métadonnées. Amazon Cognito ne prend pas en charge DCR nativement, et son secret App Client ne doit jamais être exposé à un client public. Ce proxy comble cette lacune : il maintient intact le flux Cognito Hosted UI, implémente DCR, injecte le secret App Client côté serveur lors de l’échange de jeton, et transmet le trafic MCP à votre serveur MCP en amont.
Utilisation
Section intitulée « Utilisation »Générer un proxy DCR
Section intitulée « Générer 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-proxyVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#dcr-proxy - Remplissez les paramètres requis
- Cliquez sur
Generate
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
| name | string | dcr-proxy | Le nom de votre proxy DCR, utilisé pour son projet de gestionnaire TypeScript, le nom de la classe construct/module, et son répertoire sous common/constructs ou common/terraform |
| directory | string | packages | Le répertoire dans lequel stocker le projet de gestionnaire du proxy DCR. |
| subDirectory | string | - | Le sous-répertoire dans lequel le projet de gestionnaire est placé. Par défaut, il s'agit du nom du projet. |
| iac | inherit | cdk | terraform | inherit | Le fournisseur IaC préféré (cdk ou terraform). Par défaut, cela est hérité de votre sélection initiale. |
| preferInstallDependencies | boolean | true | Indique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir sur false pour différer l'installation lors du traitement par lots de plusieurs générateurs (une installation s'exécute toujours si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une fois à la fin. |
Sortie du générateur
Section intitulée « Sortie du générateur »Le générateur crée un projet TypeScript autonome contenant les gestionnaires Lambda, et l’infrastructure pour les déployer en fonction de votre iac sélectionné.
Répertoire<dcr-proxy-name>
Répertoiresrc/
Répertoirehandlers/
- 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
Les gestionnaires sont regroupés indépendamment avec Rolldown, et les deux fournisseurs IaC référencent la sortie du bundle résultant.
Infrastructure
Section intitulée « Infrastructure »Étant donné que ce générateur fournit de l’infrastructure en tant que code basée sur votre iac choisi, il créera un projet dans packages/common qui inclut les constructs CDK ou modules Terraform pertinents.
Le projet d’infrastructure en tant que code commun est structuré comme suit :
Répertoirepackages/common/constructs
Répertoiresrc
Répertoireapp/ Constructs pour l’infrastructure spécifique à un projet/générateur
- …
Répertoirecore/ Constructs génériques qui sont réutilisés par les constructs dans
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Répertoirepackages/common/terraform
Répertoiresrc
Répertoireapp/ Terraform modules for infrastructure specific to a project/generator
- …
Répertoirecore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Pour déployer le proxy, les fichiers suivants sont générés :
Répertoirepackages/common/constructs/src
Répertoireapp
Répertoiredcr-proxies
Répertoire<dcr-proxy-name>
- <dcr-proxy-name>.ts CDK construct which deploys the proxy
Répertoirepackages/common/terraform/src
Répertoireapp
Répertoiredcr-proxies
Répertoire<dcr-proxy-name>
- <dcr-proxy-name>.tf Terraform module which deploys the proxy
L’infrastructure provisionne une API Gateway HTTP API avec les routes suivantes :
| Route | Description |
|---|---|
GET /.well-known/oauth-protected-resource | Protected resource metadata |
GET /.well-known/oauth-authorization-server | Authorization server metadata |
GET /.well-known/openid-configuration | OpenID configuration (served by the authorization server metadata handler) |
POST /register | Dynamic Client Registration |
GET /authorize | Authorization (redirects to the Cognito Hosted UI) |
POST /oauth/token | Token exchange (injects the App Client secret) |
ANY /mcp | Proxy to the upstream MCP server |
Seul le gestionnaire de jeton dispose d’un accès en lecture au secret App Client de Cognito dans Secrets Manager.
Déploiement du DCR Proxy
Section intitulée « Déploiement du DCR Proxy »Le proxy ne crée pas vos ressources Cognito ni votre serveur MCP. Au lieu de cela, vous injectez les identifiants des ressources gérées ailleurs (qu’elles soient générées par ce plugin ou provisionnées séparément), gardant le proxy découplé de la façon dont ces ressources sont provisionnées.
Vous fournissez :
- L’id du Cognito User Pool et l’id de l’App Client
- L’ARN d’un secret Secrets Manager contenant le secret App Client. Le gestionnaire de jeton lit ceci au moment de l’exécution ; la valeur n’est jamais exposée au client.
- L’URL de base du domaine Cognito Hosted UI
- L’URL complète de votre serveur MCP en amont
Instanciez le construct généré dans votre stack, en passant les propriétés requises :
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',});Le construct expose les points de terminaison du proxy (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) en tant que propriétés en lecture seule.
Référencez le module généré depuis votre configuration Terraform, en passant les variables requises :
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}Le module expose les points de terminaison du proxy (proxy_url, mcp_url, metadata_url, token_endpoint, registration_endpoint) en tant que sorties.
Placer un proxy devant un serveur MCP
Section intitulée « Placer un proxy devant un serveur MCP »Pour placer un proxy devant un serveur MCP généré avec le générateur ts#mcp-server (ou py#mcp-server) en utilisant --auth cognito, passez le même User Pool et App Client au serveur MCP et au proxy, et utilisez l’invocationUrl du construct du serveur MCP comme upstreamUrl du 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}Placer un proxy devant une AgentCore Gateway
Section intitulée « Placer un proxy devant une AgentCore Gateway »Pour placer un proxy devant une AgentCore Gateway générée avec le générateur agentcore-gateway en utilisant --auth cognito, passez le même User Pool et App Client à la gateway et au proxy, et utilisez le gatewayUrl du construct de la gateway comme upstreamUrl du 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}Autoriser les URI de redirection des clients
Section intitulée « Autoriser les URI de redirection des clients »Parce que le proxy implémente Dynamic Client Registration de manière virtuelle — il n’y a pas d’App Client Cognito par client — chaque client MCP s’authentifie via le seul App Client que vous passez au proxy. Pendant le flux OAuth, le proxy transmet le redirect_uri du client au Cognito Hosted UI sans modification, donc Cognito effectue la vérification faisant autorité : le callback doit être enregistré en tant qu’URL de callback sur cet App Client, sinon Cognito rejette la connexion.
C’est un effet secondaire de la conception DCR virtuelle. Un client peut enregistrer n’importe quel redirect_uri avec le proxy, mais la connexion ne réussit que si cette URL exacte est l’une des URL de callback de l’App Client. Cognito fait correspondre les URL de callback exactement, y compris le port, donc les clients qui écoutent sur un port éphémère aléatoire ne peuvent pas être couverts par un caractère générique — vous devez épingler chaque client à une URL de callback fixe et enregistrer cette URL exacte sur l’App Client.
Ajoutez les URL de callback que vos clients utilisent lorsque vous créez l’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", ]}Consommer un serveur MCP proxifié
Section intitulée « Consommer un serveur MCP proxifié »Le proxy permet aux clients MCP de s’authentifier auprès de votre Cognito User Pool sans aucune configuration spécifique au client au-delà de l’URL du proxy. Lorsqu’un client se connecte au point de terminaison /mcp, il découvre les métadonnées OAuth (via /.well-known/oauth-protected-resource et /.well-known/oauth-authorization-server), s’enregistre dynamiquement et guide l’utilisateur à travers la connexion Cognito Hosted UI. Le proxy injecte le secret App Client lors de l’échange de jeton, donc le client n’en a jamais besoin.
Pour connecter un client, pointez-le vers le mcpUrl du proxy (c’est-à-dire <proxyUrl>/mcp). Les exemples ci-dessous supposent que votre proxy est déployé à https://my-proxy.example.com.
Claude Code
Section intitulée « Claude Code »Ajoutez le serveur proxifié avec la commande claude mcp add, en utilisant le transport HTTP. Par défaut, Claude Code écoute sur un port de callback aléatoire ; passez --callback-port pour l’épingler au port enregistré sur votre App Client (41100 dans les exemples ci-dessus). Claude Code utilise toujours le chemin /callback, donc l’URI de redirection résultant est http://localhost:41100/callback :
claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcpLorsque vous invoquez pour la première fois un outil du serveur, Claude Code ouvre le Cognito Hosted UI pour s’authentifier avant que la requête ne soit proxifiée en amont.
Kiro CLI
Section intitulée « Kiro CLI »Ajoutez le serveur à votre configuration MCP Kiro CLI, en utilisant le transport HTTP. Sans un oauth.redirectUri explicite, Kiro choisit un port de callback aléatoire ; définissez-le sur l’URL enregistrée sur votre App Client afin que le port et le chemin correspondent exactement :
{ "mcpServers": { "my-proxied-server": { "type": "http", "url": "https://my-proxy.example.com/mcp", "oauth": { "redirectUri": "http://localhost:41100/callback" } } }}Kiro CLI déclenche la connexion Cognito Hosted UI lors de la première utilisation et gère les jetons résultants pour les requêtes suivantes.
Réutiliser un UserIdentity User Pool
Section intitulée « Réutiliser un UserIdentity User Pool »Si vous avez déjà un User Pool du générateur ts#website#auth (le construct UserIdentity), vous pouvez placer un serveur MCP devant les mêmes utilisateurs qui se connectent à votre site web. Réutilisez son userPool, mais ajoutez un App Client séparé pour le proxy : le client du site web est un client public sans secret, alors que le proxy DCR nécessite un client confidentiel (generateSecret: true) dont le secret est injecté par le gestionnaire de jeton lors de l’échange de jeton.
UserIdentity configure le domaine User Pool avec Managed Login (version 2). Managed Login nécessite un style de branding par App Client, vous devez donc en créer un pour le nouveau client proxy — sinon sa page de connexion hébergée renvoie 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}