@aws/nx-plugin-mcp 1.0.0-rc.44 → 1.0.0-rc.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/aws-nx-mcp.js +17 -9
  2. package/docs/get_started/tutorials/contribute-generator.mdx +1 -1
  3. package/docs/get_started/tutorials/dungeon-game/1.mdx +4 -4
  4. package/docs/get_started/tutorials/dungeon-game/3.mdx +1 -1
  5. package/docs/guides/agentcore-gateway.mdx +94 -7
  6. package/docs/guides/connection/py-agent-a2a.mdx +1 -1
  7. package/docs/guides/connection/py-agent-gateway.mdx +3 -1
  8. package/docs/guides/connection/py-fast-api-dynamodb.mdx +1 -1
  9. package/docs/guides/connection/py-fast-api-rdb.mdx +1 -1
  10. package/docs/guides/connection/react-agui.mdx +9 -9
  11. package/docs/guides/connection/react-py-agent.mdx +2 -2
  12. package/docs/guides/connection/ts-agent-a2a.mdx +1 -1
  13. package/docs/guides/connection/ts-agent-gateway.mdx +3 -1
  14. package/docs/guides/docker-bundling.mdx +10 -13
  15. package/docs/guides/fastapi.mdx +2 -2
  16. package/docs/guides/python-lambda-function.mdx +1 -1
  17. package/docs/guides/react-website-auth.mdx +2 -2
  18. package/docs/guides/react-website.mdx +4 -4
  19. package/docs/guides/security.mdx +1 -1
  20. package/docs/guides/trpc.mdx +2 -2
  21. package/docs/guides/ts-dcr-proxy.mdx +569 -0
  22. package/docs/guides/ts-lambda-function.mdx +1 -1
  23. package/docs/guides/ts-mcp-server.mdx +44 -27
  24. package/docs/guides/ts-nx-plugin.mdx +2 -2
  25. package/docs/guides/ts-smithy-api.mdx +3 -3
  26. package/docs/guides/typescript-infrastructure.mdx +11 -10
  27. package/docs/snippets/lambda-function/deploying-your-function.mdx +1 -1
  28. package/docs/snippets/rdb/deploying.mdx +1 -1
  29. package/docs/snippets/shared-constructs.mdx +1 -1
  30. package/docs/snippets/trivy-image-scan.mdx +13 -3
  31. package/generators.json +13 -7
  32. package/package.json +1 -1
  33. package/src/agentcore-gateway/schema.json +3 -2
  34. package/src/ts/dcr-proxy/schema.json +44 -0
@@ -550,7 +550,7 @@ You don't need `aws-jwt-verify` or any other JWT-verification library here — t
550
550
 
551
551
  ## Deploying your tRPC API
552
552
 
553
- The tRPC API generator creates CDK or Terraform infrastructure as code based on your selected `iacProvider`. You can use this to deploy your tRPC API.
553
+ The tRPC API generator creates CDK or Terraform infrastructure as code based on your selected `iac`. You can use this to deploy your tRPC API.
554
554
 
555
555
  <Infrastructure>
556
556
  <Fragment slot="cdk">
@@ -739,7 +739,7 @@ When using `Custom` auth, your API is protected by a Lambda Authorizer that **de
739
739
  <Snippet name="api/type-safe-api-integrations" parentHeading="Integrations" />
740
740
 
741
741
  :::tip[CDK Type-Safe Integrations]
742
- If you selected CDK for your `iacProvider`, when you add or remove a procedure in your tRPC API, these changes will be reflected immediately in the CDK construct without the need to rebuild.
742
+ If you selected CDK for your `iac`, when you add or remove a procedure in your tRPC API, these changes will be reflected immediately in the CDK construct without the need to rebuild.
743
743
  :::
744
744
 
745
745
  <OptionFilter when={{ auth: 'iam' }} description="Granting API invoke access — IAM-authenticated APIs only">
@@ -0,0 +1,569 @@
1
+ ---
2
+ title: DCR Proxy
3
+ description: Generate an OAuth Dynamic Client Registration proxy for Cognito-authenticated MCP servers
4
+ generator: ts#dcr-proxy
5
+ ---
6
+
7
+ import { FileTree } from '@astrojs/starlight/components';
8
+ import Link from '@components/link.astro';
9
+ import RunGenerator from '@components/run-generator.astro';
10
+ import GeneratorParameters from '@components/generator-parameters.astro';
11
+ import NxCommands from '@components/nx-commands.astro';
12
+ import Infrastructure from '@components/infrastructure.astro';
13
+ import Snippet from '@components/snippet.astro';
14
+
15
+ The DCR Proxy generator creates an OAuth [Dynamic Client Registration (DCR)](https://datatracker.ietf.org/doc/html/rfc7591) proxy in front of an Amazon Cognito User Pool.
16
+
17
+ 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.
18
+
19
+ ## Usage
20
+
21
+ ### Generate a DCR proxy
22
+
23
+ <RunGenerator generator="ts#dcr-proxy" />
24
+
25
+ ### Options
26
+
27
+ <GeneratorParameters generator="ts#dcr-proxy" />
28
+
29
+ ## Generator Output
30
+
31
+ The generator creates a standalone <Link path="/guides/typescript-project">TypeScript project</Link> containing the Lambda handlers, and infrastructure to deploy them based on your selected `iac`.
32
+
33
+ <FileTree>
34
+
35
+ - \<dcr-proxy-name>
36
+ - src/
37
+ - handlers/
38
+ - authorization-server-metadata.ts Serves `/.well-known/oauth-authorization-server` and `/.well-known/openid-configuration`
39
+ - protected-resource-metadata.ts Serves `/.well-known/oauth-protected-resource`
40
+ - register.ts RFC 7591 Dynamic Client Registration
41
+ - authorize.ts Redirects to the Cognito Hosted UI
42
+ - token.ts Injects the App Client secret and exchanges the token
43
+ - mcp-proxy.ts Proxies `/mcp` requests to the upstream MCP server
44
+
45
+ </FileTree>
46
+
47
+ The handlers are bundled independently with [Rolldown](https://rolldown.rs/), and both IaC providers reference the resulting bundle output.
48
+
49
+ ### Infrastructure
50
+
51
+ <Snippet name="shared-constructs" />
52
+
53
+ For deploying the proxy, the following files are generated:
54
+
55
+ <Infrastructure>
56
+ <Fragment slot="cdk">
57
+ <FileTree>
58
+ - packages/common/constructs/src
59
+ - app
60
+ - dcr-proxies
61
+ - \<dcr-proxy-name>
62
+ - \<dcr-proxy-name>.ts CDK construct which deploys the proxy
63
+ </FileTree>
64
+ </Fragment>
65
+ <Fragment slot="terraform">
66
+ <FileTree>
67
+ - packages/common/terraform/src
68
+ - app
69
+ - dcr-proxies
70
+ - \<dcr-proxy-name>
71
+ - \<dcr-proxy-name>.tf Terraform module which deploys the proxy
72
+ </FileTree>
73
+ </Fragment>
74
+ </Infrastructure>
75
+
76
+ The infrastructure provisions an API Gateway HTTP API with the following routes:
77
+
78
+ | Route | Description |
79
+ | --- | --- |
80
+ | `GET /.well-known/oauth-protected-resource` | Protected resource metadata |
81
+ | `GET /.well-known/oauth-authorization-server` | Authorization server metadata |
82
+ | `GET /.well-known/openid-configuration` | OpenID configuration (served by the authorization server metadata handler) |
83
+ | `POST /register` | Dynamic Client Registration |
84
+ | `GET /authorize` | Authorization (redirects to the Cognito Hosted UI) |
85
+ | `POST /oauth/token` | Token exchange (injects the App Client secret) |
86
+ | `ANY /mcp` | Proxy to the upstream MCP server |
87
+
88
+ Only the token handler is granted read access to the Cognito App Client secret in Secrets Manager.
89
+
90
+ ## Deploying the DCR Proxy
91
+
92
+ 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.
93
+
94
+ You provide:
95
+
96
+ - The Cognito User Pool id and App Client id
97
+ - 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.
98
+ - The base URL of the Cognito Hosted UI domain
99
+ - The full URL of your upstream MCP server
100
+
101
+ <Infrastructure>
102
+ <Fragment slot="cdk">
103
+ Instantiate the generated construct in your stack, passing the required properties:
104
+
105
+ ```typescript
106
+ import { DcrProxy } from ':my-scope/common-constructs';
107
+
108
+ new DcrProxy(this, 'DcrProxy', {
109
+ userPoolId: userPool.userPoolId,
110
+ userPoolClientId: userPoolClient.userPoolClientId,
111
+ cognitoClientSecretArn: clientSecret.secretArn,
112
+ cognitoHostedUiBase: userPoolDomain.baseUrl(),
113
+ upstreamUrl: 'https://my-agentcore-runtime-url/mcp',
114
+ });
115
+ ```
116
+
117
+ The construct exposes the proxy endpoints (`proxyUrl`, `mcpUrl`, `metadataUrl`, `tokenEndpoint`, `registrationEndpoint`) as readonly properties.
118
+ </Fragment>
119
+ <Fragment slot="terraform">
120
+ Reference the generated module from your Terraform configuration, passing the required variables:
121
+
122
+ ```hcl
123
+ module "dcr_proxy" {
124
+ source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
125
+
126
+ user_pool_id = aws_cognito_user_pool.main.id
127
+ user_pool_client_id = aws_cognito_user_pool_client.main.id
128
+ cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn
129
+ cognito_hosted_ui_base = "https://${aws_cognito_user_pool_domain.main.domain}.auth.${data.aws_region.current.region}.amazoncognito.com"
130
+ upstream_url = "https://my-agentcore-runtime-url/mcp"
131
+ asset_bucket_name = module.asset_bucket.bucket_name
132
+ }
133
+ ```
134
+
135
+ The module exposes the proxy endpoints (`proxy_url`, `mcp_url`, `metadata_url`, `token_endpoint`, `registration_endpoint`) as outputs.
136
+ </Fragment>
137
+ </Infrastructure>
138
+
139
+ :::note
140
+ 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.
141
+ :::
142
+
143
+ :::tip[Reusing an existing User Pool]
144
+ If you already have a User Pool from the <Link path="guides/react-website-auth">`ts#website#auth`</Link> generator, you can front an MCP server for the same website users. See <Link path="guides/ts-dcr-proxy#reusing-a-useridentity-user-pool">Reusing a UserIdentity User Pool</Link> below.
145
+ :::
146
+
147
+ ### Fronting an MCP Server
148
+
149
+ To front an MCP server generated with the <Link path="guides/ts-mcp-server">`ts#mcp-server`</Link> (or <Link path="guides/py-mcp-server">`py#mcp-server`</Link>) 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`.
150
+
151
+ <Infrastructure>
152
+ <Fragment slot="cdk">
153
+ ```typescript
154
+ import {
155
+ DcrProxy,
156
+ MyProjectMcpServer,
157
+ UserIdentity,
158
+ } from ':my-scope/common-constructs';
159
+ import { OAuthScope } from 'aws-cdk-lib/aws-cognito';
160
+ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
161
+
162
+ const identity = new UserIdentity(this, 'Identity');
163
+
164
+ // A confidential App Client the proxy uses for the token exchange. Register the
165
+ // callback URLs your clients use (see below).
166
+ const proxyClient = identity.userPool.addClient('DcrProxyClient', {
167
+ generateSecret: true,
168
+ oAuth: {
169
+ flows: { authorizationCodeGrant: true },
170
+ scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
171
+ callbackUrls: [
172
+ 'http://localhost:41100/callback',
173
+ // Callback used by Claude Desktop
174
+ 'https://claude.ai/api/mcp/auth_callback',
175
+ ],
176
+ },
177
+ });
178
+
179
+ // Store the App Client secret in Secrets Manager for the token handler to read
180
+ const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
181
+ secretStringValue: proxyClient.userPoolClientSecret,
182
+ });
183
+
184
+ // The MCP server, authorizing JWTs issued for the same App Client
185
+ const mcpServer = new MyProjectMcpServer(this, 'MyProjectMcpServer', {
186
+ identity: {
187
+ userPool: identity.userPool,
188
+ userPoolClient: proxyClient,
189
+ },
190
+ });
191
+
192
+ new DcrProxy(this, 'DcrProxy', {
193
+ userPoolId: identity.userPool.userPoolId,
194
+ userPoolClientId: proxyClient.userPoolClientId,
195
+ cognitoClientSecretArn: clientSecret.secretArn,
196
+ cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
197
+ // Use the MCP server construct's invocation URL rather than hardcoding it
198
+ upstreamUrl: mcpServer.invocationUrl,
199
+ });
200
+ ```
201
+ </Fragment>
202
+ <Fragment slot="terraform">
203
+ ```hcl
204
+ # A confidential App Client the proxy uses for the token exchange. Register the
205
+ # callback URLs your clients use (see below).
206
+ resource "aws_cognito_user_pool_client" "dcr_proxy" {
207
+ name = "dcr-proxy-client"
208
+ user_pool_id = module.user_identity.user_pool_id
209
+ generate_secret = true
210
+ allowed_oauth_flows = ["code"]
211
+ allowed_oauth_flows_user_pool_client = true
212
+ allowed_oauth_scopes = ["openid", "email", "profile"]
213
+ callback_urls = [
214
+ "http://localhost:41100/callback",
215
+ # Callback used by Claude Desktop
216
+ "https://claude.ai/api/mcp/auth_callback",
217
+ ]
218
+ }
219
+
220
+ # Store the App Client secret in Secrets Manager for the token handler to read
221
+ resource "aws_secretsmanager_secret" "client_secret" {
222
+ name = "my-dcr-proxy-client-secret"
223
+ }
224
+
225
+ resource "aws_secretsmanager_secret_version" "client_secret" {
226
+ secret_id = aws_secretsmanager_secret.client_secret.id
227
+ secret_string = aws_cognito_user_pool_client.dcr_proxy.client_secret
228
+ }
229
+
230
+ # The MCP server, authorizing JWTs issued for the same App Client
231
+ module "my_project_mcp_server" {
232
+ source = "../../common/terraform/src/app/mcp-servers/my-project-mcp-server"
233
+
234
+ user_pool_id = module.user_identity.user_pool_id
235
+ user_pool_client_ids = [aws_cognito_user_pool_client.dcr_proxy.id]
236
+ appconfig_application_id = module.runtime_config.application_id
237
+ appconfig_application_arn = module.runtime_config.application_arn
238
+ }
239
+
240
+ module "dcr_proxy" {
241
+ source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
242
+
243
+ user_pool_id = module.user_identity.user_pool_id
244
+ user_pool_client_id = aws_cognito_user_pool_client.dcr_proxy.id
245
+ cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn
246
+ cognito_hosted_ui_base = "https://${module.user_identity.user_pool_domain}.auth.${data.aws_region.current.region}.amazoncognito.com"
247
+ # Use the MCP server module's invocation URL rather than hardcoding it
248
+ upstream_url = module.my_project_mcp_server.invocation_url
249
+ asset_bucket_name = module.asset_bucket.bucket_name
250
+ }
251
+ ```
252
+ </Fragment>
253
+ </Infrastructure>
254
+
255
+ ### Fronting an AgentCore Gateway
256
+
257
+ To front an [AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html) generated with the <Link path="guides/agentcore-gateway">`agentcore-gateway`</Link> 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`.
258
+
259
+ :::caution
260
+ The proxy's OAuth flow requires a Cognito-authenticated gateway. Generate the gateway with `--auth cognito`; the default IAM-authenticated gateway is not compatible with the proxy.
261
+ :::
262
+
263
+ <Infrastructure>
264
+ <Fragment slot="cdk">
265
+ ```typescript
266
+ import {
267
+ DcrProxy,
268
+ MyGateway,
269
+ UserIdentity,
270
+ } from ':my-scope/common-constructs';
271
+ import { OAuthScope } from 'aws-cdk-lib/aws-cognito';
272
+ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
273
+
274
+ const identity = new UserIdentity(this, 'Identity');
275
+
276
+ // A confidential App Client the proxy uses for the token exchange. Register the
277
+ // callback URLs your clients use (see below).
278
+ const proxyClient = identity.userPool.addClient('DcrProxyClient', {
279
+ generateSecret: true,
280
+ oAuth: {
281
+ flows: { authorizationCodeGrant: true },
282
+ scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
283
+ callbackUrls: [
284
+ 'http://localhost:41100/callback',
285
+ // Callback used by Claude Desktop
286
+ 'https://claude.ai/api/mcp/auth_callback',
287
+ ],
288
+ },
289
+ });
290
+
291
+ // Store the App Client secret in Secrets Manager for the token handler to read
292
+ const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
293
+ secretStringValue: proxyClient.userPoolClientSecret,
294
+ });
295
+
296
+ // The gateway, authorizing JWTs issued for the same App Client
297
+ const gateway = new MyGateway(this, 'MyGateway', {
298
+ identity: {
299
+ userPool: identity.userPool,
300
+ userPoolClient: proxyClient,
301
+ },
302
+ });
303
+
304
+ new DcrProxy(this, 'DcrProxy', {
305
+ userPoolId: identity.userPool.userPoolId,
306
+ userPoolClientId: proxyClient.userPoolClientId,
307
+ cognitoClientSecretArn: clientSecret.secretArn,
308
+ cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
309
+ // Use the gateway construct's URL rather than hardcoding it
310
+ upstreamUrl: gateway.gateway.gatewayUrl,
311
+ });
312
+ ```
313
+ </Fragment>
314
+ <Fragment slot="terraform">
315
+ ```hcl
316
+ # A confidential App Client the proxy uses for the token exchange. Register the
317
+ # callback URLs your clients use (see below).
318
+ resource "aws_cognito_user_pool_client" "dcr_proxy" {
319
+ name = "dcr-proxy-client"
320
+ user_pool_id = module.user_identity.user_pool_id
321
+ generate_secret = true
322
+ allowed_oauth_flows = ["code"]
323
+ allowed_oauth_flows_user_pool_client = true
324
+ allowed_oauth_scopes = ["openid", "email", "profile"]
325
+ callback_urls = [
326
+ "http://localhost:41100/callback",
327
+ # Callback used by Claude Desktop
328
+ "https://claude.ai/api/mcp/auth_callback",
329
+ ]
330
+ }
331
+
332
+ # Store the App Client secret in Secrets Manager for the token handler to read
333
+ resource "aws_secretsmanager_secret" "client_secret" {
334
+ name = "my-dcr-proxy-client-secret"
335
+ }
336
+
337
+ resource "aws_secretsmanager_secret_version" "client_secret" {
338
+ secret_id = aws_secretsmanager_secret.client_secret.id
339
+ secret_string = aws_cognito_user_pool_client.dcr_proxy.client_secret
340
+ }
341
+
342
+ # The gateway, authorizing JWTs issued for the same App Client
343
+ module "my_gateway" {
344
+ source = "../../common/terraform/src/app/agentcore-gateway/my-gateway"
345
+
346
+ user_pool_id = module.user_identity.user_pool_id
347
+ user_pool_client_ids = [aws_cognito_user_pool_client.dcr_proxy.id]
348
+ }
349
+
350
+ module "dcr_proxy" {
351
+ source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
352
+
353
+ user_pool_id = module.user_identity.user_pool_id
354
+ user_pool_client_id = aws_cognito_user_pool_client.dcr_proxy.id
355
+ cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn
356
+ cognito_hosted_ui_base = "https://${module.user_identity.user_pool_domain}.auth.${data.aws_region.current.region}.amazoncognito.com"
357
+ # Use the gateway module's URL rather than hardcoding it
358
+ upstream_url = module.my_gateway.gateway_url
359
+ asset_bucket_name = module.asset_bucket.bucket_name
360
+ }
361
+ ```
362
+ </Fragment>
363
+ </Infrastructure>
364
+
365
+ ### Allowing Client Redirect URIs
366
+
367
+ 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.
368
+
369
+ 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.
370
+
371
+ Add the callback URLs your clients use when you create the App Client:
372
+
373
+ <Infrastructure>
374
+ <Fragment slot="cdk">
375
+ ```typescript
376
+ const userPoolClient = userPool.addClient('DcrProxyClient', {
377
+ generateSecret: true,
378
+ oAuth: {
379
+ flows: { authorizationCodeGrant: true },
380
+ callbackUrls: [
381
+ // Local clients: pin to a fixed, uncommon port rather than a default one
382
+ 'http://localhost:41100/callback',
383
+ // Callback used by Claude Desktop
384
+ 'https://claude.ai/api/mcp/auth_callback',
385
+ ],
386
+ },
387
+ });
388
+ ```
389
+ </Fragment>
390
+ <Fragment slot="terraform">
391
+ ```hcl
392
+ resource "aws_cognito_user_pool_client" "dcr_proxy" {
393
+ # ...
394
+ generate_secret = true
395
+ allowed_oauth_flows = ["code"]
396
+ allowed_oauth_flows_user_pool_client = true
397
+ callback_urls = [
398
+ # Local clients: pin to a fixed, uncommon port rather than a default one
399
+ "http://localhost:41100/callback",
400
+ # Callback used by Claude Desktop
401
+ "https://claude.ai/api/mcp/auth_callback",
402
+ ]
403
+ }
404
+ ```
405
+ </Fragment>
406
+ </Infrastructure>
407
+
408
+ :::caution
409
+ Only register callback URLs you trust — any client that can drive a browser to one of these URLs can complete a login against your User Pool.
410
+ :::
411
+
412
+ ## Consuming a Proxied MCP Server
413
+
414
+ 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.
415
+
416
+ 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`.
417
+
418
+ :::note
419
+ Each client uses a callback URL during the OAuth flow. That exact URL — including its port — must be registered as a [callback URL on your Cognito App Client](#allowing-client-redirect-uris), otherwise the login fails. Where a client would otherwise pick a random port, pin it to a fixed value and register that value.
420
+ :::
421
+
422
+ ### Claude Code
423
+
424
+ Add the proxied server with the [`claude mcp add`](https://code.claude.com/docs/en/mcp) 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`:
425
+
426
+ ```bash
427
+ claude mcp add --transport http --callback-port 41100 my-proxied-server https://my-proxy.example.com/mcp
428
+ ```
429
+
430
+ When you first invoke a tool from the server, Claude Code opens the Cognito Hosted UI to authenticate before the request is proxied upstream.
431
+
432
+ ### Kiro CLI
433
+
434
+ Add the server to your [Kiro CLI](https://kiro.dev/docs/cli/mcp/) 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:
435
+
436
+ ```json
437
+ {
438
+ "mcpServers": {
439
+ "my-proxied-server": {
440
+ "type": "http",
441
+ "url": "https://my-proxy.example.com/mcp",
442
+ "oauth": {
443
+ "redirectUri": "http://localhost:41100/callback"
444
+ }
445
+ }
446
+ }
447
+ }
448
+ ```
449
+
450
+ Kiro CLI triggers the Cognito Hosted UI login on first use and manages the resulting tokens for subsequent requests.
451
+
452
+ :::tip
453
+ Any MCP client that supports the Streamable HTTP transport along with OAuth authorization and Dynamic Client Registration can connect to the proxy the same way — supply the `/mcp` URL and the client handles discovery, registration, and login for you.
454
+ :::
455
+
456
+ ## Reusing a UserIdentity User Pool
457
+
458
+ If you already have a User Pool from the <Link path="guides/react-website-auth">`ts#website#auth`</Link> 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.
459
+
460
+ `UserIdentity` configures the User Pool domain with [Managed Login (version 2)](https://docs.aws.amazon.com/cognito/latest/developerguide/managed-login.html). 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`.
461
+
462
+ <Infrastructure>
463
+ <Fragment slot="cdk">
464
+ ```typescript
465
+ import {
466
+ DcrProxy,
467
+ MyProjectMcpServer,
468
+ UserIdentity,
469
+ } from ':my-scope/common-constructs';
470
+ import { OAuthScope, CfnManagedLoginBranding } from 'aws-cdk-lib/aws-cognito';
471
+ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
472
+
473
+ // The user pool created by ts#website#auth for your website users
474
+ const identity = new UserIdentity(this, 'Identity');
475
+
476
+ // A confidential App Client on the SAME user pool for the DCR proxy
477
+ const proxyClient = identity.userPool.addClient('DcrProxyClient', {
478
+ generateSecret: true,
479
+ oAuth: {
480
+ flows: { authorizationCodeGrant: true },
481
+ scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
482
+ callbackUrls: ['http://localhost:41100/callback'],
483
+ },
484
+ });
485
+
486
+ // Managed Login needs a branding style for the new client
487
+ new CfnManagedLoginBranding(this, 'DcrProxyClientBranding', {
488
+ userPoolId: identity.userPool.userPoolId,
489
+ clientId: proxyClient.userPoolClientId,
490
+ useCognitoProvidedValues: true,
491
+ });
492
+
493
+ const clientSecret = new secretsmanager.Secret(this, 'ClientSecret', {
494
+ secretStringValue: proxyClient.userPoolClientSecret,
495
+ });
496
+
497
+ const mcpServer = new MyProjectMcpServer(this, 'MyProjectMcpServer', {
498
+ identity: {
499
+ userPool: identity.userPool,
500
+ userPoolClient: proxyClient,
501
+ },
502
+ });
503
+
504
+ new DcrProxy(this, 'DcrProxy', {
505
+ userPoolId: identity.userPool.userPoolId,
506
+ userPoolClientId: proxyClient.userPoolClientId,
507
+ cognitoClientSecretArn: clientSecret.secretArn,
508
+ // The UserIdentity construct always creates a domain
509
+ cognitoHostedUiBase: identity.userPoolDomain.baseUrl(),
510
+ upstreamUrl: mcpServer.invocationUrl,
511
+ });
512
+ ```
513
+ </Fragment>
514
+ <Fragment slot="terraform">
515
+ ```hcl
516
+ # The user pool module created by ts#website#auth for your website users
517
+ module "user_identity" {
518
+ source = "../../common/terraform/src/core/user-identity"
519
+ }
520
+
521
+ # A confidential App Client on the SAME user pool for the DCR proxy
522
+ resource "aws_cognito_user_pool_client" "dcr_proxy" {
523
+ name = "dcr-proxy-client"
524
+ user_pool_id = module.user_identity.user_pool_id
525
+ generate_secret = true
526
+ allowed_oauth_flows = ["code"]
527
+ allowed_oauth_flows_user_pool_client = true
528
+ allowed_oauth_scopes = ["openid", "email", "profile"]
529
+ callback_urls = ["http://localhost:41100/callback"]
530
+ }
531
+
532
+ # Managed Login needs a branding style for the new client
533
+ resource "aws_cognito_managed_login_branding" "dcr_proxy" {
534
+ user_pool_id = module.user_identity.user_pool_id
535
+ client_id = aws_cognito_user_pool_client.dcr_proxy.id
536
+ use_cognito_provided_values = true
537
+ }
538
+
539
+ resource "aws_secretsmanager_secret" "client_secret" {
540
+ name = "my-dcr-proxy-client-secret"
541
+ }
542
+
543
+ resource "aws_secretsmanager_secret_version" "client_secret" {
544
+ secret_id = aws_secretsmanager_secret.client_secret.id
545
+ secret_string = aws_cognito_user_pool_client.dcr_proxy.client_secret
546
+ }
547
+
548
+ module "my_project_mcp_server" {
549
+ source = "../../common/terraform/src/app/mcp-servers/my-project-mcp-server"
550
+
551
+ user_pool_id = module.user_identity.user_pool_id
552
+ user_pool_client_ids = [aws_cognito_user_pool_client.dcr_proxy.id]
553
+ appconfig_application_id = module.runtime_config.application_id
554
+ appconfig_application_arn = module.runtime_config.application_arn
555
+ }
556
+
557
+ module "dcr_proxy" {
558
+ source = "../../common/terraform/src/app/dcr-proxies/dcr-proxy"
559
+
560
+ user_pool_id = module.user_identity.user_pool_id
561
+ user_pool_client_id = aws_cognito_user_pool_client.dcr_proxy.id
562
+ cognito_client_secret_arn = aws_secretsmanager_secret.client_secret.arn
563
+ cognito_hosted_ui_base = "https://${module.user_identity.user_pool_domain}.auth.${data.aws_region.current.region}.amazoncognito.com"
564
+ upstream_url = module.my_project_mcp_server.invocation_url
565
+ asset_bucket_name = module.asset_bucket.bucket_name
566
+ }
567
+ ```
568
+ </Fragment>
569
+ </Infrastructure>
@@ -56,7 +56,7 @@ If the `functionPath` option is provided, the generator will add the handler to
56
56
 
57
57
  <Snippet name="shared-constructs" />
58
58
 
59
- The generator creates infrastructure as code for deploying your function based on your selected `iacProvider`:
59
+ The generator creates infrastructure as code for deploying your function based on your selected `iac`:
60
60
 
61
61
  <Infrastructure>
62
62
  <Fragment slot="cdk">
@@ -79,40 +79,57 @@ If you selected `none` for `infra`, no CDK constructs or Terraform modules are g
79
79
 
80
80
  ### Adding Tools
81
81
 
82
- Tools are functions that the AI assistant can call to perform actions. You can add new tools in the `server.ts` file:
83
-
84
- ```typescript
85
- server.registerTool("toolName", {
86
- description: "tool description",
87
- inputSchema: { param1: z.string(), param2: z.number() } // Input schema using Zod
88
- },
89
- async ({ param1, param2 }) => {
90
- // Tool implementation
91
- return {
92
- content: [{ type: "text", text: "Result" }]
93
- };
94
- }
95
- );
82
+ Tools are functions that the AI assistant can call to perform actions. Each tool lives in its own file under `tools/` that exports a `register<Name>Tool` function, which you then call from `server.ts`. For example, add `tools/my-tool.ts`:
83
+
84
+ ```typescript title="tools/my-tool.ts"
85
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
86
+ import { z } from 'zod';
87
+
88
+ export const registerMyTool = (server: McpServer) => {
89
+ server.registerTool("toolName", {
90
+ description: "tool description",
91
+ inputSchema: { param1: z.string(), param2: z.number() } // Input schema using Zod
92
+ },
93
+ async ({ param1, param2 }) => {
94
+ // Tool implementation
95
+ return {
96
+ content: [{ type: "text", text: "Result" }]
97
+ };
98
+ }
99
+ );
100
+ };
101
+ ```
102
+
103
+ Then register it in `server.ts`:
104
+
105
+ ```typescript title="server.ts"
106
+ import { registerMyTool } from './tools/my-tool.js';
107
+
108
+ registerMyTool(server);
96
109
  ```
97
110
 
98
111
  ### Adding Resources
99
112
 
100
- Resources provide context to the AI assistant. You can add static resources from files or dynamic resources:
113
+ Resources provide context to the AI assistant. Like tools, each resource lives in its own file under `resources/` that exports a `register<Name>Resource` function called from `server.ts`. You can add static resources from files or dynamic resources:
114
+
115
+ ```typescript title="resources/my-resource.ts"
116
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
101
117
 
102
- ```typescript
103
- const exampleContext = 'some context to return';
118
+ export const registerMyResource = (server: McpServer) => {
119
+ const exampleContext = 'some context to return';
104
120
 
105
- server.registerResource('resource-name', 'example://resource', {}, async (uri) => ({
106
- contents: [{ uri: uri.href, text: exampleContext }],
107
- }));
121
+ server.registerResource('resource-name', 'example://resource', {}, async (uri) => ({
122
+ contents: [{ uri: uri.href, text: exampleContext }],
123
+ }));
108
124
 
109
- // Dynamic resource
110
- server.registerResource('dynamic-resource', 'dynamic://resource', {}, async (uri) => {
111
- const data = await fetchSomeData();
112
- return {
113
- contents: [{ uri: uri.href, text: data }],
114
- };
115
- });
125
+ // Dynamic resource
126
+ server.registerResource('dynamic-resource', 'dynamic://resource', {}, async (uri) => {
127
+ const data = await fetchSomeData();
128
+ return {
129
+ contents: [{ uri: uri.href, text: data }],
130
+ };
131
+ });
132
+ };
116
133
  ```
117
134
 
118
135
  ## Configuring with AI Assistants