콘텐츠로 이동

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 서버로 전달합니다.

  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 프록시의 이름으로, TypeScript 핸들러 프로젝트, construct/모듈 클래스 이름, 그리고 common/constructs 또는 common/terraform 하위 디렉토리에 사용됩니다
    directory stringpackagesDCR 프록시 핸들러 프로젝트를 저장할 디렉터리입니다.
    subDirectory string-핸들러 프로젝트가 배치되는 하위 디렉터리입니다. 기본적으로 프로젝트 이름입니다.
    iac inherit | cdk | terraforminherit선호하는 IaC 공급자(cdk 또는 terraform)입니다. 기본적으로 초기 선택에서 상속됩니다.
    preferInstallDependencies booleantrue생성기 실행 후 의존성 설치를 선호할지 여부입니다. 여러 생성기를 일괄 처리할 때 설치를 연기하려면 false로 설정하세요(후속 생성기가 Nx 프로젝트 그래프를 계산할 수 있도록 필요한 경우 설치는 여전히 실행됩니다); 마지막에 한 번 설치하세요.

    생성기는 Lambda 핸들러를 포함하는 독립형 TypeScript 프로젝트와 선택한 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 Dynamic Client Registration
          • 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 /registerDynamic Client Registration
    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',
    });

    구성은 프록시 엔드포인트(proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint)를 읽기 전용 속성으로 노출합니다.

    --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');
    // 프록시가 토큰 교환에 사용하는 기밀 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',
    ],
    },
    });
    // 토큰 핸들러가 읽을 수 있도록 Secrets Manager에 App Client 시크릿 저장
    const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
    secretStringValue: proxyClient.userPoolClientSecret,
    });
    // 동일한 App Client에 대해 발급된 JWT를 승인하는 MCP 서버
    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,
    });

    --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');
    // 프록시가 토큰 교환에 사용하는 기밀 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',
    ],
    },
    });
    // 토큰 핸들러가 읽을 수 있도록 Secrets Manager에 App Client 시크릿 저장
    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,
    });

    프록시는 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: [
    // 로컬 클라이언트: 기본 포트가 아닌 고정된 일반적이지 않은 포트에 고정
    'http://localhost:41100/callback',
    // Claude Desktop에서 사용하는 콜백
    'https://claude.ai/api/mcp/auth_callback',
    ],
    },
    });

    프록시를 사용하면 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 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를 엽니다.

    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 로그인을 트리거하고 후속 요청을 위해 결과 토큰을 관리합니다.

    ts#website#auth 생성기(UserIdentity 구성)에서 이미 User Pool이 있는 경우, 웹사이트에 로그인하는 동일한 사용자를 위해 MCP 서버를 앞에 배치할 수 있습니다. userPool을 재사용하되, 프록시를 위한 별도의 App Client를 추가하세요: 웹사이트의 클라이언트는 시크릿이 없는 공개 클라이언트인 반면, DCR 프록시는 토큰 교환 중에 토큰 핸들러가 시크릿을 주입하는 기밀 클라이언트(generateSecret: true)가 필요합니다.

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