@aws/nx-plugin-mcp 1.0.0-rc.44 → 1.0.0-rc.45
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.
- package/bin/aws-nx-mcp.js +10 -2
- package/docs/get_started/tutorials/dungeon-game/1.mdx +4 -4
- package/docs/get_started/tutorials/dungeon-game/3.mdx +1 -1
- package/docs/guides/agentcore-gateway.mdx +94 -7
- package/docs/guides/connection/py-agent-a2a.mdx +1 -1
- package/docs/guides/connection/py-agent-gateway.mdx +3 -1
- package/docs/guides/connection/react-agui.mdx +9 -9
- package/docs/guides/connection/react-py-agent.mdx +2 -2
- package/docs/guides/connection/ts-agent-a2a.mdx +1 -1
- package/docs/guides/connection/ts-agent-gateway.mdx +3 -1
- package/docs/guides/docker-bundling.mdx +10 -13
- package/docs/guides/fastapi.mdx +2 -2
- package/docs/guides/python-lambda-function.mdx +1 -1
- package/docs/guides/react-website-auth.mdx +1 -1
- package/docs/guides/react-website.mdx +3 -3
- package/docs/guides/security.mdx +1 -1
- package/docs/guides/trpc.mdx +2 -2
- package/docs/guides/ts-dcr-proxy.mdx +569 -0
- package/docs/guides/ts-lambda-function.mdx +1 -1
- package/docs/guides/ts-smithy-api.mdx +3 -3
- package/docs/guides/typescript-infrastructure.mdx +6 -6
- package/docs/snippets/lambda-function/deploying-your-function.mdx +1 -1
- package/docs/snippets/rdb/deploying.mdx +1 -1
- package/docs/snippets/shared-constructs.mdx +1 -1
- package/docs/snippets/trivy-image-scan.mdx +13 -3
- package/generators.json +6 -0
- package/package.json +1 -1
- package/src/agentcore-gateway/schema.json +3 -2
- package/src/ts/dcr-proxy/schema.json +44 -0
|
@@ -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 `iacProvider`.
|
|
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 `
|
|
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">
|
|
@@ -69,7 +69,7 @@ The generator creates two related projects in the `<directory>/<api-name>` direc
|
|
|
69
69
|
|
|
70
70
|
### Infrastructure
|
|
71
71
|
|
|
72
|
-
Since this generator creates infrastructure as code based on your chosen `
|
|
72
|
+
Since this generator creates 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.
|
|
73
73
|
|
|
74
74
|
The common infrastructure as code project is structured as follows:
|
|
75
75
|
|
|
@@ -581,7 +581,7 @@ The local server will not only hot-reload when you make TypeScript changes to yo
|
|
|
581
581
|
|
|
582
582
|
## Deploying your Smithy API
|
|
583
583
|
|
|
584
|
-
The generator creates CDK or Terraform infrastructure based on your selected `
|
|
584
|
+
The generator creates CDK or Terraform infrastructure based on your selected `iac`.
|
|
585
585
|
|
|
586
586
|
<Infrastructure>
|
|
587
587
|
<Fragment slot="cdk">
|
|
@@ -765,7 +765,7 @@ If you are actively working on both your CDK infrastructure and Smithy API toget
|
|
|
765
765
|
</Fragment>
|
|
766
766
|
<Fragment slot="terraform">
|
|
767
767
|
:::note[Terraform Limitations]
|
|
768
|
-
We do not support type-safe integrations for Terraform, and therefore no code generation targets are configured if you selected Terraform for your `
|
|
768
|
+
We do not support type-safe integrations for Terraform, and therefore no code generation targets are configured if you selected Terraform for your `iac`.
|
|
769
769
|
:::
|
|
770
770
|
</Fragment>
|
|
771
771
|
</Infrastructure>
|
|
@@ -43,7 +43,7 @@ The generator will create the following project structure in the `<directory>/<n
|
|
|
43
43
|
|
|
44
44
|
</FileTree>
|
|
45
45
|
|
|
46
|
-
If you set the `
|
|
46
|
+
If you set the `stageConfig` option, the generator also creates two shared packages for centralized credential management (if they don't already exist):
|
|
47
47
|
|
|
48
48
|
<FileTree>
|
|
49
49
|
|
|
@@ -99,9 +99,9 @@ new ApplicationStage(app, 'my-app-sandbox', {
|
|
|
99
99
|
|
|
100
100
|
The `env` property tells CDK which AWS account and region to deploy to. `CDK_DEFAULT_ACCOUNT` and `CDK_DEFAULT_REGION` are resolved automatically by the CDK CLI from your active AWS credentials. See the [CDK environments documentation](https://docs.aws.amazon.com/cdk/v2/guide/environments.html) for more details.
|
|
101
101
|
|
|
102
|
-
If you generated with `
|
|
102
|
+
If you generated with `stageConfig`, the `main.ts` reads account and region from a centralized config file instead, falling back to environment variables when no config is set:
|
|
103
103
|
|
|
104
|
-
```ts title="src/main.ts (with
|
|
104
|
+
```ts title="src/main.ts (with stageConfig)"
|
|
105
105
|
import stagesConfig from ':my-scope/common-infra-config';
|
|
106
106
|
|
|
107
107
|
const projectStages = stagesConfig.projects?.['packages/infra']?.stages ?? {};
|
|
@@ -158,12 +158,12 @@ export class ApplicationStage extends Stage {
|
|
|
158
158
|
### Stage Credential Configuration
|
|
159
159
|
|
|
160
160
|
:::note[Staged Configuration]
|
|
161
|
-
This section applies when you generate with `
|
|
161
|
+
This section applies when you generate with `stageConfig`. Without it, the generator produces a simpler setup where you manage AWS credentials yourself (e.g., by exporting `AWS_PROFILE` before deploying).
|
|
162
162
|
:::
|
|
163
163
|
|
|
164
164
|
When you have multiple stages targeting different AWS accounts, managing credentials manually can be error-prone, especially as the number of stages grows.
|
|
165
165
|
|
|
166
|
-
The `
|
|
166
|
+
The `stageConfig` option solves this by generating two shared packages:
|
|
167
167
|
|
|
168
168
|
- **`packages/common/infra-config`** — A single config file where you map each stage to its AWS credentials, account, and region. This is importable from any package in your workspace, so your CDK `main.ts` can read account and region from the same source of truth.
|
|
169
169
|
- **`packages/common/scripts`** — `infra-deploy` and `infra-destroy` commands that wrap CDK with automatic credential resolution. When you run `deploy`, the script reads the config, sets the right AWS environment variables for the CDK child process, and runs `cdk deploy`. Your shell environment is never modified.
|
|
@@ -375,7 +375,7 @@ After a build, you can deploy your infrastructure to AWS using the `deploy` targ
|
|
|
375
375
|
Use the `deploy-ci` target if deploying in a CI/CD pipeline. See below for more details.
|
|
376
376
|
:::
|
|
377
377
|
|
|
378
|
-
First, make sure you have AWS credentials configured. If you generated with `
|
|
378
|
+
First, make sure you have AWS credentials configured. If you generated with `stageConfig` and have configured stage credentials in `packages/common/infra-config/src/stages.config.ts`, the deploy command will automatically resolve and apply the correct credentials for the target stage. Otherwise, ensure your AWS credentials are set in your environment (e.g., via `AWS_PROFILE` or environment variables). See the [AWS credentials documentation](https://docs.aws.amazon.com/sdkref/latest/guide/access.html) for the available options.
|
|
379
379
|
|
|
380
380
|
Then run the deploy target:
|
|
381
381
|
|
|
@@ -3,7 +3,7 @@ title: Deploying your Function
|
|
|
3
3
|
---
|
|
4
4
|
import Infrastructure from '@components/infrastructure.astro';
|
|
5
5
|
|
|
6
|
-
This generator creates CDK or Terraform infrastructure as code based on your selected `
|
|
6
|
+
This generator creates CDK or Terraform infrastructure as code based on your selected `iac`. You can use this to deploy your function.
|
|
7
7
|
|
|
8
8
|
<Infrastructure>
|
|
9
9
|
<Fragment slot="cdk">
|
|
@@ -5,7 +5,7 @@ import Infrastructure from '@components/infrastructure.astro';
|
|
|
5
5
|
import Link from '@components/link.astro';
|
|
6
6
|
import Drawer from '@components/drawer.astro';
|
|
7
7
|
|
|
8
|
-
The relational database generator creates CDK or Terraform infrastructure based on your selected `
|
|
8
|
+
The relational database generator creates CDK or Terraform infrastructure based on your selected `iac`.
|
|
9
9
|
|
|
10
10
|
<Infrastructure>
|
|
11
11
|
<Fragment slot="cdk">
|