Salta ai contenuti

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.

Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy
Puoi anche eseguire una prova per vedere quali file verrebbero modificati
Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy --dry-run
ParametroTipoPredefinitoDescrizione
name stringdcr-proxyIl 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 stringpackagesLa 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 | terraforminheritIl provider IaC preferito (cdk o terraform). Per impostazione predefinita viene ereditato dalla selezione iniziale.
preferInstallDependencies booleantrueSe 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.

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

Gli handler vengono raggruppati indipendentemente con Rolldown, e entrambi i provider IaC fanno riferimento all’output del bundle risultante.

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

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

L’infrastruttura fornisce un API Gateway HTTP API con le seguenti route:

RouteDescrizione
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

Solo il token handler ha accesso in lettura all’App Client secret di Cognito in Secrets Manager.

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.

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

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

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

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.

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:

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

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

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.

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