DCR Proxy
DCR Proxy 생성기는 Amazon Cognito User Pool 앞에 OAuth Dynamic Client Registration (DCR) 프록시를 생성합니다.
MCP 클라이언트(Claude Code, Kiro CLI 또는 MCP Inspector 등)는 Dynamic Client Registration 및 메타데이터 검색을 지원하는 OAuth 인증 서버에 대해 인증할 것으로 예상합니다. Amazon Cognito는 DCR을 기본적으로 지원하지 않으며, App Client 시크릿은 절대 공개 클라이언트에 노출되어서는 안 됩니다. 이 프록시는 이러한 격차를 메웁니다: Cognito Hosted UI 플로우를 그대로 유지하고, DCR을 구현하며, 토큰 교환 중에 서버 측에서 App Client 시크릿을 주입하고, MCP 트래픽을 업스트림 MCP 서버로 전달합니다.
사용법
섹션 제목: “사용법”DCR 프록시 생성
섹션 제목: “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 프록시의 이름으로, TypeScript 핸들러 프로젝트, construct/모듈 클래스 이름, 그리고 common/constructs 또는 common/terraform 하위 디렉토리에 사용됩니다 |
| directory | string | packages | DCR 프록시 핸들러 프로젝트를 저장할 디렉터리입니다. |
| subDirectory | string | - | 핸들러 프로젝트가 배치되는 하위 디렉터리입니다. 기본적으로 프로젝트 이름입니다. |
| iac | inherit | cdk | terraform | inherit | 선호하는 IaC 공급자(cdk 또는 terraform)입니다. 기본적으로 초기 선택에서 상속됩니다. |
| preferInstallDependencies | boolean | true | 생성기 실행 후 의존성 설치를 선호할지 여부입니다. 여러 생성기를 일괄 처리할 때 설치를 연기하려면 false로 설정하세요(후속 생성기가 Nx 프로젝트 그래프를 계산할 수 있도록 필요한 경우 설치는 여전히 실행됩니다); 마지막에 한 번 설치하세요. |
생성기 출력
섹션 제목: “생성기 출력”생성기는 Lambda 핸들러를 포함하는 독립형 TypeScript 프로젝트와 선택한 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를 기반으로 코드형 인프라를 제공하므로, 관련 CDK constructs 또는 Terraform 모듈을 포함하는 packages/common에 프로젝트를 생성합니다.
공통 코드형 인프라 프로젝트는 다음과 같이 구성됩니다:
디렉터리packages/common/constructs
디렉터리src
디렉터리app/ 프로젝트/생성기에 특정한 인프라를 위한 Constructs
- …
디렉터리core/
app의 constructs에서 재사용되는 일반 constructs- …
- index.ts
app에서 constructs를 내보내는 진입점
- 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 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 배포
섹션 제목: “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 서버 프론팅
섹션 제목: “MCP 서버 프론팅”--auth cognito를 사용하여 ts#mcp-server(또는 py#mcp-server) 생성기로 생성된 MCP 서버를 프론트하려면, MCP 서버와 프록시 모두에 동일한 User Pool 및 App Client를 전달하고, 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 프론팅
섹션 제목: “AgentCore Gateway 프론팅”--auth cognito를 사용하여 agentcore-gateway 생성기로 생성된 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');
// 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 허용
섹션 제목: “클라이언트 리디렉션 URI 허용”프록시는 Dynamic Client Registration을 가상으로 구현하기 때문에 — 클라이언트별 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에 고정하고 해당 정확한 URL을 App Client에 등록해야 합니다.
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 서버 사용
섹션 제목: “프록시된 MCP 서버 사용”프록시를 사용하면 MCP 클라이언트가 프록시 URL 외에 클라이언트별 구성 없이 Cognito User Pool에 대해 인증할 수 있습니다. 클라이언트가 /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
섹션 제목: “Claude Code”HTTP 전송을 사용하여 claude mcp add 명령으로 프록시된 서버를 추가합니다. 기본적으로 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
섹션 제목: “Kiro CLI”HTTP 전송을 사용하여 Kiro CLI MCP 구성에 서버를 추가합니다. 명시적인 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 재사용
섹션 제목: “UserIdentity User Pool 재사용”ts#website#auth 생성기에서 이미 User Pool이 있는 경우(UserIdentity 구성), 웹사이트에 로그인하는 동일한 사용자를 위한 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';
// 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}