跳转到内容

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

  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 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 提供 /.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 服务器

    处理程序使用 Rolldown 独立打包,两个 IaC 提供程序都引用生成的打包输出。

    由于该生成器会根据您选择的 iac 以基础设施即代码的形式输出,它将在 packages/common 目录下创建一个包含相关 CDK 构造体或 Terraform 模块的项目。

    通用的基础设施即代码项目结构如下:

    • 文件夹packages/common/constructs
      • 文件夹src
        • 文件夹app/ 针对特定项目/生成器的基础设施构造体
        • 文件夹core/ app 目录构造体重用的通用构造体
        • index.ts 导出 app 目录构造体的入口文件
      • project.json 项目构建目标与配置

    为了部署代理,会生成以下文件:

    • 文件夹packages/common/constructs/src
      • 文件夹app
        • 文件夹dcr-proxies
          • 文件夹<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 /register动态客户端注册
    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 服务器提供前端,请将相同的 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 颁发的 JWT
    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,
    });

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

    代理允许 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 生成器(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 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,
    });