@aws/nx-plugin-mcp 1.0.0-rc.37 → 1.0.0-rc.39
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 +328 -132
- package/docs/get_started/building-with-ai.mdx +116 -0
- package/docs/get_started/concepts.mdx +52 -0
- package/docs/get_started/existing-project.mdx +176 -0
- package/docs/get_started/quick-start.mdx +266 -0
- package/docs/get_started/tutorials/contribute-generator.mdx +405 -0
- package/docs/get_started/tutorials/dungeon-game/1.mdx +1205 -0
- package/docs/get_started/tutorials/dungeon-game/2.mdx +237 -0
- package/docs/get_started/tutorials/dungeon-game/3.mdx +76 -0
- package/docs/get_started/tutorials/dungeon-game/4.mdx +162 -0
- package/docs/get_started/tutorials/dungeon-game/overview.mdx +144 -0
- package/docs/get_started/tutorials/dungeon-game/wrap-up.mdx +41 -0
- package/docs/get_started/tutorials/existing-project.mdx +4 -0
- package/docs/guides/docker-bundling.mdx +55 -1
- package/docs/guides/py-agent.mdx +4 -0
- package/docs/guides/py-mcp-server.mdx +4 -0
- package/docs/guides/py-rdb.mdx +4 -0
- package/docs/guides/ts-agent.mdx +4 -0
- package/docs/guides/ts-mcp-server.mdx +4 -0
- package/docs/guides/ts-rdb.mdx +4 -0
- package/docs/snippets/trivy-image-scan.mdx +27 -0
- package/generators.json +7 -0
- package/package.json +1 -1
- package/src/init/schema.json +35 -0
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Set up a monorepo
|
|
3
|
+
description: A walkthrough of how to build an agentic AI-powered dungeon adventure game using the @aws/nx-plugin.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import { Aside, Code, FileTree, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
7
|
+
import { Image } from 'astro:assets';
|
|
8
|
+
import Link from '@components/link.astro';
|
|
9
|
+
import Drawer from '@components/drawer.astro';
|
|
10
|
+
import RunGenerator from '@components/run-generator.astro';
|
|
11
|
+
import NxCommands from '@components/nx-commands.astro';
|
|
12
|
+
import InstallCommand from '@components/install-command.astro';
|
|
13
|
+
import CreateNxWorkspaceCommand from '@components/create-nx-workspace-command.astro';
|
|
14
|
+
import PackageManagerShortCommand from '@components/package-manager-short-command.astro';
|
|
15
|
+
import E2EDiff from '@components/e2e-diff.astro';
|
|
16
|
+
|
|
17
|
+
import dungeonAdventureArchitecturePng from '@assets/dungeon-game-architecture.png'
|
|
18
|
+
import baselineWebsitePng from '@assets/baseline-website.png'
|
|
19
|
+
import baselineGamePng from '@assets/baseline-game.png'
|
|
20
|
+
import nxGraphPng from '@assets/nx-graph.png'
|
|
21
|
+
import gameSelectPng from '@assets/game-select.png'
|
|
22
|
+
import gameConversationPng from '@assets/game-conversation.png'
|
|
23
|
+
|
|
24
|
+
## Task 1: Create a monorepo
|
|
25
|
+
|
|
26
|
+
To create a new monorepo, from within your desired directory, run the following command:
|
|
27
|
+
|
|
28
|
+
<CreateNxWorkspaceCommand workspace="dungeon-adventure" iac="cdk" />
|
|
29
|
+
|
|
30
|
+
:::note[CDK as IaC Provider]
|
|
31
|
+
We use `--iac=cdk` as we will use CDK for infrastructure as code in this tutorial. The Nx Plugin for AWS also supports `terraform`.
|
|
32
|
+
:::
|
|
33
|
+
|
|
34
|
+
This will set up a NX monorepo within the `dungeon-adventure` directory. When you open the directory in VSCode, you will see this file structure:
|
|
35
|
+
|
|
36
|
+
<FileTree>
|
|
37
|
+
- .nx/
|
|
38
|
+
- .vscode/
|
|
39
|
+
- node_modules/
|
|
40
|
+
- packages/ this is where your sub-projects will reside
|
|
41
|
+
- .gitignore
|
|
42
|
+
- biome.json configures Biome for linting and formatting
|
|
43
|
+
- nx.json configures the Nx CLI and monorepo defaults
|
|
44
|
+
- package.json all node dependencies are defined here
|
|
45
|
+
- pnpm-lock.yaml or bun.lock, yarn.lock, package-lock.json depending on package manager
|
|
46
|
+
- pnpm-workspace.yaml if using pnpm
|
|
47
|
+
- README.md
|
|
48
|
+
- tsconfig.base.json all node based sub-projects extend this
|
|
49
|
+
- tsconfig.json
|
|
50
|
+
- aws-nx-plugin.config.mts configuraton for the Nx Plugin for AWS
|
|
51
|
+
</FileTree>
|
|
52
|
+
|
|
53
|
+
We can now start creating our different sub-projects using the `@aws/nx-plugin`.
|
|
54
|
+
|
|
55
|
+
<Aside type="tip" title="Commit Often">It is best practice to ensure all your unstaged files are committed in Git before running any generators. This allows you to see what has changed after running your generator via `git diff`.</Aside>
|
|
56
|
+
|
|
57
|
+
## Task 2: Create a Game API
|
|
58
|
+
|
|
59
|
+
First, let's create our Game API. To do this, create a tRPC API called `GameApi` using these steps:
|
|
60
|
+
|
|
61
|
+
<RunGenerator generator="ts#api" requiredParameters={{ name: "GameApi", framework: "trpc" }} noInteractive />
|
|
62
|
+
|
|
63
|
+
<br />
|
|
64
|
+
|
|
65
|
+
You will see some new files appear in your file tree.
|
|
66
|
+
|
|
67
|
+
<Aside title="Root Dependencies">
|
|
68
|
+
The root `package.json` is now configured with a `type` of `module`, which means ESM is the default module type for all node based sub-projects vended by the `@aws/nx-plugin`.
|
|
69
|
+
For more details on working with TypeScript projects, refer to the <Link path="guides/typescript-project">ts#project generator guide</Link>.
|
|
70
|
+
</Aside>
|
|
71
|
+
|
|
72
|
+
<Drawer title="ts#api updated files" trigger="Click here to examine these files in more detail.">
|
|
73
|
+
Below is a list of all files which have been generated by the `ts#api` generator. We are going to examine some of the key files highlighted in the file tree:
|
|
74
|
+
<FileTree>
|
|
75
|
+
- packages/
|
|
76
|
+
- common/
|
|
77
|
+
- constructs/
|
|
78
|
+
- src/
|
|
79
|
+
- app/ app specific cdk constructs
|
|
80
|
+
- apis/
|
|
81
|
+
- **game-api.ts** cdk construct to create your tRPC API
|
|
82
|
+
- index.ts
|
|
83
|
+
- ...
|
|
84
|
+
- index.ts
|
|
85
|
+
- core/ generic cdk constructs
|
|
86
|
+
- api/
|
|
87
|
+
- rest-api.ts base cdk construct for an API Gateway Rest API
|
|
88
|
+
- trpc-utils.ts utilities for trpc API CDK constructs
|
|
89
|
+
- utils.ts utilities for API constructs
|
|
90
|
+
- index.ts
|
|
91
|
+
- runtime-config.ts
|
|
92
|
+
- index.ts
|
|
93
|
+
- project.json
|
|
94
|
+
- ...
|
|
95
|
+
- game-api/ tRPC API
|
|
96
|
+
- src/
|
|
97
|
+
- client/ vanilla client typically used for ts machine to machine calls
|
|
98
|
+
- index.ts
|
|
99
|
+
- middleware/ powertools instrumentation
|
|
100
|
+
- error.ts
|
|
101
|
+
- index.ts
|
|
102
|
+
- logger.ts
|
|
103
|
+
- metrics.ts
|
|
104
|
+
- tracer.ts
|
|
105
|
+
- schema/ definitions of inputs and outputs for your API
|
|
106
|
+
- index.ts
|
|
107
|
+
- **echo.ts** sample input and output schema
|
|
108
|
+
- z-async-iterable.ts wrapper Zod schema for tRPC subscription output
|
|
109
|
+
- procedures/ specific implementations for your API procedures/routes
|
|
110
|
+
- **echo.ts** sample procedure implementation
|
|
111
|
+
- index.ts
|
|
112
|
+
- init.ts sets up context and middleware
|
|
113
|
+
- handler.ts Lambda handler entrypoint (uses response streaming for REST APIs)
|
|
114
|
+
- local-server.ts used when running the tRPC server locally
|
|
115
|
+
- **router.ts** defines the tRPC router and all procedures
|
|
116
|
+
- project.json
|
|
117
|
+
- ...
|
|
118
|
+
- vitest.workspace.ts
|
|
119
|
+
</FileTree>
|
|
120
|
+
|
|
121
|
+
Let us look at these key files:
|
|
122
|
+
|
|
123
|
+
```ts {5}
|
|
124
|
+
// packages/game-api/src/router.ts
|
|
125
|
+
import { echo } from './procedures/echo.js';
|
|
126
|
+
import { t } from './init.js';
|
|
127
|
+
|
|
128
|
+
export const router = t.router;
|
|
129
|
+
|
|
130
|
+
export const appRouter = router({
|
|
131
|
+
echo,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export type AppRouter = typeof appRouter;
|
|
135
|
+
```
|
|
136
|
+
The router defines the tRPC router for your API and is the place where you will declare all of your API methods. As you can see above, we have a method called `echo` with it's implementation in the `./procedures/echo.ts` file. The Lambda handler entrypoint is in `handler.ts`, which is configured automatically by the generator.
|
|
137
|
+
|
|
138
|
+
```ts {2-5}
|
|
139
|
+
// packages/game-api/src/procedures/echo.ts
|
|
140
|
+
import { publicProcedure } from '../init.js';
|
|
141
|
+
import {
|
|
142
|
+
EchoInputSchema,
|
|
143
|
+
EchoOutputSchema,
|
|
144
|
+
} from '../schema/echo.js';
|
|
145
|
+
|
|
146
|
+
export const echo = publicProcedure
|
|
147
|
+
.input(EchoInputSchema)
|
|
148
|
+
.output(EchoOutputSchema)
|
|
149
|
+
.query((opts) => ({ message: opts.input.message }));
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This file is the implementation of the `echo` method and as you can see is strongly typed by declaring its input and output data structures.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// packages/game-api/src/schema/echo.ts
|
|
156
|
+
import { z } from 'zod';
|
|
157
|
+
|
|
158
|
+
export const EchoInputSchema = z.object({
|
|
159
|
+
message: z.string().max(1024),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export type IEchoInput = z.TypeOf<typeof EchoInputSchema>;
|
|
163
|
+
|
|
164
|
+
export const EchoOutputSchema = z.object({
|
|
165
|
+
message: z.string().max(1024),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export type IEchoOutput = z.TypeOf<typeof EchoOutputSchema>;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
All tRPC schema definitions are defined using [Zod](https://zod.dev/) and are exported as typescript types via the `z.TypeOf` syntax.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// packages/common/constructs/src/app/apis/game-api.ts
|
|
175
|
+
import { Construct } from 'constructs';
|
|
176
|
+
import * as url from 'url';
|
|
177
|
+
import { Distribution } from 'aws-cdk-lib/aws-cloudfront';
|
|
178
|
+
import {
|
|
179
|
+
Code,
|
|
180
|
+
Runtime,
|
|
181
|
+
Function,
|
|
182
|
+
FunctionProps,
|
|
183
|
+
Tracing,
|
|
184
|
+
} from 'aws-cdk-lib/aws-lambda';
|
|
185
|
+
import {
|
|
186
|
+
AuthorizationType,
|
|
187
|
+
LambdaIntegration,
|
|
188
|
+
ResponseTransferMode,
|
|
189
|
+
} from 'aws-cdk-lib/aws-apigateway';
|
|
190
|
+
import { Aspects, Duration } from 'aws-cdk-lib';
|
|
191
|
+
import {
|
|
192
|
+
PolicyDocument,
|
|
193
|
+
PolicyStatement,
|
|
194
|
+
Effect,
|
|
195
|
+
AnyPrincipal,
|
|
196
|
+
IGrantable,
|
|
197
|
+
Grant,
|
|
198
|
+
} from 'aws-cdk-lib/aws-iam';
|
|
199
|
+
import {
|
|
200
|
+
IntegrationBuilder,
|
|
201
|
+
RestApiIntegration,
|
|
202
|
+
} from '../../core/api/utils.js';
|
|
203
|
+
import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';
|
|
204
|
+
import { Procedures, routerToOperations } from '../../core/api/trpc-utils.js';
|
|
205
|
+
import { AppRouter, appRouter } from ':dungeon-adventure/game-api';
|
|
206
|
+
|
|
207
|
+
// String union type for all API operation names
|
|
208
|
+
type Operations = Procedures<AppRouter>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Properties for creating a GameApi construct
|
|
212
|
+
*
|
|
213
|
+
* @template TIntegrations - Map of operation names to their integrations
|
|
214
|
+
*/
|
|
215
|
+
export interface GameApiProps<
|
|
216
|
+
TIntegrations extends Record<Operations, RestApiIntegration>,
|
|
217
|
+
> {
|
|
218
|
+
/**
|
|
219
|
+
* Map of operation names to their API Gateway integrations
|
|
220
|
+
*/
|
|
221
|
+
integrations: TIntegrations;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A CDK construct that creates and configures an AWS API Gateway REST API
|
|
226
|
+
* specifically for GameApi.
|
|
227
|
+
* @template TIntegrations - Map of operation names to their integrations
|
|
228
|
+
*/
|
|
229
|
+
export class GameApi<
|
|
230
|
+
TIntegrations extends Record<Operations, RestApiIntegration>,
|
|
231
|
+
> extends RestApi<Operations, TIntegrations> {
|
|
232
|
+
private allowedOrigins: readonly string[] = ['*'];
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Creates default integrations for all operations, which implement each operation as
|
|
236
|
+
* its own individual lambda function.
|
|
237
|
+
*
|
|
238
|
+
* @param scope - The CDK construct scope
|
|
239
|
+
* @returns An IntegrationBuilder with default lambda integrations
|
|
240
|
+
*/
|
|
241
|
+
public static defaultIntegrations = (scope: Construct) => {
|
|
242
|
+
return IntegrationBuilder.rest({
|
|
243
|
+
operations: routerToOperations(appRouter),
|
|
244
|
+
defaultIntegrationOptions: <FunctionProps>{
|
|
245
|
+
runtime: Runtime.NODEJS_LATEST,
|
|
246
|
+
handler: 'index.handler',
|
|
247
|
+
code: Code.fromAsset(
|
|
248
|
+
url.fileURLToPath(
|
|
249
|
+
new URL(
|
|
250
|
+
'../../../../../../dist/packages/game-api/bundle',
|
|
251
|
+
import.meta.url,
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
),
|
|
255
|
+
timeout: Duration.seconds(30),
|
|
256
|
+
tracing: Tracing.ACTIVE,
|
|
257
|
+
},
|
|
258
|
+
buildDefaultIntegration: (op, props: FunctionProps) => {
|
|
259
|
+
const handler = new Function(scope, `GameApi${op}Handler`, props);
|
|
260
|
+
return {
|
|
261
|
+
handler,
|
|
262
|
+
integration: new LambdaIntegration(handler, {
|
|
263
|
+
responseTransferMode: ResponseTransferMode.STREAM,
|
|
264
|
+
}),
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
constructor(
|
|
271
|
+
scope: Construct,
|
|
272
|
+
id: string,
|
|
273
|
+
props: GameApiProps<TIntegrations>,
|
|
274
|
+
) {
|
|
275
|
+
super(scope, id, {
|
|
276
|
+
apiName: 'GameApi',
|
|
277
|
+
defaultMethodOptions: {
|
|
278
|
+
authorizationType: AuthorizationType.IAM,
|
|
279
|
+
},
|
|
280
|
+
deployOptions: {
|
|
281
|
+
tracingEnabled: true,
|
|
282
|
+
},
|
|
283
|
+
policy: new PolicyDocument({
|
|
284
|
+
statements: [
|
|
285
|
+
// Open up OPTIONS to allow browsers to make unauthenticated preflight requests
|
|
286
|
+
new PolicyStatement({
|
|
287
|
+
effect: Effect.ALLOW,
|
|
288
|
+
principals: [new AnyPrincipal()],
|
|
289
|
+
actions: ['execute-api:Invoke'],
|
|
290
|
+
resources: ['execute-api:/*/OPTIONS/*'],
|
|
291
|
+
}),
|
|
292
|
+
],
|
|
293
|
+
}),
|
|
294
|
+
operations: routerToOperations(appRouter),
|
|
295
|
+
...props,
|
|
296
|
+
});
|
|
297
|
+
Aspects.of(this).add(new AddCorsPreflightAspect(() => this.allowedOrigins));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Restricts CORS to the provided origins
|
|
302
|
+
*
|
|
303
|
+
* Configures the CloudFront distribution domains or origin strings
|
|
304
|
+
* as the only permitted CORS origins in API Gateway preflight responses and the AWS
|
|
305
|
+
* Lambda integrations.
|
|
306
|
+
*
|
|
307
|
+
* @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
|
|
308
|
+
*/
|
|
309
|
+
public restrictCorsTo(
|
|
310
|
+
...origins: (string | Distribution | { cloudFrontDistribution: Distribution })[]
|
|
311
|
+
) {
|
|
312
|
+
const allowedOrigins = origins.map((origin) =>
|
|
313
|
+
typeof origin === 'string'
|
|
314
|
+
? origin
|
|
315
|
+
: 'cloudFrontDistribution' in origin
|
|
316
|
+
? `https://${origin.cloudFrontDistribution.distributionDomainName}`
|
|
317
|
+
: `https://${origin.distributionDomainName}`,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
this.allowedOrigins = allowedOrigins;
|
|
321
|
+
|
|
322
|
+
// Set ALLOWED_ORIGINS environment variable for all Lambda integrations
|
|
323
|
+
Object.values(this.integrations).forEach((integration) => {
|
|
324
|
+
if ('handler' in integration && integration.handler instanceof Function) {
|
|
325
|
+
integration.handler.addEnvironment(
|
|
326
|
+
'ALLOWED_ORIGINS',
|
|
327
|
+
allowedOrigins.join(','),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Grants IAM permissions to invoke any method on this API.
|
|
335
|
+
*
|
|
336
|
+
* @param grantee - The IAM principal to grant permissions to
|
|
337
|
+
*/
|
|
338
|
+
public grantInvokeAccess(grantee: IGrantable) {
|
|
339
|
+
// Here we grant grantee permission to call the api.
|
|
340
|
+
// Machine to machine fine-grained access can be defined here using more specific principals (eg roles or
|
|
341
|
+
// users) and resources (eg which api paths may be invoked by which principal) if required.
|
|
342
|
+
this.api.addToResourcePolicy(
|
|
343
|
+
new PolicyStatement({
|
|
344
|
+
effect: Effect.ALLOW,
|
|
345
|
+
principals: [grantee.grantPrincipal],
|
|
346
|
+
actions: ['execute-api:Invoke'],
|
|
347
|
+
resources: ['execute-api:/*'],
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
Grant.addToPrincipal({
|
|
352
|
+
grantee,
|
|
353
|
+
actions: ['execute-api:Invoke'],
|
|
354
|
+
resourceArns: [this.api.arnForExecuteApi('*', '/*', '*')],
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
This is the CDK construct that defines our `GameApi`. It provides a `defaultIntegrations` method which automatically creates a Lambda function for each procedure in our tRPC API, pointing to the bundled API implementation. This means that at `cdk synth` time, bundling does not occur (opposed to using [NodeJsFunction](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html)) as we have already bundled it as part of the backend project's build target.
|
|
361
|
+
|
|
362
|
+
</Drawer>
|
|
363
|
+
|
|
364
|
+
## Task 3: Create Story agents
|
|
365
|
+
|
|
366
|
+
Now let's create our Story Agents.
|
|
367
|
+
|
|
368
|
+
### Story agent: Python project
|
|
369
|
+
|
|
370
|
+
To create a Python project:
|
|
371
|
+
|
|
372
|
+
<RunGenerator generator="py#project" requiredParameters={{name:"story"}} noInteractive />
|
|
373
|
+
|
|
374
|
+
You will see some new files appear in your file tree.
|
|
375
|
+
<Drawer title="py#project updated files" trigger="Click here to examine these files in more detail.">
|
|
376
|
+
The `py#project` generates these files:
|
|
377
|
+
|
|
378
|
+
<FileTree>
|
|
379
|
+
- .venv/ single virtual env for monorepo
|
|
380
|
+
- packages/
|
|
381
|
+
- story/
|
|
382
|
+
- dungeon_adventure_story/ python module
|
|
383
|
+
- tests/
|
|
384
|
+
- .python-version
|
|
385
|
+
- pyproject.toml
|
|
386
|
+
- project.json
|
|
387
|
+
- .python-version pinned uv python version
|
|
388
|
+
- pyproject.toml
|
|
389
|
+
- uv.lock
|
|
390
|
+
</FileTree>
|
|
391
|
+
|
|
392
|
+
This has configured a Python project and [UV Workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/) with shared virtual environment.
|
|
393
|
+
|
|
394
|
+
</Drawer>
|
|
395
|
+
|
|
396
|
+
### Story agent
|
|
397
|
+
|
|
398
|
+
To add a Strands agent to the project with the `py#agent` generator:
|
|
399
|
+
|
|
400
|
+
<RunGenerator generator="py#agent" requiredParameters={{project:"story", auth:"cognito", protocol:"ag-ui"}} noInteractive />
|
|
401
|
+
|
|
402
|
+
:::note[AG-UI protocol]
|
|
403
|
+
We choose `--protocol=AG-UI` so the agent speaks the [Agent-User Interaction protocol](https://docs.copilotkit.ai/aws-strands/protocol) — this lets our React website talk to it directly via [CopilotKit](https://docs.copilotkit.ai/), with streaming, tool calls, and conversation history handled by the protocol instead of a hand-rolled HTTP client.
|
|
404
|
+
:::
|
|
405
|
+
|
|
406
|
+
You will see some new files appear in your file tree.
|
|
407
|
+
<Drawer title="py#agent updated files" trigger="Click here to examine these files in more detail.">
|
|
408
|
+
The `py#agent` generates these files:
|
|
409
|
+
|
|
410
|
+
<FileTree>
|
|
411
|
+
- packages/
|
|
412
|
+
- story/
|
|
413
|
+
- dungeon_adventure_story/ python module
|
|
414
|
+
- agent/
|
|
415
|
+
- init.py sets up the FastAPI app and middleware
|
|
416
|
+
- main.py entrypoint for your agent in Bedrock AgentCore Runtime
|
|
417
|
+
- agent.py defines an example agent and tools
|
|
418
|
+
- Dockerfile defines the docker image for deployment to AgentCore Runtime
|
|
419
|
+
- common/constructs/
|
|
420
|
+
- src
|
|
421
|
+
- app/agents/story-agent/
|
|
422
|
+
- story-agent.ts construct for deploying your Story agent to AgentCore Runtime
|
|
423
|
+
</FileTree>
|
|
424
|
+
|
|
425
|
+
Let's take a look at some of the files in detail:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
# agent/agent.py
|
|
429
|
+
from contextlib import contextmanager
|
|
430
|
+
|
|
431
|
+
from strands import Agent, tool
|
|
432
|
+
from strands_tools import current_time
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@tool
|
|
436
|
+
def subtract(a: int, b: int) -> int:
|
|
437
|
+
return a - b
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@contextmanager
|
|
441
|
+
def get_agent():
|
|
442
|
+
yield Agent(
|
|
443
|
+
name="StoryAgent",
|
|
444
|
+
description="StoryAgent Agent",
|
|
445
|
+
system_prompt="""
|
|
446
|
+
You are a mathematical wizard.
|
|
447
|
+
Use your tools for mathematical tasks.
|
|
448
|
+
Refer to tools as your 'spellbook'.
|
|
449
|
+
""",
|
|
450
|
+
tools=[subtract, current_time],
|
|
451
|
+
)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
This creates an example Strands agent and defines a subtraction tool.
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
# agent/main.py
|
|
458
|
+
import logging
|
|
459
|
+
import uuid
|
|
460
|
+
|
|
461
|
+
from ag_ui_strands import StrandsAgent, create_strands_app
|
|
462
|
+
from dungeon_adventure_agent_connection import session_id_context
|
|
463
|
+
from fastapi import Request
|
|
464
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
465
|
+
|
|
466
|
+
from .agent import get_agent
|
|
467
|
+
|
|
468
|
+
logging.basicConfig(level=logging.INFO)
|
|
469
|
+
|
|
470
|
+
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
|
|
471
|
+
|
|
472
|
+
# Create AG-UI agent wrapper
|
|
473
|
+
_agent_ctx = get_agent()
|
|
474
|
+
_agent = _agent_ctx.__enter__()
|
|
475
|
+
|
|
476
|
+
agui_agent = StrandsAgent(
|
|
477
|
+
agent=_agent,
|
|
478
|
+
name="StoryAgent",
|
|
479
|
+
description="An Agent exposed via the AG-UI protocol.",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class _SessionIdMiddleware(BaseHTTPMiddleware):
|
|
484
|
+
"""Bind the session ID for this request so downstream MCP / A2A clients forward it on outbound calls."""
|
|
485
|
+
|
|
486
|
+
async def dispatch(self, request: Request, call_next):
|
|
487
|
+
session_id = request.headers.get(SESSION_ID_HEADER) or str(uuid.uuid4())
|
|
488
|
+
with session_id_context(session_id):
|
|
489
|
+
return await call_next(request)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# Create FastAPI app with AG-UI endpoint and health check
|
|
493
|
+
app = create_strands_app(agui_agent, path="/invocations")
|
|
494
|
+
app.add_middleware(_SessionIdMiddleware)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
This is the entrypoint for the agent. Because we selected `--protocol=AG-UI`, the generator wraps our Strands `Agent` with `StrandsAgent` from [`ag_ui_strands`](https://docs.copilotkit.ai/aws-strands/integration) and mounts it on a FastAPI app that speaks the [AG-UI protocol](https://docs.copilotkit.ai/aws-strands/protocol) — this is what CopilotKit will talk to from the React website. The `_SessionIdMiddleware` binds the inbound AgentCore runtime session ID onto a `ContextVar` so any downstream MCP/A2A client we wire up later (e.g. the Inventory MCP server in <Link path="get_started/tutorials/dungeon-game/2">Module 2</Link>) automatically forwards it on its outbound calls. In <Link path="get_started/tutorials/dungeon-game/3">Module 3</Link> we'll also add a `session_manager_provider` so each thread id gets its own `S3SessionManager` and conversation history persists across turns.
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
// common/constructs/src/app/agents/story-agent.ts
|
|
501
|
+
import { Lazy, Names } from 'aws-cdk-lib';
|
|
502
|
+
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
|
|
503
|
+
import { Construct } from 'constructs';
|
|
504
|
+
import { execSync } from 'child_process';
|
|
505
|
+
import * as path from 'path';
|
|
506
|
+
import * as url from 'url';
|
|
507
|
+
import {
|
|
508
|
+
AgentRuntimeArtifact,
|
|
509
|
+
ProtocolType,
|
|
510
|
+
Runtime,
|
|
511
|
+
RuntimeProps,
|
|
512
|
+
} from 'aws-cdk-lib/aws-bedrockagentcore';
|
|
513
|
+
import { IGrantable, IPrincipal } from 'aws-cdk-lib/aws-iam';
|
|
514
|
+
|
|
515
|
+
export type StoryAgentProps = Omit<
|
|
516
|
+
RuntimeProps,
|
|
517
|
+
'runtimeName' | 'protocolConfiguration' | 'agentRuntimeArtifact'
|
|
518
|
+
>;
|
|
519
|
+
|
|
520
|
+
export class StoryAgent extends Construct implements IGrantable {
|
|
521
|
+
public readonly dockerImage: AgentRuntimeArtifact;
|
|
522
|
+
public readonly agentCoreRuntime: Runtime;
|
|
523
|
+
|
|
524
|
+
constructor(scope: Construct, id: string, props?: StoryAgentProps) {
|
|
525
|
+
super(scope, id);
|
|
526
|
+
|
|
527
|
+
this.dockerImage = AgentRuntimeArtifact.fromAsset(
|
|
528
|
+
path.dirname(url.fileURLToPath(new URL(import.meta.url))),
|
|
529
|
+
{
|
|
530
|
+
platform: Platform.LINUX_ARM64,
|
|
531
|
+
extraHash: execSync(
|
|
532
|
+
`docker inspect dungeon-adventure-story-agent:latest --format '{{.Id}}'`,
|
|
533
|
+
{ encoding: 'utf-8' },
|
|
534
|
+
).trim(),
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
this.agentCoreRuntime = new Runtime(this, 'StoryAgent', {
|
|
539
|
+
runtimeName: Lazy.string({
|
|
540
|
+
produce: () =>
|
|
541
|
+
Names.uniqueResourceName(this.agentCoreRuntime, { maxLength: 40 }),
|
|
542
|
+
}),
|
|
543
|
+
protocolConfiguration: ProtocolType.HTTP,
|
|
544
|
+
agentRuntimeArtifact: this.dockerImage,
|
|
545
|
+
...props,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
public get grantPrincipal(): IPrincipal {
|
|
550
|
+
return this.agentCoreRuntime.grantPrincipal;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
This configures a CDK `AgentRuntimeArtifact` which uploads your agent Docker image to ECR, and hosts it using AgentCore Runtime.
|
|
556
|
+
|
|
557
|
+
You may notice an extra `Dockerfile`, that references the Docker image from the `story` project, allowing us to co-locate the Dockerfile and agent source code.
|
|
558
|
+
|
|
559
|
+
</Drawer>
|
|
560
|
+
|
|
561
|
+
## Task 4: Set up inventory tools
|
|
562
|
+
|
|
563
|
+
### Inventory: TypeScript project
|
|
564
|
+
|
|
565
|
+
Let us create an MCP server to provide tools for our Story Agent to manage a player's inventory.
|
|
566
|
+
|
|
567
|
+
First, we create a TypeScript project:
|
|
568
|
+
|
|
569
|
+
<RunGenerator generator="ts#project" requiredParameters={{name:"inventory"}} noInteractive />
|
|
570
|
+
|
|
571
|
+
This will create an empty TypeScript project.
|
|
572
|
+
|
|
573
|
+
<Drawer title="ts#project updated files" trigger="Click here to examine these files in more detail.">
|
|
574
|
+
The `ts#project` generator generates these files.
|
|
575
|
+
|
|
576
|
+
<FileTree>
|
|
577
|
+
- packages/
|
|
578
|
+
- inventory/
|
|
579
|
+
- src/
|
|
580
|
+
- index.ts entry point with example function
|
|
581
|
+
- project.json project configuration
|
|
582
|
+
- vitest.config.mts test configuration
|
|
583
|
+
- tsconfig.json base typescript configuration for the project
|
|
584
|
+
- tsconfig.lib.json typescript configuration for the project targeted for compilation and bundling
|
|
585
|
+
- tsconfig.spec.json typescript configuration for tests
|
|
586
|
+
- tsconfig.base.json updated to configure an alias for other projects to reference this
|
|
587
|
+
</FileTree>
|
|
588
|
+
|
|
589
|
+
</Drawer>
|
|
590
|
+
|
|
591
|
+
### Inventory: MCP server
|
|
592
|
+
|
|
593
|
+
Next, we'll add an MCP server to our TypeScript project:
|
|
594
|
+
|
|
595
|
+
<RunGenerator generator="ts#mcp-server" requiredParameters={{project:"inventory"}} noInteractive />
|
|
596
|
+
|
|
597
|
+
This will add an MCP server.
|
|
598
|
+
<Drawer title="ts#mcp-server updated files" trigger="Click here to examine these files in more detail.">
|
|
599
|
+
The `ts#mcp-server` generator generates these files.
|
|
600
|
+
|
|
601
|
+
<FileTree>
|
|
602
|
+
- packages/
|
|
603
|
+
- inventory/
|
|
604
|
+
- src/mcp-server/
|
|
605
|
+
- index.ts barrel export
|
|
606
|
+
- server.ts creates the MCP server
|
|
607
|
+
- tools/
|
|
608
|
+
- divide.ts example tool
|
|
609
|
+
- resources/
|
|
610
|
+
- sample-guidance.ts example resource
|
|
611
|
+
- stdio.ts entry point for MCP with STDIO transport
|
|
612
|
+
- http.ts entry point for MCP with Streamable HTTP transport
|
|
613
|
+
- Dockerfile builds the image for AgentCore Runtime
|
|
614
|
+
- rolldown.config.ts configuration for bundling the MCP server for deployment to AgentCore
|
|
615
|
+
- common/constructs/
|
|
616
|
+
- src
|
|
617
|
+
- app/mcp-servers/inventory-mcp-server/
|
|
618
|
+
- inventory-mcp-server.ts construct for deploying your inventory MCP server to AgentCore Runtime
|
|
619
|
+
</FileTree>
|
|
620
|
+
|
|
621
|
+
</Drawer>
|
|
622
|
+
|
|
623
|
+
## Task 5: Create the game database
|
|
624
|
+
|
|
625
|
+
Our game state — saved games and each player's inventory — lives in [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). Create a DynamoDB project called `DungeonDb` with the `ts#dynamodb` generator:
|
|
626
|
+
|
|
627
|
+
<RunGenerator generator="ts#dynamodb" requiredParameters={{name:"DungeonDb"}} noInteractive />
|
|
628
|
+
|
|
629
|
+
:::tip[Local-first development]
|
|
630
|
+
The `ts#dynamodb` generator vends a `dev` target that runs [DynamoDB Local](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) in a container. Combined with the `connection` generator (which we run below), this means **the entire game runs on your machine without a deployment** until the end of the tutorial.
|
|
631
|
+
:::
|
|
632
|
+
|
|
633
|
+
You will see some new files appear in your file tree.
|
|
634
|
+
<Drawer title="ts#dynamodb updated files" trigger="Click here to examine these files in more detail.">
|
|
635
|
+
The `ts#dynamodb` generator generates these files.
|
|
636
|
+
|
|
637
|
+
<FileTree>
|
|
638
|
+
- packages/
|
|
639
|
+
- dungeon-db/
|
|
640
|
+
- config.json DynamoDB configuration including port, table name, container settings and Global Secondary Indexes
|
|
641
|
+
- src/
|
|
642
|
+
- index.ts entry point and exports
|
|
643
|
+
- client.ts DynamoDB client singleton and table name resolution
|
|
644
|
+
- entities/
|
|
645
|
+
- example.ts example ElectroDB entity (we will replace this)
|
|
646
|
+
- index.ts entity exports
|
|
647
|
+
- project.json adds the `dev` and `pull-image` targets
|
|
648
|
+
- common/
|
|
649
|
+
- scripts/
|
|
650
|
+
- src/
|
|
651
|
+
- dynamodb/
|
|
652
|
+
- create-local-table.ts creates the table in DynamoDB Local
|
|
653
|
+
- pull-image.ts pulls the DynamoDB Local image
|
|
654
|
+
- start-container.ts starts the DynamoDB Local container
|
|
655
|
+
- constructs/
|
|
656
|
+
- src/
|
|
657
|
+
- app/dynamodb/
|
|
658
|
+
- dungeon-db.ts construct for provisioning your table
|
|
659
|
+
- core/
|
|
660
|
+
- dynamodb.ts generic DynamoDB table construct
|
|
661
|
+
</FileTree>
|
|
662
|
+
|
|
663
|
+
The generated `src/client.ts` exports `getDynamoDBClient()` and `resolveTableName()`. When `LOCAL_DEV=true` (set automatically by the `dev` targets) these connect to DynamoDB Local; otherwise they connect to AWS and resolve the deployed table name from <Link path="guides/runtime-config">Runtime Configuration</Link>. We'll model our `Game` and `Inventory` entities in this project in <Link path="get_started/tutorials/dungeon-game/2">Module 2</Link>.
|
|
664
|
+
|
|
665
|
+
For more details, refer to the <Link path="guides/ts-dynamodb">ts#dynamodb generator guide</Link>.
|
|
666
|
+
|
|
667
|
+
</Drawer>
|
|
668
|
+
|
|
669
|
+
## Task 6: Create the User Interface (UI)
|
|
670
|
+
|
|
671
|
+
In this task, we will create the UI which will allow you to interact with the game.
|
|
672
|
+
|
|
673
|
+
### Game UI: Website
|
|
674
|
+
|
|
675
|
+
To create the UI, create a website called `GameUI` using these steps:
|
|
676
|
+
|
|
677
|
+
<RunGenerator generator="ts#website" requiredParameters={{name:"GameUI", ux:"shadcn"}} noInteractive />
|
|
678
|
+
|
|
679
|
+
:::note[Shadcn UI]
|
|
680
|
+
We select `--ux=shadcn` so the generated website uses [shadcn/ui](https://ui.shadcn.com/) components styled with Tailwind — all our route code uses shadcn primitives like `Card`, `Button`, and `Input`, and the `connection` generator later wires a matching shadcn-themed [CopilotKit](https://docs.copilotkit.ai/) chat surface.
|
|
681
|
+
:::
|
|
682
|
+
|
|
683
|
+
You will see some new files appear in your file tree.
|
|
684
|
+
|
|
685
|
+
<Drawer title="ts#website updated files" trigger="Click here to examine these files in more detail.">
|
|
686
|
+
The `ts#website` generates these files. Let us examine some of the key files highlighted in the file tree:
|
|
687
|
+
|
|
688
|
+
<FileTree>
|
|
689
|
+
- packages/
|
|
690
|
+
- common/
|
|
691
|
+
- constructs/
|
|
692
|
+
- src/
|
|
693
|
+
- app/ app specific cdk constructs
|
|
694
|
+
- static-websites/
|
|
695
|
+
- **game-ui.ts** cdk construct to create your Game UI
|
|
696
|
+
- core/
|
|
697
|
+
- static-website.ts generic static website construct
|
|
698
|
+
- game-ui/
|
|
699
|
+
- public/
|
|
700
|
+
- src/
|
|
701
|
+
- components/
|
|
702
|
+
- AppLayout/
|
|
703
|
+
- index.tsx overall page layout using shadcn `SidebarProvider` + header
|
|
704
|
+
- app-sidebar.tsx default shadcn sidebar with nav items
|
|
705
|
+
- alert.tsx, spinner.tsx shadcn-wrapped feedback primitives
|
|
706
|
+
- routes/ @tanstack/react-router file based routes
|
|
707
|
+
- **index.tsx** root '/' page
|
|
708
|
+
- __root.tsx all pages use this component as a base
|
|
709
|
+
- config.ts
|
|
710
|
+
- **main.tsx** React entrypoint
|
|
711
|
+
- routeTree.gen.ts this is automatically updated by @tanstack/react-router
|
|
712
|
+
- styles.css imports shared shadcn globals (Tailwind v4)
|
|
713
|
+
- index.html
|
|
714
|
+
- project.json
|
|
715
|
+
- vite.config.mts
|
|
716
|
+
- ...
|
|
717
|
+
- common/
|
|
718
|
+
- shadcn/ shared shadcn/ui library (theme tokens, `Button`, `Card`, `Input`, `Sidebar`, …) imported by every `uxProvider=Shadcn` website
|
|
719
|
+
- src/components/ui/*
|
|
720
|
+
- src/styles/globals.css Tailwind + shadcn design tokens
|
|
721
|
+
- ...
|
|
722
|
+
</FileTree>
|
|
723
|
+
|
|
724
|
+
```ts
|
|
725
|
+
// packages/common/constructs/src/app/static-websites/game-ui.ts
|
|
726
|
+
import * as url from 'url';
|
|
727
|
+
import { Construct } from 'constructs';
|
|
728
|
+
import { StaticWebsite } from '../../core/index.js';
|
|
729
|
+
|
|
730
|
+
export class GameUI extends StaticWebsite {
|
|
731
|
+
constructor(scope: Construct, id: string) {
|
|
732
|
+
super(scope, id, {
|
|
733
|
+
websiteName: 'GameUI',
|
|
734
|
+
websiteFilePath: url.fileURLToPath(
|
|
735
|
+
new URL(
|
|
736
|
+
'../../../../../../dist/packages/game-ui/bundle',
|
|
737
|
+
import.meta.url,
|
|
738
|
+
),
|
|
739
|
+
),
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
This is the CDK construct that defines our GameUI. It has already configured the file path to the generated bundle for our Vite based UI. This means that at `build` time, bundling occurs within the game-ui project's build target and the output is used here.
|
|
746
|
+
|
|
747
|
+
```tsx
|
|
748
|
+
// packages/game-ui/src/main.tsx
|
|
749
|
+
import React from 'react';
|
|
750
|
+
import { createRoot } from 'react-dom/client';
|
|
751
|
+
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
|
752
|
+
import { routeTree } from './routeTree.gen';
|
|
753
|
+
import './styles.css';
|
|
754
|
+
|
|
755
|
+
export type RouterProviderContext = {};
|
|
756
|
+
|
|
757
|
+
const router = createRouter({ routeTree, context: {} });
|
|
758
|
+
|
|
759
|
+
declare module '@tanstack/react-router' {
|
|
760
|
+
interface Register {
|
|
761
|
+
router: typeof router;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const App = () => <RouterProvider router={router} context={{}} />;
|
|
766
|
+
|
|
767
|
+
const root = document.getElementById('root');
|
|
768
|
+
root &&
|
|
769
|
+
createRoot(root).render(
|
|
770
|
+
<React.StrictMode>
|
|
771
|
+
<App />
|
|
772
|
+
</React.StrictMode>,
|
|
773
|
+
);
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
This is the entry point where React is mounted. Styling comes from Tailwind v4 tokens imported via `styles.css`. `@tanstack/react-router` is configured in [file-based routing](https://tanstack.com/router/v1/docs/framework/react/routing/file-based-routing) mode: as long as the dev server is running, any file you create under `routes/` is picked up automatically and the route tree is regenerated. Later generators (auth, connection) will AST-patch this file to wrap `<App />` in additional providers.
|
|
777
|
+
|
|
778
|
+
```tsx
|
|
779
|
+
// packages/game-ui/src/routes/index.tsx
|
|
780
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
781
|
+
|
|
782
|
+
export const Route = createFileRoute('/')({
|
|
783
|
+
component: RouteComponent,
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
function RouteComponent() {
|
|
787
|
+
return (
|
|
788
|
+
<div className="text-center">
|
|
789
|
+
<header>
|
|
790
|
+
<h1>Welcome</h1>
|
|
791
|
+
<p>Welcome to your new React website!</p>
|
|
792
|
+
</header>
|
|
793
|
+
</div>
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
A component will be rendered when navigating to the `/` route. `@tanstack/react-router` will manage the `Route` for you whenever you create/move this file (as long as the dev server is running).
|
|
799
|
+
|
|
800
|
+
</Drawer>
|
|
801
|
+
|
|
802
|
+
### Game UI: Auth
|
|
803
|
+
|
|
804
|
+
Let us configure our Game UI to require authenticated access via Amazon Cognito using these steps:
|
|
805
|
+
|
|
806
|
+
<RunGenerator generator="ts#website#auth" requiredParameters={{cognitoDomain:"game-ui", project:"@dungeon-adventure/game-ui", allowSignup:true}} noInteractive />
|
|
807
|
+
|
|
808
|
+
You will see some new files appear/change in your file tree.
|
|
809
|
+
|
|
810
|
+
<Drawer title="ts#website#auth updated files" trigger="Click here to examine these files in more detail.">
|
|
811
|
+
The `ts#website#auth` generator updates/generates these files. Let us examine some of the key files highlighted in the file tree:
|
|
812
|
+
|
|
813
|
+
<FileTree>
|
|
814
|
+
- packages/
|
|
815
|
+
- common/
|
|
816
|
+
- constructs/
|
|
817
|
+
- src/
|
|
818
|
+
- core/
|
|
819
|
+
- user-identity.ts cdk construct for creating user/identity pools
|
|
820
|
+
- game-ui/
|
|
821
|
+
- src/
|
|
822
|
+
- components/
|
|
823
|
+
- AppLayout/
|
|
824
|
+
- index.tsx adds the logged in user/logout to the header
|
|
825
|
+
- CognitoAuth/
|
|
826
|
+
- index.tsx manages logging into Cognito
|
|
827
|
+
- RuntimeConfig/
|
|
828
|
+
- index.tsx fetches the `runtime-config.json` and provides it to children via context
|
|
829
|
+
- hooks/
|
|
830
|
+
- useRuntimeConfig.tsx
|
|
831
|
+
- **main.tsx** Updated to add Cognito
|
|
832
|
+
</FileTree>
|
|
833
|
+
|
|
834
|
+
```diff lang="tsx"
|
|
835
|
+
// packages/game-ui/src/main.tsx
|
|
836
|
+
+import { useAuth } from 'react-oidc-context';
|
|
837
|
+
+import CognitoAuth from './components/CognitoAuth';
|
|
838
|
+
+import { useRuntimeConfig } from './hooks/useRuntimeConfig';
|
|
839
|
+
+import RuntimeConfigProvider from './components/RuntimeConfig';
|
|
840
|
+
import React from 'react';
|
|
841
|
+
import { createRoot } from 'react-dom/client';
|
|
842
|
+
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
|
843
|
+
import { routeTree } from './routeTree.gen';
|
|
844
|
+
import './styles.css';
|
|
845
|
+
-export type RouterProviderContext = {};
|
|
846
|
+
+export type RouterProviderContext = {
|
|
847
|
+
+ runtimeConfig?: ReturnType<typeof useRuntimeConfig>;
|
|
848
|
+
+ auth?: ReturnType<typeof useAuth>;
|
|
849
|
+
+};
|
|
850
|
+
-const router = createRouter({ routeTree, context: {} });
|
|
851
|
+
+const router = createRouter({
|
|
852
|
+
+ routeTree,
|
|
853
|
+
+ context: { runtimeConfig: undefined, auth: undefined },
|
|
854
|
+
+});
|
|
855
|
+
// Register the router instance for type safety
|
|
856
|
+
declare module '@tanstack/react-router' {
|
|
857
|
+
interface Register {
|
|
858
|
+
router: typeof router;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
-const App = () => <RouterProvider router={router} context={{}} />;
|
|
862
|
+
+const App = () => {
|
|
863
|
+
+ const auth = useAuth();
|
|
864
|
+
+ const runtimeConfig = useRuntimeConfig();
|
|
865
|
+
+ return <RouterProvider router={router} context={{ runtimeConfig, auth }} />;
|
|
866
|
+
+};
|
|
867
|
+
const root = document.getElementById('root');
|
|
868
|
+
root &&
|
|
869
|
+
createRoot(root).render(
|
|
870
|
+
<React.StrictMode>
|
|
871
|
+
+ <RuntimeConfigProvider>
|
|
872
|
+
+ <CognitoAuth>
|
|
873
|
+
<App />
|
|
874
|
+
+ </CognitoAuth>
|
|
875
|
+
+ </RuntimeConfigProvider>
|
|
876
|
+
</React.StrictMode>,
|
|
877
|
+
);
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
The `RuntimeConfigProvider` and `CognitoAuth` components have been added to the `main.tsx` file via an AST transform. This allows the `CognitoAuth` component to authenticate with Amazon Cognito by fetching the `runtime-config.json` which contains the required cognito connection configuration in order to make the backend calls to the correct destination.
|
|
881
|
+
|
|
882
|
+
</Drawer>
|
|
883
|
+
|
|
884
|
+
### Game UI: Connect to Game API
|
|
885
|
+
|
|
886
|
+
Let us configure our Game UI to connect to our previously created Game API.
|
|
887
|
+
|
|
888
|
+
<RunGenerator generator="connection" requiredParameters={{sourceProject:"@dungeon-adventure/game-ui", targetProject:"@dungeon-adventure/game-api"}} noInteractive />
|
|
889
|
+
|
|
890
|
+
You will see some new files have appear/change in your file tree.
|
|
891
|
+
|
|
892
|
+
<Drawer title="UI -> tRPC connection updated files" trigger="Click here to examine these files in more detail.">
|
|
893
|
+
The `connection` generator generates/updates these files. Let us examine some of the key files highlighted in the file tree:
|
|
894
|
+
|
|
895
|
+
<FileTree>
|
|
896
|
+
- packages/
|
|
897
|
+
- game-ui/
|
|
898
|
+
- src/
|
|
899
|
+
- components/
|
|
900
|
+
- GameApiClientProvider.tsx sets up the GameAPI client
|
|
901
|
+
- hooks/
|
|
902
|
+
- **useGameApi.tsx** hooks to call the GameApi
|
|
903
|
+
- **main.tsx** injects the trpc client providers
|
|
904
|
+
- package.json
|
|
905
|
+
|
|
906
|
+
</FileTree>
|
|
907
|
+
|
|
908
|
+
```tsx
|
|
909
|
+
// packages/game-ui/src/hooks/useGameApi.tsx
|
|
910
|
+
import { useContext } from 'react';
|
|
911
|
+
import { GameApiTRPCContext } from '../components/GameApiClientProvider';
|
|
912
|
+
|
|
913
|
+
export const useGameApi = () => {
|
|
914
|
+
const container = useContext(GameApiTRPCContext);
|
|
915
|
+
if (!container) {
|
|
916
|
+
throw new Error('useGameApi must be used within GameApiClientProvider');
|
|
917
|
+
}
|
|
918
|
+
return container.optionsProxy;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
export const useGameApiClient = () => {
|
|
922
|
+
const container = useContext(GameApiTRPCContext);
|
|
923
|
+
if (!container) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
'useGameApiClient must be used within GameApiClientProvider',
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
return container.client;
|
|
929
|
+
};
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
This hook provides access to the tRPC client for calling the GameApi. For examples on how to call tRPC APIs, refer to the <Link path="guides/connection/react-trpc#using-the-generated-code">using the tRPC hook guide</Link>.
|
|
933
|
+
|
|
934
|
+
<Aside title="Type-Safe Hooks">
|
|
935
|
+
Thanks to tRPC's [Typescript inference](https://trpc.io/docs/concepts), `useGameApi` doesn't need a build step for backend changes to reach the frontend — edits to the router or any procedure are picked up by the type checker instantly.
|
|
936
|
+
</Aside>
|
|
937
|
+
|
|
938
|
+
```diff lang="tsx"
|
|
939
|
+
// packages/game-ui/src/main.tsx
|
|
940
|
+
+import GameApiClientProvider from './components/GameApiClientProvider';
|
|
941
|
+
+import QueryClientProvider from './components/QueryClientProvider';
|
|
942
|
+
import { useAuth } from 'react-oidc-context';
|
|
943
|
+
import CognitoAuth from './components/CognitoAuth';
|
|
944
|
+
import { useRuntimeConfig } from './hooks/useRuntimeConfig';
|
|
945
|
+
import RuntimeConfigProvider from './components/RuntimeConfig';
|
|
946
|
+
import React from 'react';
|
|
947
|
+
import { createRoot } from 'react-dom/client';
|
|
948
|
+
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
|
949
|
+
import { routeTree } from './routeTree.gen';
|
|
950
|
+
import './styles.css';
|
|
951
|
+
...
|
|
952
|
+
const root = document.getElementById('root');
|
|
953
|
+
root &&
|
|
954
|
+
createRoot(root).render(
|
|
955
|
+
<React.StrictMode>
|
|
956
|
+
<RuntimeConfigProvider>
|
|
957
|
+
<CognitoAuth>
|
|
958
|
+
+ <QueryClientProvider>
|
|
959
|
+
+ <GameApiClientProvider>
|
|
960
|
+
<App />
|
|
961
|
+
+ </GameApiClientProvider>
|
|
962
|
+
+ </QueryClientProvider>
|
|
963
|
+
</CognitoAuth>
|
|
964
|
+
</RuntimeConfigProvider>
|
|
965
|
+
</React.StrictMode>,
|
|
966
|
+
);
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
The `main.tsx` file has been updated via an AST transform to inject the tRPC providers.
|
|
970
|
+
|
|
971
|
+
</Drawer>
|
|
972
|
+
|
|
973
|
+
### Story Agent: Connect to Inventory MCP Server
|
|
974
|
+
|
|
975
|
+
Let us connect our Story Agent to the Inventory MCP server so the agent can discover and invoke the MCP server's tools.
|
|
976
|
+
|
|
977
|
+
<RunGenerator generator="connection" requiredParameters={{sourceProject:"story", targetProject:"inventory"}} noInteractive />
|
|
978
|
+
|
|
979
|
+
<Drawer title="Story Agent -> Inventory MCP connection updated files" trigger="Click here to examine these files in more detail.">
|
|
980
|
+
The `connection` generator generates/updates these files:
|
|
981
|
+
|
|
982
|
+
<FileTree>
|
|
983
|
+
- packages/
|
|
984
|
+
- common/
|
|
985
|
+
- agent\_connection/
|
|
986
|
+
- dungeon\_adventure\_agent\_connection/
|
|
987
|
+
- core/
|
|
988
|
+
- **agentcore\_endpoints.py** Framework-agnostic ARN/URL resolution
|
|
989
|
+
- **agentcore\_mcp\_transport.py** Framework-agnostic MCP transport
|
|
990
|
+
- **agentcore\_mcp\_client\_strands.py** Strands MCP client wrapping the transport
|
|
991
|
+
- auth/ Framework-agnostic SigV4 / session-forwarding `httpx.Auth`
|
|
992
|
+
- app/
|
|
993
|
+
- **inventory\_mcp\_server\_client\_strands.py** Strands client for connecting to the Inventory MCP server
|
|
994
|
+
- **\_\_init\_\_.py** Re-exports per-connection clients
|
|
995
|
+
- story/
|
|
996
|
+
- dungeon\_adventure\_story/agent/
|
|
997
|
+
- **agent.py** Modified to import and use the MCP client
|
|
998
|
+
|
|
999
|
+
</FileTree>
|
|
1000
|
+
|
|
1001
|
+
The generator:
|
|
1002
|
+
- Creates a shared `agent_connection` Python project (if it doesn't already exist) with the core `AgentCoreMCPClientStrands`
|
|
1003
|
+
- Generates an `InventoryMcpServerClientStrands` class that handles connecting to the MCP server both locally (direct HTTP) and when deployed (via AgentCore with IAM auth)
|
|
1004
|
+
- Transforms `agent.py` to import the client, create an instance, and wire the MCP server's tools into the agent
|
|
1005
|
+
- Adds the `agent_connection` project as a workspace dependency of the story project
|
|
1006
|
+
- Updates the `dev` target to automatically start the MCP server when running locally
|
|
1007
|
+
|
|
1008
|
+
For more details, refer to the <Link path="guides/connection/py-agent-mcp">Python Agent to MCP connection guide</Link>.
|
|
1009
|
+
</Drawer>
|
|
1010
|
+
|
|
1011
|
+
### Game UI: Connect to Story Agent
|
|
1012
|
+
|
|
1013
|
+
Let us connect our Game UI to the Story Agent. Since the agent speaks AG-UI, the `connection` generator wires up [CopilotKit](https://docs.copilotkit.ai/): a themed chat component and an `@ag-ui/client` `HttpAgent` ready to render.
|
|
1014
|
+
|
|
1015
|
+
<RunGenerator generator="connection" requiredParameters={{sourceProject:"@dungeon-adventure/game-ui", targetProject:"story"}} noInteractive />
|
|
1016
|
+
|
|
1017
|
+
<Drawer title="UI -> Story Agent connection updated files" trigger="Click here to examine these files in more detail.">
|
|
1018
|
+
The `connection` generator generates/updates these files:
|
|
1019
|
+
|
|
1020
|
+
<FileTree>
|
|
1021
|
+
- packages/
|
|
1022
|
+
- game-ui/
|
|
1023
|
+
- src/
|
|
1024
|
+
- components/
|
|
1025
|
+
- **AguiProvider.tsx** `CopilotKitProvider` with every connected AG-UI agent registered
|
|
1026
|
+
- copilot/
|
|
1027
|
+
- **index.tsx** Shadcn-themed `CopilotChat` / `CopilotSidebar` / `CopilotPopup`
|
|
1028
|
+
- ShadcnAssistantMessage.tsx, ShadcnUserMessage.tsx, ShadcnChatInput.tsx, ShadcnCursor.tsx, copilot.css
|
|
1029
|
+
- hooks/
|
|
1030
|
+
- **useAguiStoryAgent.tsx** Builds an `HttpAgent`, injects the Cognito bearer token, and pads `threadId` to AgentCore's 33-char session id
|
|
1031
|
+
- **main.tsx** Wraps `<App />` in `<AguiProvider>`
|
|
1032
|
+
|
|
1033
|
+
</FileTree>
|
|
1034
|
+
|
|
1035
|
+
The generator:
|
|
1036
|
+
- Detects the React website's `uxProvider` (Shadcn here) and vends matching chat components.
|
|
1037
|
+
- Registers every connected agent on a single `CopilotKitProvider` — re-running for another agent just adds another hook.
|
|
1038
|
+
- Reads the agent's runtime ARN from Runtime Configuration, builds the AgentCore invocation URL, and attaches the Cognito bearer token plus the AgentCore session id header.
|
|
1039
|
+
|
|
1040
|
+
For more details, refer to the <Link path="guides/connection/react-agui">React to AG-UI connection guide</Link>.
|
|
1041
|
+
</Drawer>
|
|
1042
|
+
|
|
1043
|
+
### Connect the Game API and Inventory MCP server to the database
|
|
1044
|
+
|
|
1045
|
+
Both the Game API and the Inventory MCP server read and write our DynamoDB table, so let us connect them to the `DungeonDb` project. The `connection` generator detects that the target is a `ts#dynamodb` project and wires each source project's `dev` target to start DynamoDB Local automatically.
|
|
1046
|
+
|
|
1047
|
+
<RunGenerator generator="connection" requiredParameters={{sourceProject:"@dungeon-adventure/game-api", targetProject:"@dungeon-adventure/dungeon-db"}} noInteractive />
|
|
1048
|
+
|
|
1049
|
+
<RunGenerator generator="connection" requiredParameters={{sourceProject:"@dungeon-adventure/inventory", targetProject:"@dungeon-adventure/dungeon-db"}} noInteractive />
|
|
1050
|
+
|
|
1051
|
+
<Aside type="tip" title="One command boots the whole stack locally">
|
|
1052
|
+
Because the Story Agent's `agent-dev` already depends on the Inventory MCP server's `mcp-server-dev` (via the `story → inventory` connection above), and both the Game API and MCP server now depend on `dungeon-db:dev`, running any one project's `dev` target starts every dependency it needs in the right order.
|
|
1053
|
+
</Aside>
|
|
1054
|
+
|
|
1055
|
+
### Game UI: Infrastructure
|
|
1056
|
+
|
|
1057
|
+
Let us create the final sub-project for the CDK infrastructure.
|
|
1058
|
+
|
|
1059
|
+
<RunGenerator generator="ts#infra" requiredParameters={{name:"infra"}} noInteractive />
|
|
1060
|
+
|
|
1061
|
+
You will see some new files have appear/change in your file tree.
|
|
1062
|
+
|
|
1063
|
+
<Drawer title="ts#infra updated files" trigger="Click here to examine these files in more detail.">
|
|
1064
|
+
The `ts#infra` generator generates/updates these. Let us examine some of the key files highlighted in the file tree:
|
|
1065
|
+
|
|
1066
|
+
<FileTree>
|
|
1067
|
+
- packages/
|
|
1068
|
+
- common/
|
|
1069
|
+
- constructs/
|
|
1070
|
+
- src/
|
|
1071
|
+
- core/
|
|
1072
|
+
- checkov.ts
|
|
1073
|
+
- index.ts
|
|
1074
|
+
- infra
|
|
1075
|
+
- src/
|
|
1076
|
+
- stages/
|
|
1077
|
+
- **application-stage.ts** cdk stacks defined here
|
|
1078
|
+
- stacks/
|
|
1079
|
+
- **application-stack.ts** cdk resources defined here
|
|
1080
|
+
- **main.ts** entrypoint which defines all stages
|
|
1081
|
+
- cdk.json
|
|
1082
|
+
- checkov.yml
|
|
1083
|
+
- project.json
|
|
1084
|
+
- ...
|
|
1085
|
+
- package.json
|
|
1086
|
+
- tsconfig.json add references
|
|
1087
|
+
- tsconfig.base.json add alias
|
|
1088
|
+
|
|
1089
|
+
</FileTree>
|
|
1090
|
+
|
|
1091
|
+
```ts
|
|
1092
|
+
// packages/infra/src/main.ts
|
|
1093
|
+
import { ApplicationStage } from './stages/application-stage.js';
|
|
1094
|
+
import { App } from ':dungeon-adventure/common-constructs';
|
|
1095
|
+
|
|
1096
|
+
const app = new App();
|
|
1097
|
+
|
|
1098
|
+
// Use this to deploy your own sandbox environment (assumes your CLI credentials)
|
|
1099
|
+
new ApplicationStage(app, 'dungeon-adventure-infra-sandbox', {
|
|
1100
|
+
env: {
|
|
1101
|
+
account: process.env.CDK_DEFAULT_ACCOUNT,
|
|
1102
|
+
region: process.env.CDK_DEFAULT_REGION,
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
app.synth();
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
<Aside type="tip" title="Fixing Import Errors">If you see an import error within your IDE, this is because our infrastructure project does not have a typescript reference set up yet in the `tsconfig.json`. Nx has been [configured](https://nx.dev/nx-api/js/generators/typescript-sync) to create these references *dynamically* whenever a build/compile is run or if you run the `nx sync` command manually. For more information refer to the <Link path="guides/typescript-project#importing-your-library-code-in-other-projects">Typescript guide</Link>.</Aside>
|
|
1110
|
+
|
|
1111
|
+
This is the entry point for your CDK application.
|
|
1112
|
+
|
|
1113
|
+
```ts
|
|
1114
|
+
// packages/infra/src/stacks/application-stack.ts
|
|
1115
|
+
import { Stack, StackProps } from 'aws-cdk-lib';
|
|
1116
|
+
import { Construct } from 'constructs';
|
|
1117
|
+
|
|
1118
|
+
export class ApplicationStack extends Stack {
|
|
1119
|
+
constructor(scope: Construct, id: string, props?: StackProps) {
|
|
1120
|
+
super(scope, id, props);
|
|
1121
|
+
|
|
1122
|
+
// The code that defines your stack goes here
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
Let us instantiate our CDK constructs to build our dungeon adventure game.
|
|
1128
|
+
|
|
1129
|
+
</Drawer>
|
|
1130
|
+
|
|
1131
|
+
## Task 7: Update our infrastructure
|
|
1132
|
+
|
|
1133
|
+
Let's update `packages/infra/src/stacks/application-stack.ts` to instantiate some of our generated constructs:
|
|
1134
|
+
|
|
1135
|
+
<E2EDiff before="dungeon-adventure/1/application-stack.ts.original.template" after="dungeon-adventure/1/application-stack.ts.template" lang="ts" />
|
|
1136
|
+
|
|
1137
|
+
:::note[Default Integrations]
|
|
1138
|
+
We supply default integrations for our Game API. By default, each operation in our API is mapped to an individual Lambda function to handle that operation.
|
|
1139
|
+
:::
|
|
1140
|
+
|
|
1141
|
+
## Task 8: Build the code
|
|
1142
|
+
|
|
1143
|
+
<Drawer title="Nx commands" trigger="Now it's time for us to build our code for the first time">
|
|
1144
|
+
|
|
1145
|
+
###### Single vs Multiple targets
|
|
1146
|
+
|
|
1147
|
+
The `run-many` command will run a target on multiple listed subprojects (`--all` will target them all). This ensures dependencies are executed in the correct order.
|
|
1148
|
+
|
|
1149
|
+
You can also trigger a build (or any other task) for a single project target by running the target on the project directly. For example, to build the `@dungeon-adventure/infra` project, run the following command:
|
|
1150
|
+
|
|
1151
|
+
<NxCommands commands={['build infra']} />
|
|
1152
|
+
|
|
1153
|
+
You can also omit the scope, and use the Nx shorthand syntax if you prefer:
|
|
1154
|
+
|
|
1155
|
+
<NxCommands commands={['build infra']} />
|
|
1156
|
+
|
|
1157
|
+
###### Visualizing your dependencies
|
|
1158
|
+
|
|
1159
|
+
To visualize your dependencies, run:
|
|
1160
|
+
|
|
1161
|
+
<NxCommands commands={['graph']} />
|
|
1162
|
+
<br/>
|
|
1163
|
+
|
|
1164
|
+
<Image src={nxGraphPng} alt="nx-graph.png" width="800" height="600" />
|
|
1165
|
+
|
|
1166
|
+
###### Caching
|
|
1167
|
+
|
|
1168
|
+
Nx relies on [caching](https://nx.dev/concepts/how-caching-works) so that you can re-use artifacts from previous builds in order to speed up development. There is some configuration required to get this to work correctly and there may be cases where you want to perform a build **without using the cache**. To do that, simply append the `--skip-nx-cache` argument to your command. For example:
|
|
1169
|
+
|
|
1170
|
+
<NxCommands commands={['build infra --skip-nx-cache']} />
|
|
1171
|
+
If for whatever reason you ever wanted to clear your cache (stored in the `.nx` folder), you can run the following command:
|
|
1172
|
+
|
|
1173
|
+
<NxCommands commands={['reset']} />
|
|
1174
|
+
|
|
1175
|
+
</Drawer>
|
|
1176
|
+
|
|
1177
|
+
Using the command line, run the following command to fix any lint issues first:
|
|
1178
|
+
|
|
1179
|
+
<PackageManagerShortCommand commands={["lint"]} />
|
|
1180
|
+
|
|
1181
|
+
Then, run the following command for a full build:
|
|
1182
|
+
|
|
1183
|
+
<PackageManagerShortCommand commands={["build"]} />
|
|
1184
|
+
|
|
1185
|
+
You will be prompted with the following:
|
|
1186
|
+
|
|
1187
|
+
```bash
|
|
1188
|
+
NX The workspace is out of sync
|
|
1189
|
+
|
|
1190
|
+
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
|
|
1191
|
+
|
|
1192
|
+
This will result in an error in CI.
|
|
1193
|
+
|
|
1194
|
+
? Would you like to sync the identified changes to get your workspace up to date? …
|
|
1195
|
+
Yes, sync the changes and run the tasks
|
|
1196
|
+
No, run the tasks without syncing the changes
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
This message indicates that NX has detected some files which can be updated automatically for you. In this case, it is referring to the `tsconfig.json` files which do not have Typescript references set up on references projects.
|
|
1200
|
+
|
|
1201
|
+
Select the **Yes, sync the changes and run the tasks** option to proceed. You should notice all of you IDE related import errors get automatically resolved as the sync generator will add the missing typescript references automatically!
|
|
1202
|
+
|
|
1203
|
+
All built artifacts are now available within the `dist/ folder` located at the root of the monorepo. This is a standard pactice when using projects generated by the `@aws/nx-plugin` as it does not pollute your file-tree with generated files. In the event you want to clean your files, delete the `dist/` folder without worrying about build artifacts being littered throughout the file tree.
|
|
1204
|
+
|
|
1205
|
+
Congratulations! You've created all of the required sub-projects required to start implementing the core of our AI Dungeon Adventure game. 🎉🎉🎉
|