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”- 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
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-runOpzioni
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 Serve
/.well-known/oauth-authorization-servere/.well-known/openid-configuration - protected-resource-metadata.ts Serve
/.well-known/oauth-protected-resource - register.ts RFC 7591 Dynamic Client Registration
- authorize.ts Reindirizza alla Cognito Hosted UI
- token.ts Inietta l’App Client secret e scambia il token
- mcp-proxy.ts Inoltra le richieste
/mcpal server MCP upstream
- authorization-server-metadata.ts Serve
Gli handler sono 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 selezionato, creerà un progetto in packages/common che include i relativi costrutti CDK o moduli Terraform.
Il progetto comune di infrastruttura come codice è strutturato come segue:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Construct per l’infrastruttura specifica di un progetto/generatore
- …
Directorycore/ Construct generici riutilizzati dai construct in
app- …
- index.ts Punto di ingresso che esporta i construct da
app
- project.json Target di build e configurazione del progetto
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Moduli Terraform per l’infrastruttura specifica di un progetto/generatore
- …
Directorycore/ Moduli generici riutilizzati dai moduli in
app- …
- project.json Target di build e configurazione del progetto
Per distribuire il proxy, vengono generati i seguenti file:
Directorypackages/common/constructs/src
Directoryapp
Directorydcr-proxies
Directory<dcr-proxy-name>
- <dcr-proxy-name>.ts Costrutto CDK che distribuisce il proxy
Directorypackages/common/terraform/src
Directoryapp
Directorydcr-proxies
Directory<dcr-proxy-name>
- <dcr-proxy-name>.tf Modulo Terraform che distribuisce il proxy
L’infrastruttura fornisce un’API Gateway HTTP API con le seguenti route:
| Route | Descrizione |
|---|---|
GET /.well-known/oauth-protected-resource | Metadati della risorsa protetta |
GET /.well-known/oauth-authorization-server | Metadati del server di autorizzazione |
GET /.well-known/openid-configuration | Configurazione OpenID (servita dall’handler dei metadati del server di autorizzazione) |
POST /register | Dynamic Client Registration |
GET /authorize | Autorizzazione (reindirizza alla Cognito Hosted UI) |
POST /oauth/token | Scambio del token (inietta l’App Client secret) |
ANY /mcp | Proxy al server MCP upstream |
Solo il token handler ha accesso in lettura all’App Client secret di Cognito in Secrets Manager.
Distribuzione del DCR Proxy
Sezione intitolata “Distribuzione del 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 tali 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à readonly.
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.
Fronting di un server MCP
Sezione intitolata “Fronting di un server MCP”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');
// Un App Client confidenziale che il proxy utilizza per lo scambio del token. Registra// gli URL di callback utilizzati dai tuoi client (vedi sotto).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 utilizzato da Claude Desktop 'https://claude.ai/api/mcp/auth_callback', ], },});
// Memorizza l'App Client secret in Secrets Manager per il token handler da leggereconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// Il server MCP, che autorizza i JWT emessi per lo stesso 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(), // Usa l'URL di invocazione del costrutto del server MCP invece di codificarlo upstreamUrl: mcpServer.invocationUrl,});# Un App Client confidenziale che il proxy utilizza per lo scambio del token. Registra# gli URL di callback utilizzati dai tuoi client (vedi sotto).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 utilizzato da Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Memorizza l'App Client secret in Secrets Manager per il token handler da leggereresource "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}
# Il server MCP, che autorizza i JWT emessi per lo stesso 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" # Usa l'URL di invocazione del modulo del server MCP invece di codificarlo upstream_url = module.my_project_mcp_server.invocation_url asset_bucket_name = module.asset_bucket.bucket_name}Fronting di un AgentCore Gateway
Sezione intitolata “Fronting di 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');
// Un App Client confidenziale che il proxy utilizza per lo scambio del token. Registra// gli URL di callback utilizzati dai tuoi client (vedi sotto).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 utilizzato da Claude Desktop 'https://claude.ai/api/mcp/auth_callback', ], },});
// Memorizza l'App Client secret in Secrets Manager per il token handler da leggereconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// Il gateway, che autorizza i JWT emessi per lo stesso 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(), // Usa l'URL del costrutto del gateway invece di codificarlo upstreamUrl: gateway.gateway.gatewayUrl,});# Un App Client confidenziale che il proxy utilizza per lo scambio del token. Registra# gli URL di callback utilizzati dai tuoi client (vedi sotto).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 utilizzato da Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Memorizza l'App Client secret in Secrets Manager per il token handler da leggereresource "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}
# Il gateway, che autorizza i JWT emessi per lo stesso 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" # Usa l'URL del modulo del gateway invece di codificarlo 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 Cognito App Client 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 alla 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 utilizzati dai tuoi client quando crei l’App Client:
const userPoolClient = userPool.addClient('DcrProxyClient', { generateSecret: true, oAuth: { flows: { authorizationCodeGrant: true }, callbackUrls: [ // Client locali: fissa a una porta fissa e non comune piuttosto che a una predefinita 'http://localhost:41100/callback', // Callback utilizzato da 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 = [ # Client locali: fissa a una porta fissa e non comune piuttosto che a una predefinita "http://localhost:41100/callback", # Callback utilizzato da 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, utilizzando 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 utilizza 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 la 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 di Kiro CLI, utilizzando 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.
Riutilizzo di un User Pool UserIdentity
Sezione intitolata “Riutilizzo di un User Pool UserIdentity”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';
// Il user pool creato da ts#website#auth per gli utenti del tuo sito webconst identity = new UserIdentity(this, 'Identity');
// Un App Client confidenziale sullo STESSO user pool per il 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 necessita di uno stile di branding per il nuovo 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, // Il costrutto UserIdentity crea sempre un dominio cognitoHostedUiBase: identity.userPoolDomain.baseUrl(), upstreamUrl: mcpServer.invocationUrl,});# Il modulo user pool creato da ts#website#auth per gli utenti del tuo sito webmodule "user_identity" { source = "../../common/terraform/src/core/user-identity"}
# Un App Client confidenziale sullo STESSO user pool per il 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 necessita di uno stile di branding per il nuovo 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}