Skip to content

DCR Proxy

DCR Proxyジェネレーターは、Amazon Cognito User Poolの前段にOAuth Dynamic Client Registration (DCR) プロキシを作成します。

MCPクライアント(Claude Code、Kiro CLI、MCP Inspectorなど)は、Dynamic Client RegistrationとメタデータディスカバリーをサポートするOAuth認可サーバーに対して認証することを期待します。Amazon CognitoはDCRをネイティブにサポートしておらず、そのApp Clientシークレットは決してパブリッククライアントに公開してはいけません。このプロキシはそのギャップを埋めます:Cognito Hosted UIフローをそのまま維持し、DCRを実装し、トークン交換時にサーバー側でApp Clientシークレットを注入し、MCPトラフィックを上流のMCPサーバーに転送します。

  1. インストール Nx Console VSCode Plugin まだインストールしていない場合
  2. VSCodeでNxコンソールを開く
  3. クリック Generate (UI) "Common Nx Commands"セクションで
  4. 検索 @aws/nx-plugin - ts#dcr-proxy
  5. 必須パラメータを入力
    • クリック Generate
    パラメータデフォルト説明
    name stringdcr-proxyDCR プロキシの名前。TypeScript ハンドラープロジェクト、コンストラクト/モジュールのクラス名、および common/constructs または common/terraform 配下のディレクトリに使用されます
    directory stringpackagesDCR プロキシハンドラープロジェクトを格納するディレクトリ。
    subDirectory string-ハンドラープロジェクトが配置されるサブディレクトリ。デフォルトではプロジェクト名になります。
    iac inherit | cdk | terraforminherit優先する IaC プロバイダー(cdk または terraform)。デフォルトでは初期選択から継承されます。
    preferInstallDependencies booleantrueジェネレーター実行後に依存関係のインストールを優先するかどうか。複数のジェネレーターをバッチ処理する場合は false に設定してインストールを延期します(後続のジェネレーターが Nx プロジェクトグラフを計算できるよう、必要に応じてインストールは実行されます)。最後に一度だけインストールします。

    ジェネレーターは、Lambdaハンドラーを含むスタンドアロンのTypeScriptプロジェクトと、選択したiacに基づいてそれらをデプロイするためのインフラストラクチャを作成します。

    • Directory<dcr-proxy-name>
      • Directorysrc/
        • Directoryhandlers/
          • authorization-server-metadata.ts /.well-known/oauth-authorization-server/.well-known/openid-configurationを提供
          • protected-resource-metadata.ts /.well-known/oauth-protected-resourceを提供
          • register.ts RFC 7591 Dynamic Client Registration
          • authorize.ts Cognito Hosted UIへリダイレクト
          • token.ts App Clientシークレットを注入してトークンを交換
          • mcp-proxy.ts /mcpリクエストを上流のMCPサーバーにプロキシ

    ハンドラーはRolldownで独立してバンドルされ、両方のIaCプロバイダーが結果のバンドル出力を参照します。

    このジェネレータは選択した iac に基づいてInfrastructure as Codeを生成するため、packages/common に関連するCDKコンストラクトまたはTerraformモジュールを含むプロジェクトを作成します。

    共通のInfrastructure as Codeプロジェクトは以下の構造を持ちます:

    • Directorypackages/common/constructs
      • Directorysrc
        • Directoryapp/ プロジェクト/ジェネレータ固有のインフラストラクチャ用コンストラクト
        • Directorycore/ app 内のコンストラクトで再利用される汎用コンストラクト
        • index.ts app からコンストラクトをエクスポートするエントリーポイント
      • project.json プロジェクトのビルドターゲットと設定

    プロキシをデプロイするために、以下のファイルが生成されます:

    • Directorypackages/common/constructs/src
      • Directoryapp
        • Directorydcr-proxies
          • Directory<dcr-proxy-name>
            • <dcr-proxy-name>.ts プロキシをデプロイするCDKコンストラクト

    インフラストラクチャは、以下のルートを持つAPI Gateway HTTP APIをプロビジョニングします:

    ルート説明
    GET /.well-known/oauth-protected-resource保護されたリソースのメタデータ
    GET /.well-known/oauth-authorization-server認可サーバーのメタデータ
    GET /.well-known/openid-configurationOpenID設定(認可サーバーメタデータハンドラーによって提供)
    POST /registerDynamic Client Registration
    GET /authorize認可(Cognito Hosted UIへリダイレクト)
    POST /oauth/tokenトークン交換(App Clientシークレットを注入)
    ANY /mcp上流のMCPサーバーへのプロキシ

    トークンハンドラーのみが、Secrets Manager内のCognito App Clientシークレットへの読み取りアクセスを許可されます。

    プロキシはCognitoリソースやMCPサーバーを作成しません。代わりに、他の場所で管理されているリソース(このプラグインによって生成されたものか、別途プロビジョニングされたもの)の識別子を注入し、プロキシをそれらのリソースのプロビジョニング方法から切り離します。

    以下を提供します:

    • Cognito User Pool idとApp Client id
    • App Clientシークレットを保持するSecrets ManagerシークレットのARN。トークンハンドラーは実行時にこれを読み取ります。値はクライアントに公開されません。
    • Cognito Hosted UIドメインのベースURL
    • 上流のMCPサーバーの完全なURL

    生成されたコンストラクトをスタック内でインスタンス化し、必要なプロパティを渡します:

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

    コンストラクトは、プロキシエンドポイント(proxyUrlmcpUrlmetadataUrltokenEndpointregistrationEndpoint)を読み取り専用プロパティとして公開します。

    ts#mcp-server(またはpy#mcp-server)ジェネレーターで--auth cognitoを使用して生成されたMCPサーバーを前段に配置するには、MCPサーバーとプロキシの両方に同じUser PoolとApp Clientを渡し、MCPサーバーコンストラクトのinvocationUrlをプロキシのupstreamUrlとして使用します。

    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');
    // プロキシがトークン交換に使用する機密App Client。クライアントが使用する
    // コールバックURLを登録します(以下を参照)。
    const proxyClient = identity.userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
    callbackUrls: [
    'http://localhost:41100/callback',
    // Claude Desktopで使用されるコールバック
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });
    // トークンハンドラーが読み取るためにApp ClientシークレットをSecrets Managerに保存
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // 同じApp Client用に発行されたJWTを認可するMCPサーバー
    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(),
    // ハードコードではなく、MCPサーバーコンストラクトの呼び出しURLを使用
    upstreamUrl: mcpServer.invocationUrl,
    });

    agentcore-gatewayジェネレーターで--auth cognitoを使用して生成されたAgentCore Gatewayを前段に配置するには、ゲートウェイとプロキシの両方に同じUser PoolとApp Clientを渡し、ゲートウェイコンストラクトのgatewayUrlをプロキシのupstreamUrlとして使用します。

    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');
    // プロキシがトークン交換に使用する機密App Client。クライアントが使用する
    // コールバックURLを登録します(以下を参照)。
    const proxyClient = identity.userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
    callbackUrls: [
    'http://localhost:41100/callback',
    // Claude Desktopで使用されるコールバック
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });
    // トークンハンドラーが読み取るためにApp ClientシークレットをSecrets Managerに保存
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // 同じApp Client用に発行されたJWTを認可するゲートウェイ
    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(),
    // ハードコードではなく、ゲートウェイコンストラクトのURLを使用
    upstreamUrl: gateway.gateway.gatewayUrl,
    });

    クライアントリダイレクトURIの許可

    Section titled “クライアントリダイレクトURIの許可”

    プロキシは仮想的にDynamic Client Registrationを実装するため — クライアントごとのCognito App Clientは存在しません — すべてのMCPクライアントは、プロキシに渡す単一のApp Clientを通じて認証します。OAuthフロー中、プロキシはクライアントのredirect_uriを変更せずにCognito Hosted UIに転送するため、Cognitoが権威的なチェックを実行します:コールバックはそのApp Clientのコールバック URLとして登録されている必要があり、そうでない場合Cognitoはログインを拒否します。

    これは仮想DCR設計の副作用です。クライアントは任意のredirect_uriをプロキシに登録できますが、その正確なURLがApp Clientのコールバック URLの1つである場合にのみログインが成功します。Cognitoはコールバック URLをポートを含めて正確に照合するため、ランダムなエフェメラルポートでリッスンするクライアントはワイルドカードでカバーできません — 各クライアントを固定のコールバック URLにピン留めし、その正確なURLをApp Clientに登録する必要があります。

    App Clientを作成する際に、クライアントが使用するコールバック URLを追加します:

    const userPoolClient = userPool.addClient('DcrProxyClient', {
    generateSecret: true,
    oAuth: {
    flows: { authorizationCodeGrant: true },
    callbackUrls: [
    // ローカルクライアント:デフォルトのポートではなく、固定の一般的でないポートにピン留め
    'http://localhost:41100/callback',
    // Claude Desktopで使用されるコールバック
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });

    プロキシされたMCPサーバーの利用

    Section titled “プロキシされたMCPサーバーの利用”

    プロキシにより、MCPクライアントはプロキシURL以外のクライアント固有の設定なしでCognito User Poolに対して認証できます。クライアントが/mcpエンドポイントに接続すると、OAuthメタデータ(/.well-known/oauth-protected-resource/.well-known/oauth-authorization-server経由)を検出し、動的に自身を登録し、Cognito Hosted UIログインを通じてユーザーを誘導します。プロキシはトークン交換中にApp Clientシークレットを注入するため、クライアントはそれを必要としません。

    クライアントを接続するには、プロキシのmcpUrl(つまり<proxyUrl>/mcp)を指定します。以下の例では、プロキシがhttps://my-proxy.example.comにデプロイされていると仮定しています。

    claude mcp addコマンドを使用して、HTTPトランスポートでプロキシされたサーバーを追加します。デフォルトでは、Claude Codeはランダムなコールバックポートでリッスンします。--callback-portを渡して、App Clientに登録されたポート(上記の例では41100)にピン留めします。Claude Codeは常に/callbackパスを使用するため、結果のリダイレクトURIはhttp://localhost:41100/callbackになります:

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

    サーバーからツールを初めて呼び出すと、Claude CodeはCognito Hosted UIを開いて認証し、リクエストが上流にプロキシされる前に認証します。

    HTTPトランスポートを使用して、Kiro CLI MCP設定にサーバーを追加します。明示的なoauth.redirectUriがない場合、Kiroはランダムなコールバックポートを選択します。ポートとパスが正確に一致するように、App Clientに登録されたURLに設定します:

    {
    "mcpServers": {
    "my-proxied-server": {
    "type": "http",
    "url": "https://my-proxy.example.com/mcp",
    "oauth": {
    "redirectUri": "http://localhost:41100/callback"
    }
    }
    }
    }

    Kiro CLIは初回使用時にCognito Hosted UIログインをトリガーし、後続のリクエストのために結果のトークンを管理します。

    ts#website#authジェネレーター(UserIdentityコンストラクト)から既にUser Poolを持っている場合、Webサイトにログインする同じユーザーのためにMCPサーバーを前段に配置できます。そのuserPoolを再利用しますが、プロキシ用に別のApp Clientを追加します:Webサイトのクライアントはシークレットのないパブリッククライアントですが、DCRプロキシはトークン交換時にトークンハンドラーがシークレットを注入する機密クライアント(generateSecret: true)を必要とします。

    UserIdentityは、Managed Login (version 2)でUser Poolドメインを設定します。Managed LoginはApp Clientごとにブランディングスタイルを必要とするため、新しいプロキシクライアント用に作成する必要があります — そうしないと、ホストされたログインページが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';
    // ts#website#authによってWebサイトユーザー用に作成されたUser Pool
    const identity = new UserIdentity(this, 'Identity');
    // DCRプロキシ用の同じUser Pool上の機密App Client
    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は新しいクライアント用のブランディングスタイルが必要
    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,
    // UserIdentityコンストラクトは常にドメインを作成
    cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
    upstreamUrl: mcpServer.invocationUrl,
    });