feat: ts dcr proxy generator#939
Conversation
Adds a ts#dcr-proxy generator that scaffolds an OAuth Dynamic Client Registration (DCR) proxy CDK construct for Cognito-authenticated MCP servers, ported from the standalone @drskur/dcr-proxy package. - Generates the DcrProxy construct plus 6 TypeScript Lambda handlers (authorize, token, register, mcp-proxy, and two metadata endpoints) bundled at synth time via NodejsFunction (esbuild) - Emits only into packages/common/constructs (no standalone project); Cognito UserPool/Client/Domain and the upstream MCP URL are injected by the consumer's own stack via DcrProxyProps - Defaults the name to 'dcr-proxy'; CDK-only (throws on terraform) - token handler is the sole reader of the Secrets Manager client secret
…objects Change the DCR proxy construct props to take plain string identifiers (userPoolId, userPoolClientId, cognitoHostedUiBase) and a Secrets Manager secret ARN (cognitoClientSecretArn) instead of Cognito L2 objects. The construct now references an existing secret via fromSecretCompleteArn rather than creating one from the client's generated secret, giving an IaC-neutral interface and decoupling the proxy from how Cognito resources are provisioned.
Move the DCR proxy Lambda handlers out of the shared constructs package into a standalone TypeScript project that is bundled with rolldown, so both CDK and Terraform can reference the same bundle output. Previously the generator was CDK-only because it relied on NodejsFunction's synth-time esbuild bundling. - Generate a dedicated TS handler project (tsProjectGenerator) and add a rolldown bundle target per handler - CDK construct now uses Function + Code.fromAsset pointing at the bundles instead of NodejsFunction - Add a Terraform module (6 Lambdas, Secrets Manager data source scoped to the token handler, shared core http-api module, 7 routes) mirroring the construct - Accept cdk/terraform/inherit for iac and remove the terraform throw - Reference the client secret by ARN via a data source so the value is never exposed in Terraform state
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #939 +/- ##
==========================================
+ Coverage 87.56% 87.65% +0.08%
==========================================
Files 162 164 +2
Lines 6007 6066 +59
Branches 1450 1461 +11
==========================================
+ Hits 5260 5317 +57
- Misses 375 377 +2
Partials 372 372 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| </Infrastructure> | ||
|
|
||
| :::note | ||
| The `/mcp` proxy uses API Gateway HTTP API integrations, which have a hard 29 second timeout. Long-running upstream requests are buffered by the proxy rather than streamed. |
There was a problem hiding this comment.
Would it be better to use a REST API with response streaming to avoid this limitation? :)
|
|
||
| :::note | ||
| The `/mcp` proxy uses API Gateway HTTP API integrations, which have a hard 29 second timeout. Long-running upstream requests are buffered by the proxy rather than streamed. | ||
| ::: |
There was a problem hiding this comment.
Please could we add a section like ## Consuming a Proxied MCP Server and include some documentation on how this enables agents to invoke an mcp server with auth? eg how do you set up Claude Code, Kiro CLI, or MCP Inspector to point to your proxied MCP server
| function_name = "snapshot-proxy-\${each.key}-\${random_string.suffix.result}" | ||
| role = each.key == "token" ? aws_iam_role.token.arn : aws_iam_role.proxy.arn | ||
| handler = "index.handler" | ||
| runtime = "nodejs20.x" |
There was a problem hiding this comment.
Same here can we use a newer node? :)
| await tsDcrProxyGenerator(tree, { name: 'my-proxy', iac: 'cdk' }); | ||
|
|
||
| expect(tree.exists(`${constructDir}/my-proxy.ts`)).toBe(true); | ||
| for (const handler of [ |
There was a problem hiding this comment.
Please could we snapshot each handler? :)
| import { createTreeUsingTsSolutionSetup } from '../../utils/test'; | ||
| import { TS_DCR_PROXY_GENERATOR_INFO, tsDcrProxyGenerator } from './generator'; | ||
|
|
||
| describe('ts#dcr-proxy generator', () => { |
There was a problem hiding this comment.
Please could you also add ts#dcr-proxy to the generator-matrix.ts so it participates in the smoke tests?
| const UPSTREAM_URL = process.env.UPSTREAM_URL!; | ||
| const PROXY_METADATA_URL = process.env.PROXY_METADATA_URL!; | ||
|
|
||
| const HOP_BY_HOP = new Set([ |
There was a problem hiding this comment.
I'm not sure what "hop by hop" means :) Is there a better name and/or could we add a short comment explaining what these headers are used for?
| // The token handler imports @aws-sdk/client-secrets-manager (provided by the | ||
| // Lambda runtime but needed for type-checking and local dev) and the handlers | ||
| // are typed against @types/aws-lambda. | ||
| addDependenciesToPackageJson( | ||
| tree, | ||
| withVersions(['@aws-sdk/client-secrets-manager']), | ||
| withVersions(['@types/aws-lambda']), | ||
| ); |
There was a problem hiding this comment.
I think this should be in dcr-proxy/generator.ts? The dependencies are for the handlers and this is currently only called if vending cdk infrastructure.
| const cognitoIssuer = `https://cognito-idp.${region}.amazonaws.com/${props.userPoolId}`; | ||
|
|
||
| const baseEnv: Record<string, string> = { | ||
| COGNITO_USER_POOL_ID: props.userPoolId, |
There was a problem hiding this comment.
nit: looks like this env variable is unused? :)
| userPoolClientId: userPoolClient.userPoolClientId, | ||
| cognitoClientSecretArn: clientSecret.secretArn, | ||
| cognitoHostedUiBase: userPoolDomain.baseUrl(), | ||
| upstreamUrl: 'https://my-agentcore-runtime-url/mcp', |
There was a problem hiding this comment.
Looks like allowedRedirectPatterns needs to be set for the proxy to work? We should call out what this should be set to for eg claude code in a Consuming a Proxied MCP Server
|
|
||
| ## Using the DCR Proxy | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Nice, I like how decoupled this is :) I think we should add a section to the docs for how to use this with the ts#mcp-server or py#mcp-server, ie I assume you generate it with --auth cognito, then pass the same user pool and client it uses. Would be nice to have full examples for both cdk and tf :)
There was a problem hiding this comment.
Also if you rebase, the example can use the new invocation url property from #942 :)
Reason for this change
Cognito does not support DCR (Dynamic Client Registration), which made it hard to implement authentication via OAuth. So I implemented a generator for a proxy that virtually handles DCR.
Description of changes
Description of how you validated changes
upstream configuration, the shared terraform build dependency, and idempotency. Snapshots were regenerated.
Issue # (if applicable)
Closes #.
Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license