Skip to content

DCR Proxy

The DCR Proxy generator creates an OAuth Dynamic Client Registration (DCR) proxy in front of an Amazon Cognito User Pool.

MCP clients (such as Claude Code, Kiro CLI or the MCP Inspector) expect to authenticate against an OAuth authorization server that supports Dynamic Client Registration and metadata discovery. Amazon Cognito does not support DCR natively, and its App Client secret must never be exposed to a public client. This proxy bridges that gap: it keeps the Cognito Hosted UI flow intact, implements DCR, injects the App Client secret server-side during the token exchange, and forwards MCP traffic to your upstream MCP server.

  1. Install the Nx Console VSCode Plugin if you haven't already
  2. Open the Nx Console in VSCode
  3. Click Generate (UI) in the "Common Nx Commands" section
  4. Search for @aws/nx-plugin - ts#dcr-proxy
  5. Fill in the required parameters
    • Click Generate
    ParameterTypeDefaultDescription
    name stringdcr-proxyThe name of your DCR proxy, used for its TypeScript handler project, the construct/module class name, and its directory under common/constructs or common/terraform
    directory stringpackagesThe directory to store the DCR proxy handler project in.
    subDirectory string-The sub directory the handler project is placed in. By default this is the project name.
    iac inherit | cdk | terraforminheritThe preferred IaC provider (cdk or terraform). By default this is inherited from your initial selection.
    preferInstallDependencies booleantrueWhether to prefer installing dependencies after the generator runs. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end.

    The generator creates a standalone TypeScript project containing the Lambda handlers, and infrastructure to deploy them based on your selected iac.

    • Directory<dcr-proxy-name>
      • Directorysrc/
        • Directoryhandlers/
          • 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

    The handlers are bundled independently with Rolldown, and both IaC providers reference the resulting bundle output.

    Since this generator vends infrastructure as code based on your chosen iac, it will create a project in packages/common which includes the relevant CDK constructs or Terraform modules.

    The common infrastructure as code project is structured as follows:

    • Directorypackages/common/constructs
      • Directorysrc
        • Directoryapp/ Constructs for infrastructure specific to a project/generator
        • Directorycore/ Generic constructs which are reused by constructs in app
        • index.ts Entry point exporting constructs from app
      • project.json Project build targets and configuration

    For deploying the proxy, the following files are generated:

    • Directorypackages/common/constructs/src
      • Directoryapp
        • Directorydcr-proxies
          • Directory<dcr-proxy-name>
            • <dcr-proxy-name>.ts CDK construct which deploys the proxy

    The infrastructure provisions an API Gateway HTTP API with the following routes:

    RouteDescription
    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

    Only the token handler is granted read access to the Cognito App Client secret in Secrets Manager.

    The proxy does not create your Cognito resources or your MCP server. Instead, you inject the identifiers of resources managed elsewhere (whether generated by this plugin or provisioned separately), keeping the proxy decoupled from how those resources are provisioned.

    You provide:

    • The Cognito User Pool id and App Client id
    • The ARN of a Secrets Manager secret holding the App Client secret. The token handler reads this at runtime; the value is never exposed to the client.
    • The base URL of the Cognito Hosted UI domain
    • The full URL of your upstream MCP server

    Instantiate the generated construct in your stack, passing the required properties:

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

    The construct exposes the proxy endpoints (proxyUrl, mcpUrl, metadataUrl, tokenEndpoint, registrationEndpoint) as readonly properties.

    To front an MCP server generated with the ts#mcp-server (or py#mcp-server) generator using --auth cognito, pass the same User Pool and App Client to both the MCP server and the proxy, and use the MCP server construct’s invocationUrl as the proxy’s 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,
    });

    To front an AgentCore Gateway generated with the agentcore-gateway generator using --auth cognito, pass the same User Pool and App Client to both the gateway and the proxy, and use the gateway construct’s gatewayUrl as the proxy’s 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,
    });

    Because the proxy implements Dynamic Client Registration virtually — there is no per-client Cognito App Client — every MCP client authenticates through the single App Client you pass to the proxy. During the OAuth flow the proxy forwards the client’s redirect_uri to the Cognito Hosted UI unchanged, so Cognito performs the authoritative check: the callback must be registered as a callback URL on that App Client, otherwise Cognito rejects the login.

    This is a side effect of the virtual DCR design. A client can register any redirect_uri with the proxy, but the login only succeeds if that exact URL is one of the App Client’s callback URLs. Cognito matches callback URLs exactly, including the port, so clients that listen on a random ephemeral port cannot be covered by a wildcard — you must pin each client to a fixed callback URL and register that exact URL on the App Client.

    Add the callback URLs your clients use when you create the 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',
    ],
    },
    });

    The proxy lets MCP clients authenticate against your Cognito User Pool without any client-specific configuration beyond the proxy URL. When a client connects to the /mcp endpoint, it discovers the OAuth metadata (via /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server), dynamically registers itself, and drives the user through the Cognito Hosted UI login. The proxy injects the App Client secret during the token exchange, so the client never needs it.

    To connect a client, point it at the proxy’s mcpUrl (i.e. <proxyUrl>/mcp). The examples below assume your proxy is deployed at https://my-proxy.example.com.

    Add the proxied server with the claude mcp add command, using the HTTP transport. By default Claude Code listens on a random callback port; pass --callback-port to pin it to the port registered on your App Client (41100 in the examples above). Claude Code always uses the /callback path, so the resulting redirect URI is http://localhost:41100/callback:

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

    When you first invoke a tool from the server, Claude Code opens the Cognito Hosted UI to authenticate before the request is proxied upstream.

    Add the server to your Kiro CLI MCP configuration, using the HTTP transport. Without an explicit oauth.redirectUri Kiro picks a random callback port; set it to the URL registered on your App Client so the port and path match exactly:

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

    Kiro CLI triggers the Cognito Hosted UI login on first use and manages the resulting tokens for subsequent requests.

    If you already have a User Pool from the ts#website#auth generator (the UserIdentity construct), you can front an MCP server for the same users who log in to your website. Reuse its userPool, but add a separate App Client for the proxy: the website’s client is a public client without a secret, whereas the DCR proxy requires a confidential client (generateSecret: true) whose secret the token handler injects during the token exchange.

    UserIdentity configures the User Pool domain with Managed Login (version 2). Managed Login requires a branding style per App Client, so you must create one for the new proxy client — otherwise its hosted login page returns 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,
    });