跳转到内容

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 服务器。

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 proxy 的名称,用于其 TypeScript 处理程序项目、构造/模块类名称,以及在 common/constructs 或 common/terraform 下的目录
directory stringpackages存储 DCR 代理处理程序项目的目录。
subDirectory string-处理程序项目所在的子目录。默认情况下,这是项目名称。
iac inherit | cdk | terraforminherit首选的 IaC 提供程序(cdk 或 terraform)。默认情况下,这继承自您的初始选择。
preferInstallDependencies booleantrue是否在生成器运行后优先安装依赖项。设置为 false 可在批量运行多个生成器时推迟安装(如果需要,仍会运行安装以便后续生成器可以计算 Nx 项目图);在最后一次性安装。

生成器创建一个独立的 TypeScript 项目,其中包含 Lambda 处理程序,以及基于您选择的 iac 部署它们的基础设施。

  • 文件夹<dcr-proxy-name>
    • 文件夹src/
      • 文件夹handlers/
        • 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 提供基础设施即代码,它将在 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/constructs/src
    • 文件夹app
      • 文件夹dcr-proxies
        • 文件夹<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 密钥的读取访问权限。

代理不会创建您的 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)生成器生成的 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 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,
});

要为使用 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 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,
});

由于代理虚拟地实现动态客户端注册——没有为每个客户端创建 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',
],
},
});

代理让 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 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 进行身份验证,然后请求才会被代理到上游。

将服务器添加到您的 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 登录,并管理后续请求的生成令牌。

如果您已经从 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 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,
});