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 secretは公開クライアントに公開してはいけません。このプロキシはそのギャップを埋めます:Cognito Hosted UIフローをそのまま維持し、DCRを実装し、トークン交換時にサーバー側でApp Client secretを注入し、MCPトラフィックを上流のMCPサーバーに転送します。

Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy
変更されるファイルを確認するためにドライランを実行することもできます
Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy --dry-run
パラメータデフォルト説明
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 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

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

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

共通のインフラストラクチャコードプロジェクトは、次のように構成されています:

  • 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 construct which deploys the proxy

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

ルート説明
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

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

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

以下を提供します:

  • Cognito User Pool idとApp Client id
  • App Client secretを保持する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)を読み取り専用プロパティとして公開します。

--auth cognitoを使用してts#mcp-server(またはpy#mcp-server)ジェネレーターで生成された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');
// 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,
});

--auth cognitoを使用してagentcore-gatewayジェネレーターで生成された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');
// 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,
});

クライアントリダイレクト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: [
// 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',
],
},
});

プロキシされた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 secretを注入するため、クライアントはそれを必要としません。

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

claude mcp addコマンドを使用して、HTTPトランスポートでプロキシされたサーバーを追加します。デフォルトでClaude Codeはランダムなコールバックポートでリッスンします。App Clientに登録されているポート(上記の例では41100)に固定するには、--callback-portを渡します。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';
// 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,
});