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.

  1. Installa il Nx Console VSCode Plugin se non l'hai già fatto
  2. Apri la console Nx in VSCode
  3. Clicca su Generate (UI) nella sezione "Common Nx Commands"
  4. Cerca @aws/nx-plugin - ts#dcr-proxy
  5. Compila i parametri richiesti
    • Clicca su Generate
    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 Serve /.well-known/oauth-authorization-server e /.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 /mcp al server MCP upstream

    Gli handler sono 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 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

    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

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

    RouteDescrizione
    GET /.well-known/oauth-protected-resourceMetadati della risorsa protetta
    GET /.well-known/oauth-authorization-serverMetadati del server di autorizzazione
    GET /.well-known/openid-configurationConfigurazione OpenID (servita dall’handler dei metadati del server di autorizzazione)
    POST /registerDynamic Client Registration
    GET /authorizeAutorizzazione (reindirizza alla Cognito Hosted UI)
    POST /oauth/tokenScambio del token (inietta l’App Client secret)
    ANY /mcpProxy al server MCP upstream

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

    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 leggere
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // Il server MCP, che autorizza i JWT emessi per lo stesso 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(),
    // Usa l'URL di invocazione del costrutto del server MCP invece di codificarlo
    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');
    // 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 leggere
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // Il gateway, che autorizza i JWT emessi per lo stesso 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(),
    // Usa l'URL del costrutto del gateway invece di codificarlo
    upstreamUrl: gateway.gateway.gatewayUrl,
    });

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

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

    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 la Cognito Hosted UI per autenticarsi prima che la richiesta venga inoltrata upstream.

    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.

    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 web
    const identity = new UserIdentity(this, 'Identity');
    // Un App Client confidenziale sullo STESSO user pool per il 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 necessita di uno stile di branding per il nuovo 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,
    // Il costrutto UserIdentity crea sempre un dominio
    cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
    upstreamUrl: mcpServer.invocationUrl,
    });