@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.
- package/bin/aws-nx-mcp.js +17 -9
- package/docs/get_started/tutorials/contribute-generator.mdx +1 -1
- 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/py-fast-api-dynamodb.mdx +1 -1
- package/docs/guides/connection/py-fast-api-rdb.mdx +1 -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 +2 -2
- package/docs/guides/react-website.mdx +4 -4
- 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-mcp-server.mdx +44 -27
- package/docs/guides/ts-nx-plugin.mdx +2 -2
- package/docs/guides/ts-smithy-api.mdx +3 -3
- package/docs/guides/typescript-infrastructure.mdx +11 -10
- 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 +13 -7
- package/package.json +1 -1
- package/src/agentcore-gateway/schema.json +3 -2
- package/src/ts/dcr-proxy/schema.json +44 -0
package/docs/guides/trpc.mdx
CHANGED
|
@@ -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 `
|
|
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 `
|
|
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 `
|
|
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.
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
server.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|