DCR Proxy
DCR Proxy 生成器在 Amazon Cognito User Pool 前创建一个 OAuth 动态客户端注册 (DCR) 代理。
MCP 客户端(如 Claude Code、Kiro CLI 或 MCP Inspector)期望针对支持动态客户端注册和元数据发现的 OAuth 授权服务器进行身份验证。Amazon Cognito 本身不支持 DCR,并且其 App Client 密钥绝不能暴露给公共客户端。此代理弥补了这一差距:它保持 Cognito Hosted UI 流程完整,实现 DCR,在令牌交换期间在服务器端注入 App Client 密钥,并将 MCP 流量转发到您的上游 MCP 服务器。
生成 DCR 代理
Section titled “生成 DCR 代理”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-proxy- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#dcr-proxy - 填写必需参数
- 点击
Generate
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| name | string | dcr-proxy | DCR proxy 的名称,用于其 TypeScript 处理程序项目、构造/模块类名称,以及在 common/constructs 或 common/terraform 下的目录 |
| directory | string | packages | 存储 DCR 代理处理程序项目的目录。 |
| subDirectory | string | - | 处理程序项目所在的子目录。默认情况下,这是项目名称。 |
| iac | inherit | cdk | terraform | inherit | 首选的 IaC 提供程序(cdk 或 terraform)。默认情况下,这继承自您的初始选择。 |
| preferInstallDependencies | boolean | true | 是否在生成器运行后优先安装依赖项。设置为 false 可在批量运行多个生成器时推迟安装(如果需要,仍会运行安装以便后续生成器可以计算 Nx 项目图);在最后一次性安装。 |
生成器创建一个独立的 TypeScript 项目,其中包含 Lambda 处理程序,以及基于您选择的 iac 部署它们的基础设施。
文件夹<dcr-proxy-name>
文件夹src/
文件夹handlers/
- authorization-server-metadata.ts Serves
/.well-known/oauth-authorization-serverand/.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
/mcprequests to the upstream MCP server
- authorization-server-metadata.ts Serves
处理程序使用 Rolldown 独立打包,两个 IaC 提供程序都引用生成的打包输出。
由于此生成器根据您选择的 iac 提供基础设施即代码,它将在 packages/common 中创建一个项目,其中包含相关的 CDK 构造或 Terraform 模块。
通用基础设施即代码项目的结构如下:
文件夹packages/common/constructs
文件夹src
文件夹app/ Constructs for infrastructure specific to a project/generator
- …
文件夹core/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
文件夹packages/common/terraform
文件夹src
文件夹app/ Terraform modules for infrastructure specific to a project/generator
- …
文件夹core/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
为了部署代理,会生成以下文件:
文件夹packages/common/constructs/src
文件夹app
文件夹dcr-proxies
文件夹<dcr-proxy-name>
- <dcr-proxy-name>.ts CDK construct which deploys the proxy
文件夹packages/common/terraform/src
文件夹app
文件夹dcr-proxies
文件夹<dcr-proxy-name>
- <dcr-proxy-name>.tf Terraform module which deploys the proxy
基础设施提供一个带有以下路由的 API Gateway HTTP API:
| 路由 | 描述 |
|---|---|
GET /.well-known/oauth-protected-resource | Protected resource metadata |
GET /.well-known/oauth-authorization-server | Authorization server metadata |
GET /.well-known/openid-configuration | OpenID configuration (served by the authorization server metadata handler) |
POST /register | Dynamic Client Registration |
GET /authorize | Authorization (redirects to the Cognito Hosted UI) |
POST /oauth/token | Token exchange (injects the App Client secret) |
ANY /mcp | Proxy to the upstream MCP server |
只有令牌处理程序被授予对 Secrets Manager 中 Cognito App Client 密钥的读取访问权限。
部署 DCR Proxy
Section titled “部署 DCR Proxy”代理不会创建您的 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',});该构造将代理端点(proxyUrl、mcpUrl、metadataUrl、tokenEndpoint、registrationEndpoint)作为只读属性公开。
从您的 Terraform 配置中引用生成的模块,传递所需的变量:
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}该模块将代理端点(proxy_url、mcp_url、metadata_url、token_endpoint、registration_endpoint)作为输出公开。
为 MCP 服务器提供前端
Section titled “为 MCP 服务器提供前端”要为使用 ts#mcp-server(或 py#mcp-server)生成器生成的 MCP 服务器提供前端,使用 --auth cognito,将相同的 User Pool 和 App Client 传递给 MCP 服务器和代理,并使用 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 readconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// The MCP server, authorizing JWTs issued for the same 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(), // Use the MCP server construct's invocation URL rather than hardcoding it upstreamUrl: mcpServer.invocationUrl,});# A confidential App Client the proxy uses for the token exchange. Register the# callback URLs your clients use (see below).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 used by Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Store the App Client secret in Secrets Manager for the token handler to readresource "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}
# The MCP server, authorizing JWTs issued for the same 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" # Use the MCP server module's invocation URL rather than hardcoding it upstream_url = module.my_project_mcp_server.invocation_url asset_bucket_name = module.asset_bucket.bucket_name}为 AgentCore Gateway 提供前端
Section titled “为 AgentCore Gateway 提供前端”要为使用 agentcore-gateway 生成器生成的 AgentCore Gateway 提供前端,使用 --auth cognito,将相同的 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 readconst clientSecret = new secretsmanager.Secret(this, 'ClientSecret', { secretStringValue: proxyClient.userPoolClientSecret,});
// The gateway, authorizing JWTs issued for the same 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(), // Use the gateway construct's URL rather than hardcoding it upstreamUrl: gateway.gateway.gatewayUrl,});# A confidential App Client the proxy uses for the token exchange. Register the# callback URLs your clients use (see below).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 used by Claude Desktop "https://claude.ai/api/mcp/auth_callback", ]}
# Store the App Client secret in Secrets Manager for the token handler to readresource "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}
# The gateway, authorizing JWTs issued for the same 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" # Use the gateway module's URL rather than hardcoding it upstream_url = module.my_gateway.gateway_url asset_bucket_name = module.asset_bucket.bucket_name}允许客户端重定向 URI
Section titled “允许客户端重定向 URI”由于代理虚拟地实现动态客户端注册——没有为每个客户端创建 Cognito App Client——每个 MCP 客户端都通过您传递给代理的单个 App Client 进行身份验证。在 OAuth 流程期间,代理将客户端的 redirect_uri 原封不动地转发到 Cognito Hosted UI,因此 Cognito 执行权威检查:回调必须注册为该 App Client 上的回调 URL,否则 Cognito 会拒绝登录。
这是虚拟 DCR 设计的副作用。客户端可以向代理注册任何 redirect_uri,但只有当该确切 URL 是 App Client 的回调 URL 之一时,登录才会成功。Cognito 精确匹配回调 URL,包括端口,因此监听随机临时端口的客户端无法通过通配符覆盖——您必须将每个客户端固定到固定的回调 URL,并在 App Client 上注册该确切 URL。
在创建 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', ], },});resource "aws_cognito_user_pool_client" "dcr_proxy" { # ... generate_secret = true allowed_oauth_flows = ["code"] allowed_oauth_flows_user_pool_client = true callback_urls = [ # 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 客户端能够针对您的 Cognito User Pool 进行身份验证,而无需任何特定于客户端的配置,除了代理 URL。当客户端连接到 /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 Code
Section titled “Claude Code”使用 claude mcp add 命令添加代理服务器,使用 HTTP 传输。默认情况下,Claude Code 监听随机回调端口;传递 --callback-port 将其固定到在您的 App Client 上注册的端口(上面示例中的 41100)。Claude Code 始终使用 /callback 路径,因此生成的重定向 URI 是 http://localhost:41100/callback:
claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcp当您首次从服务器调用工具时,Claude Code 会打开 Cognito Hosted UI 进行身份验证,然后请求才会被代理到上游。
Kiro CLI
Section titled “Kiro CLI”将服务器添加到您的 Kiro CLI MCP 配置中,使用 HTTP 传输。如果没有显式的 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 登录,并管理后续请求的生成令牌。
重用 UserIdentity User Pool
Section titled “重用 UserIdentity User Pool”如果您已经从 ts#website#auth 生成器获得了一个 User Pool(UserIdentity 构造),您可以为登录到您网站的相同用户提供 MCP 服务器前端。重用其 userPool,但为代理添加一个单独的 App Client:网站的客户端是没有密钥的公共客户端,而 DCR 代理需要一个机密客户端(generateSecret: true),其密钥由令牌处理程序在令牌交换期间注入。
UserIdentity 使用 Managed Login(版本 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 usersconst identity = new UserIdentity(this, 'Identity');
// A confidential App Client on the SAME user pool for the 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 needs a branding style for the new 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, // The UserIdentity construct always creates a domain cognitoHostedUiBase: identity.userPoolDomain.baseUrl(), upstreamUrl: mcpServer.invocationUrl,});# The user pool module created by ts#website#auth for your website usersmodule "user_identity" { source = "../../common/terraform/src/core/user-identity"}
# A confidential App Client on the SAME user pool for the 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 needs a branding style for the new 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}