DCR 代理
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 代理”- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#dcr-proxy - 填写必需参数
- 点击
Generate
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| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| 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 提供
/.well-known/oauth-authorization-server和/.well-known/openid-configuration - protected-resource-metadata.ts 提供
/.well-known/oauth-protected-resource - register.ts RFC 7591 动态客户端注册
- authorize.ts 重定向到 Cognito Hosted UI
- token.ts 注入 App Client 密钥并交换令牌
- mcp-proxy.ts 将
/mcp请求代理到上游 MCP 服务器
- authorization-server-metadata.ts 提供
处理程序使用 Rolldown 独立打包,两个 IaC 提供程序都引用生成的打包输出。
由于该生成器会根据您选择的 iac 以基础设施即代码的形式输出,它将在 packages/common 目录下创建一个包含相关 CDK 构造体或 Terraform 模块的项目。
通用的基础设施即代码项目结构如下:
文件夹packages/common/constructs
文件夹src
文件夹app/ 针对特定项目/生成器的基础设施构造体
- …
文件夹core/ 被
app目录构造体重用的通用构造体- …
- index.ts 导出
app目录构造体的入口文件
- project.json 项目构建目标与配置
文件夹packages/common/terraform
文件夹src
文件夹app/ 针对特定项目/生成器的 Terraform 模块
- …
文件夹core/ 被
app目录模块重用的通用模块- …
- project.json 项目构建目标与配置
为了部署代理,会生成以下文件:
文件夹packages/common/constructs/src
文件夹app
文件夹dcr-proxies
文件夹<dcr-proxy-name>
- <dcr-proxy-name>.ts 部署代理的 CDK 构造
文件夹packages/common/terraform/src
文件夹app
文件夹dcr-proxies
文件夹<dcr-proxy-name>
- <dcr-proxy-name>.tf 部署代理的 Terraform 模块
基础设施配置一个 API Gateway HTTP API,包含以下路由:
| 路由 | 描述 |
|---|---|
GET /.well-known/oauth-protected-resource | 受保护资源元数据 |
GET /.well-known/oauth-authorization-server | 授权服务器元数据 |
GET /.well-known/openid-configuration | OpenID 配置(由授权服务器元数据处理程序提供) |
POST /register | 动态客户端注册 |
GET /authorize | 授权(重定向到 Cognito Hosted UI) |
POST /oauth/token | 令牌交换(注入 App Client 密钥) |
ANY /mcp | 代理到上游 MCP 服务器 |
只有令牌处理程序被授予对 Secrets Manager 中 Cognito App Client 密钥的读取访问权限。
部署 DCR 代理
Section titled “部署 DCR 代理”代理不会创建您的 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)生成器并使用 --auth cognito 生成的 MCP 服务器提供前端,请将相同的 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');
// 代理用于令牌交换的机密 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,});
// MCP 服务器,授权为相同 App Client 颁发的 JWTconst 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,});# 代理用于令牌交换的机密 App Client。注册# 您的客户端使用的回调 URL(见下文)。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", # Claude Desktop 使用的回调 "https://claude.ai/api/mcp/auth_callback", ]}
# 将 App Client 密钥存储在 Secrets Manager 中供令牌处理程序读取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}
# MCP 服务器,授权为相同 App Client 颁发的 JWTmodule "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" # 使用 MCP 服务器模块的调用 URL 而不是硬编码 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 生成器并使用 --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 颁发的 JWTconst 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,});# 代理用于令牌交换的机密 App Client。注册# 您的客户端使用的回调 URL(见下文)。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", # Claude Desktop 使用的回调 "https://claude.ai/api/mcp/auth_callback", ]}
# 将 App Client 密钥存储在 Secrets Manager 中供令牌处理程序读取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}
# 网关,授权为相同 App Client 颁发的 JWTmodule "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" # 使用网关模块的 URL 而不是硬编码 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: [ // 本地客户端:固定到一个不常见的固定端口,而不是默认端口 'http://localhost:41100/callback', // 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 = [ # 本地客户端:固定到一个不常见的固定端口,而不是默认端口 "http://localhost:41100/callback", # 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 生成器(UserIdentity 构造)获得了一个 User Pool,您可以为登录到您网站的相同用户提供 MCP 服务器前端。重用其 userPool,但为代理添加一个单独的 App Client:网站的客户端是没有密钥的公共客户端,而 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 为您的网站用户创建的 user poolconst identity = new UserIdentity(this, 'Identity');
// DCR 代理在相同 user pool 上的机密 App Clientconst 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,});# ts#website#auth 为您的网站用户创建的 user pool 模块module "user_identity" { source = "../../common/terraform/src/core/user-identity"}
# DCR 代理在相同 user pool 上的机密 App Clientresource "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 需要为新客户端创建品牌样式resource "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}