Bỏ qua để đến nội dung

DCR Proxy

Trình tạo DCR Proxy tạo một proxy OAuth Dynamic Client Registration (DCR) đặt trước Amazon Cognito User Pool.

Các client MCP (như Claude Code, Kiro CLI hoặc MCP Inspector) mong đợi xác thực với một máy chủ ủy quyền OAuth hỗ trợ Dynamic Client Registration và khám phá metadata. Amazon Cognito không hỗ trợ DCR nguyên bản, và App Client secret của nó không bao giờ được phép lộ ra cho một public client. Proxy này bắc cầu khoảng trống đó: nó giữ nguyên luồng Cognito Hosted UI, triển khai DCR, chèn App Client secret ở phía máy chủ trong quá trình trao đổi token, và chuyển tiếp lưu lượng MCP đến máy chủ MCP upstream của bạn.

Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy
Bạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
Terminal window
pnpm nx g @aws/nx-plugin:ts#dcr-proxy --dry-run
Tham sốKiểuMặc địnhMô tả
name stringdcr-proxyTên của DCR proxy của bạn, được sử dụng cho dự án TypeScript handler, tên class construct/module, và thư mục của nó trong common/constructs hoặc common/terraform
directory stringpackagesThư mục để lưu trữ dự án DCR proxy handler.
subDirectory string-Thư mục con mà dự án handler được đặt trong đó. Mặc định là tên dự án.
iac inherit | cdk | terraforminheritNhà cung cấp IaC ưu tiên (cdk hoặc terraform). Mặc định được kế thừa từ lựa chọn ban đầu của bạn.
preferInstallDependencies booleantrueCó nên cài đặt dependencies sau khi generator chạy hay không. Đặt thành false để hoãn việc cài đặt khi chạy nhiều generator cùng lúc (việc cài đặt vẫn chạy nếu cần thiết để các generator tiếp theo có thể tính toán Nx project graph); cài đặt một lần vào cuối.

Trình tạo tạo một dự án TypeScript độc lập chứa các Lambda handler, và cơ sở hạ tầng để triển khai chúng dựa trên iac bạn đã chọn.

  • Thư mục<dcr-proxy-name>
    • Thư mụcsrc/
      • Thư mụchandlers/
        • authorization-server-metadata.ts Phục vụ /.well-known/oauth-authorization-server/.well-known/openid-configuration
        • protected-resource-metadata.ts Phục vụ /.well-known/oauth-protected-resource
        • register.ts RFC 7591 Dynamic Client Registration
        • authorize.ts Chuyển hướng đến Cognito Hosted UI
        • token.ts Chèn App Client secret và trao đổi token
        • mcp-proxy.ts Proxy các yêu cầu /mcp đến máy chủ MCP upstream

Các handler được đóng gói độc lập với Rolldown, và cả hai nhà cung cấp IaC đều tham chiếu đến đầu ra bundle kết quả.

Vì generator này cung cấp infrastructure as code dựa trên iac bạn đã chọn, nó sẽ tạo một dự án trong packages/common bao gồm các CDK constructs hoặc Terraform modules liên quan.

Dự án infrastructure as code chung được cấu trúc như sau:

  • Thư mụcpackages/common/constructs
    • Thư mụcsrc
      • Thư mụcapp/ Constructs for infrastructure specific to a project/generator
      • Thư mụccore/ Generic constructs which are reused by constructs in app
      • index.ts Entry point exporting constructs from app
    • project.json Project build targets and configuration

Để triển khai proxy, các tệp sau được tạo:

  • Thư mụcpackages/common/constructs/src
    • Thư mụcapp
      • Thư mụcdcr-proxies
        • Thư mục<dcr-proxy-name>
          • <dcr-proxy-name>.ts CDK construct triển khai proxy

Cơ sở hạ tầng cung cấp một API Gateway HTTP API với các route sau:

RouteMô tả
GET /.well-known/oauth-protected-resourceMetadata tài nguyên được bảo vệ
GET /.well-known/oauth-authorization-serverMetadata máy chủ ủy quyền
GET /.well-known/openid-configurationCấu hình OpenID (được phục vụ bởi handler metadata máy chủ ủy quyền)
POST /registerDynamic Client Registration
GET /authorizeỦy quyền (chuyển hướng đến Cognito Hosted UI)
POST /oauth/tokenTrao đổi token (chèn App Client secret)
ANY /mcpProxy đến máy chủ MCP upstream

Chỉ có token handler được cấp quyền truy cập đọc vào Cognito App Client secret trong Secrets Manager.

Proxy không tạo các tài nguyên Cognito hoặc máy chủ MCP của bạn. Thay vào đó, bạn chèn các định danh của các tài nguyên được quản lý ở nơi khác (dù được tạo bởi plugin này hay được cung cấp riêng biệt), giữ cho proxy tách rời khỏi cách các tài nguyên đó được cung cấp.

Bạn cung cấp:

  • Id của Cognito User Pool và id của App Client
  • ARN của một secret Secrets Manager chứa App Client secret. Token handler đọc điều này tại runtime; giá trị không bao giờ được lộ ra cho client.
  • URL cơ sở của domain Cognito Hosted UI
  • URL đầy đủ của máy chủ MCP upstream của bạn

Khởi tạo construct được tạo trong stack của bạn, truyền các thuộc tính bắt buộc:

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',
});

Construct hiển thị các endpoint proxy (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) dưới dạng các thuộc tính readonly.

Để đặt trước một máy chủ MCP được tạo bằng trình tạo ts#mcp-server (hoặc py#mcp-server) sử dụng --auth cognito, truyền cùng User Pool và App Client cho cả máy chủ MCP và proxy, và sử dụng invocationUrl của construct máy chủ MCP làm upstreamUrl của proxy.

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,
});

Để đặt trước một AgentCore Gateway được tạo bằng trình tạo agentcore-gateway sử dụng --auth cognito, truyền cùng User Pool và App Client cho cả gateway và proxy, và sử dụng gatewayUrl của construct gateway làm upstreamUrl của proxy.

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,
});

Bởi vì proxy triển khai Dynamic Client Registration một cách ảo — không có Cognito App Client riêng cho từng client — mọi client MCP đều xác thực thông qua App Client duy nhất mà bạn truyền cho proxy. Trong quá trình luồng OAuth, proxy chuyển tiếp redirect_uri của client đến Cognito Hosted UI không thay đổi, vì vậy Cognito thực hiện kiểm tra có thẩm quyền: callback phải được đăng ký là một URL callback trên App Client đó, nếu không Cognito sẽ từ chối đăng nhập.

Đây là một tác dụng phụ của thiết kế DCR ảo. Một client có thể đăng ký bất kỳ redirect_uri nào với proxy, nhưng đăng nhập chỉ thành công nếu URL chính xác đó là một trong các URL callback của App Client. Cognito khớp các URL callback chính xác, bao gồm cả cổng, vì vậy các client lắng nghe trên một cổng tạm thời ngẫu nhiên không thể được bao phủ bởi một ký tự đại diện — bạn phải ghim mỗi client vào một URL callback cố định và đăng ký URL chính xác đó trên App Client.

Thêm các URL callback mà client của bạn sử dụng khi bạn tạo App Client:

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',
],
},
});

Proxy cho phép các client MCP xác thực với Cognito User Pool của bạn mà không cần bất kỳ cấu hình cụ thể nào cho client ngoài URL proxy. Khi một client kết nối đến endpoint /mcp, nó khám phá metadata OAuth (thông qua /.well-known/oauth-protected-resource/.well-known/oauth-authorization-server), tự động đăng ký chính nó, và dẫn người dùng qua đăng nhập Cognito Hosted UI. Proxy chèn App Client secret trong quá trình trao đổi token, vì vậy client không bao giờ cần nó.

Để kết nối một client, hướng nó đến mcpUrl của proxy (tức là <proxyUrl>/mcp). Các ví dụ dưới đây giả định proxy của bạn được triển khai tại https://my-proxy.example.com.

Thêm máy chủ được proxy bằng lệnh claude mcp add, sử dụng giao thức HTTP. Theo mặc định Claude Code lắng nghe trên một cổng callback ngẫu nhiên; truyền --callback-port để ghim nó vào cổng được đăng ký trên App Client của bạn (41100 trong các ví dụ trên). Claude Code luôn sử dụng đường dẫn /callback, vì vậy URI chuyển hướng kết quả là http://localhost:41100/callback:

Terminal window
claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcp

Khi bạn lần đầu tiên gọi một công cụ từ máy chủ, Claude Code mở Cognito Hosted UI để xác thực trước khi yêu cầu được proxy upstream.

Thêm máy chủ vào cấu hình MCP Kiro CLI của bạn, sử dụng giao thức HTTP. Không có oauth.redirectUri rõ ràng, Kiro chọn một cổng callback ngẫu nhiên; đặt nó thành URL được đăng ký trên App Client của bạn để cổng và đường dẫn khớp chính xác:

{
"mcpServers": {
"my-proxied-server": {
"type": "http",
"url": "https://my-proxy.example.com/mcp",
"oauth": {
"redirectUri": "http://localhost:41100/callback"
}
}
}
}

Kiro CLI kích hoạt đăng nhập Cognito Hosted UI khi sử dụng lần đầu và quản lý các token kết quả cho các yêu cầu tiếp theo.

Nếu bạn đã có một User Pool từ trình tạo ts#website#auth (construct UserIdentity), bạn có thể đặt trước một máy chủ MCP cho cùng những người dùng đăng nhập vào website của bạn. Tái sử dụng userPool của nó, nhưng thêm một App Client riêng biệt cho proxy: client của website là một public client không có secret, trong khi DCR proxy yêu cầu một confidential client (generateSecret: true) mà secret của nó được token handler chèn trong quá trình trao đổi token.

UserIdentity cấu hình domain User Pool với Managed Login (phiên bản 2). Managed Login yêu cầu một branding style cho mỗi App Client, vì vậy bạn phải tạo một cho client proxy mới — nếu không trang đăng nhập hosted của nó trả về 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,
});