DCR Proxy
Il generatore DCR Proxy crea un proxy OAuth Dynamic Client Registration (DCR) davanti a un Amazon Cognito User Pool.
I client MCP (come Claude Code, Kiro CLI o MCP Inspector) si aspettano di autenticarsi contro un server di autorizzazione OAuth che supporta Dynamic Client Registration e metadata discovery. Amazon Cognito non supporta DCR nativamente, e il suo App Client secret non deve mai essere esposto a un client pubblico. Questo proxy colma questa lacuna: mantiene intatto il flusso Cognito Hosted UI, implementa DCR, inietta l’App Client secret lato server durante lo scambio del token e inoltra il traffico MCP al tuo server MCP upstream.
Utilizzo
Sezione intitolata “Utilizzo”Generare un DCR proxy
Sezione intitolata “Generare un DCR proxy”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-proxyPuoi anche eseguire una prova per vedere quali file verrebbero modificati
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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#dcr-proxy - Compila i parametri richiesti
- Clicca su
Generate
Opzioni
Sezione intitolata “Opzioni”| Parametro | Tipo | Predefinito | Descrizione |
|---|---|---|---|
| name | string | dcr-proxy | Il nome del tuo proxy DCR, utilizzato per il progetto del gestore TypeScript, il nome della classe del costrutto/modulo e la sua directory sotto common/constructs o common/terraform |
| directory | string | packages | La directory in cui memorizzare il progetto handler del proxy DCR. |
| subDirectory | string | - | La sotto-directory in cui viene posizionato il progetto handler. Per impostazione predefinita corrisponde al nome del progetto. |
| iac | inherit | cdk | terraform | inherit | Il provider IaC preferito (cdk o terraform). Per impostazione predefinita viene ereditato dalla selezione iniziale. |
| preferInstallDependencies | boolean | true | Se preferire l'installazione delle dipendenze dopo l'esecuzione del generatore. Impostare su false per differire l'installazione quando si eseguono più generatori in batch (un'installazione viene comunque eseguita se necessaria affinché i generatori successivi possano calcolare il grafo del progetto Nx); installare una volta alla fine. |
Output del generatore
Sezione intitolata “Output del generatore”Il generatore crea un progetto TypeScript autonomo contenente i Lambda handler e l’infrastruttura per distribuirli in base al tuo iac selezionato.
Directory<dcr-proxy-name>
Directorysrc/
Directoryhandlers/
- 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
Gli handler vengono raggruppati indipendentemente con Rolldown, e entrambi i provider IaC fanno riferimento all’output del bundle risultante.
Infrastruttura
Sezione intitolata “Infrastruttura”Poiché questo generatore fornisce infrastruttura come codice basata sul tuo iac scelto, creerà un progetto in packages/common che include i costrutti CDK o i moduli Terraform pertinenti.
Il progetto comune di infrastruttura come codice è strutturato come segue:
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
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directorycore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Per distribuire il proxy, vengono generati i seguenti file:
Directorypackages/common/constructs/src
Directoryapp
Directorydcr-proxies
Directory<dcr-proxy-name>
- <dcr-proxy-name>.ts CDK construct which deploys the proxy
Directorypackages/common/terraform/src
Directoryapp
Directorydcr-proxies
Directory<dcr-proxy-name>
- <dcr-proxy-name>.tf Terraform module which deploys the proxy
L’infrastruttura fornisce un API Gateway HTTP API con le seguenti route:
| Route | Descrizione |
|---|---|
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 |
Solo il token handler ha accesso in lettura all’App Client secret di Cognito in Secrets Manager.
Distribuire il DCR Proxy
Sezione intitolata “Distribuire il DCR Proxy”Il proxy non crea le tue risorse Cognito o il tuo server MCP. Invece, inietti gli identificatori delle risorse gestite altrove (sia generate da questo plugin che fornite separatamente), mantenendo il proxy disaccoppiato da come queste risorse vengono fornite.
Fornisci:
- L’id del Cognito User Pool e l’id dell’App Client
- L’ARN di un secret di Secrets Manager che contiene l’App Client secret. Il token handler lo legge a runtime; il valore non viene mai esposto al client.
- L’URL di base del dominio Cognito Hosted UI
- L’URL completo del tuo server MCP upstream
Istanzia il costrutto generato nel tuo stack, passando le proprietà richieste:
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',});Il costrutto espone gli endpoint del proxy (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) come proprietà di sola lettura.
Fai riferimento al modulo generato dalla tua configurazione Terraform, passando le variabili richieste:
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}Il modulo espone gli endpoint del proxy (proxy_url, mcp_url, metadata_url, token_endpoint, registration_endpoint) come output.
Mettere davanti un MCP Server
Sezione intitolata “Mettere davanti un MCP Server”Per mettere davanti un server MCP generato con il generatore ts#mcp-server (o py#mcp-server) usando --auth cognito, passa lo stesso User Pool e App Client sia al server MCP che al proxy, e usa l’invocationUrl del costrutto del server MCP come 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');
// 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}Mettere davanti un AgentCore Gateway
Sezione intitolata “Mettere davanti un AgentCore Gateway”Per mettere davanti un AgentCore Gateway generato con il generatore agentcore-gateway usando --auth cognito, passa lo stesso User Pool e App Client sia al gateway che al proxy, e usa il gatewayUrl del costrutto del gateway come 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');
// 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}Consentire gli URI di reindirizzamento del client
Sezione intitolata “Consentire gli URI di reindirizzamento del client”Poiché il proxy implementa Dynamic Client Registration virtualmente — non esiste un App Client Cognito per client — ogni client MCP si autentica attraverso il singolo App Client che passi al proxy. Durante il flusso OAuth il proxy inoltra il redirect_uri del client al Cognito Hosted UI invariato, quindi Cognito esegue il controllo autorevole: il callback deve essere registrato come URL di callback su quell’App Client, altrimenti Cognito rifiuta il login.
Questo è un effetto collaterale del design DCR virtuale. Un client può registrare qualsiasi redirect_uri con il proxy, ma il login ha successo solo se quell’URL esatto è uno degli URL di callback dell’App Client. Cognito confronta gli URL di callback esattamente, inclusa la porta, quindi i client che ascoltano su una porta effimera casuale non possono essere coperti da un wildcard — devi fissare ogni client a un URL di callback fisso e registrare quell’URL esatto sull’App Client.
Aggiungi gli URL di callback che i tuoi client utilizzano quando crei 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", ]}Consumare un server MCP con proxy
Sezione intitolata “Consumare un server MCP con proxy”Il proxy consente ai client MCP di autenticarsi contro il tuo Cognito User Pool senza alcuna configurazione specifica del client oltre all’URL del proxy. Quando un client si connette all’endpoint /mcp, scopre i metadati OAuth (tramite /.well-known/oauth-protected-resource e /.well-known/oauth-authorization-server), si registra dinamicamente e guida l’utente attraverso il login Cognito Hosted UI. Il proxy inietta l’App Client secret durante lo scambio del token, quindi il client non ne ha mai bisogno.
Per connettere un client, puntalo all’mcpUrl del proxy (cioè <proxyUrl>/mcp). Gli esempi seguenti presuppongono che il tuo proxy sia distribuito su https://my-proxy.example.com.
Claude Code
Sezione intitolata “Claude Code”Aggiungi il server con proxy con il comando claude mcp add, usando il trasporto HTTP. Per impostazione predefinita Claude Code ascolta su una porta di callback casuale; passa --callback-port per fissarla alla porta registrata sul tuo App Client (41100 negli esempi sopra). Claude Code usa sempre il percorso /callback, quindi l’URI di reindirizzamento risultante è http://localhost:41100/callback:
claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcpQuando invochi per la prima volta uno strumento dal server, Claude Code apre il Cognito Hosted UI per autenticarsi prima che la richiesta venga inoltrata upstream.
Kiro CLI
Sezione intitolata “Kiro CLI”Aggiungi il server alla tua configurazione MCP Kiro CLI, usando il trasporto HTTP. Senza un oauth.redirectUri esplicito Kiro sceglie una porta di callback casuale; impostalo sull’URL registrato sul tuo App Client in modo che la porta e il percorso corrispondano esattamente:
{ "mcpServers": { "my-proxied-server": { "type": "http", "url": "https://my-proxy.example.com/mcp", "oauth": { "redirectUri": "http://localhost:41100/callback" } } }}Kiro CLI attiva il login Cognito Hosted UI al primo utilizzo e gestisce i token risultanti per le richieste successive.
Riutilizzare un UserIdentity User Pool
Sezione intitolata “Riutilizzare un UserIdentity User Pool”Se hai già un User Pool dal generatore ts#website#auth (il costrutto UserIdentity), puoi mettere davanti un server MCP per gli stessi utenti che accedono al tuo sito web. Riutilizza il suo userPool, ma aggiungi un App Client separato per il proxy: il client del sito web è un client pubblico senza secret, mentre il DCR proxy richiede un client confidenziale (generateSecret: true) il cui secret viene iniettato dal token handler durante lo scambio del token.
UserIdentity configura il dominio User Pool con Managed Login (versione 2). Managed Login richiede uno stile di branding per App Client, quindi devi crearne uno per il nuovo client proxy — altrimenti la sua pagina di login ospitata restituisce 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}