Aller au contenu

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.

Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy --dry-run
ParamètreTypePar défautDescription
name stringdcr-proxyLe 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 stringpackagesLe 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 | terraforminheritLe fournisseur IaC préféré (cdk ou terraform). Par défaut, cela est hérité de votre sélection initiale.
preferInstallDependencies booleantrueIndique 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.

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

Les gestionnaires sont regroupés indépendamment avec Rolldown, et les deux fournisseurs IaC référencent la sortie du bundle résultant.

É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

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

L’infrastructure provisionne une API Gateway HTTP API avec les routes suivantes :

RouteDescription
GET /.well-known/oauth-protected-resourceProtected resource metadata
GET /.well-known/oauth-authorization-serverAuthorization server metadata
GET /.well-known/openid-configurationOpenID configuration (served by the authorization server metadata handler)
POST /registerDynamic Client Registration
GET /authorizeAuthorization (redirects to the Cognito Hosted UI)
POST /oauth/tokenToken exchange (injects the App Client secret)
ANY /mcpProxy 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.

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.

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

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

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

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.

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 :

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

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

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.

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