@aws/nx-plugin-mcp 1.0.0-rc.2 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/bin/aws-nx-mcp.js +5919 -4776
  2. package/docs/guides/agentcore-gateway.mdx +240 -0
  3. package/docs/guides/connection/agentcore-gateway-gateway.mdx +154 -0
  4. package/docs/guides/connection/agentcore-gateway-mcp.mdx +134 -0
  5. package/docs/guides/connection/py-agent-a2a.mdx +8 -5
  6. package/docs/guides/connection/py-agent-dynamodb.mdx +116 -0
  7. package/docs/guides/connection/py-agent-gateway.mdx +161 -0
  8. package/docs/guides/connection/py-agent-mcp.mdx +7 -4
  9. package/docs/guides/connection/py-fast-api-dynamodb.mdx +56 -0
  10. package/docs/guides/connection/py-mcp-server-dynamodb.mdx +116 -0
  11. package/docs/guides/connection/react-smithy.mdx +1 -1
  12. package/docs/guides/connection/react-trpc.mdx +1 -1
  13. package/docs/guides/connection/smithy-dynamodb.mdx +68 -0
  14. package/docs/guides/connection/smithy-rdb.mdx +1 -1
  15. package/docs/guides/connection/trpc-dynamodb.mdx +62 -0
  16. package/docs/guides/connection/trpc-rdb.mdx +1 -1
  17. package/docs/guides/connection/ts-agent-a2a.mdx +8 -5
  18. package/docs/guides/connection/ts-agent-dynamodb.mdx +128 -0
  19. package/docs/guides/connection/ts-agent-gateway.mdx +152 -0
  20. package/docs/guides/connection/ts-agent-mcp.mdx +7 -4
  21. package/docs/guides/connection/ts-mcp-server-dynamodb.mdx +125 -0
  22. package/docs/guides/connection.mdx +89 -0
  23. package/docs/guides/docker-bundling.mdx +13 -7
  24. package/docs/guides/fastapi.mdx +213 -3
  25. package/docs/guides/license.mdx +264 -109
  26. package/docs/guides/local-development.mdx +76 -0
  27. package/docs/guides/nx-generator.mdx +5 -0
  28. package/docs/guides/py-agent.mdx +42 -3
  29. package/docs/guides/py-dynamodb.mdx +449 -0
  30. package/docs/guides/py-mcp-server.mdx +5 -1
  31. package/docs/guides/react-website-auth.mdx +15 -0
  32. package/docs/guides/react-website.mdx +12 -4
  33. package/docs/guides/trpc.mdx +8 -8
  34. package/docs/guides/ts-agent.mdx +40 -2
  35. package/docs/guides/ts-dynamodb.mdx +158 -0
  36. package/docs/guides/ts-mcp-server.mdx +5 -1
  37. package/docs/guides/ts-rdb.mdx +58 -10
  38. package/docs/guides/ts-smithy-api.mdx +148 -3
  39. package/docs/guides/typescript-project.mdx +5 -10
  40. package/docs/guides/workspace.mdx +8 -2
  41. package/docs/snippets/api/type-safe-api-integrations.mdx +31 -0
  42. package/docs/snippets/api/waf-configuration.mdx +1 -1
  43. package/docs/snippets/connection/dynamodb-local-development.mdx +7 -0
  44. package/docs/snippets/connection/lambda-dynamodb-access.mdx +80 -0
  45. package/docs/snippets/connection/py-dynamodb-local-development.mdx +7 -0
  46. package/docs/snippets/dynamodb/deploying-table.mdx +171 -0
  47. package/docs/snippets/dynamodb/gsi-config.mdx +38 -0
  48. package/docs/snippets/dynamodb/infrastructure.mdx +33 -0
  49. package/docs/snippets/dynamodb/serve-local-start.mdx +13 -0
  50. package/docs/snippets/dynamodb/serve-local-windows.mdx +15 -0
  51. package/docs/snippets/mcp/config.mdx +1 -1
  52. package/docs/snippets/required-prerequisites.mdx +1 -1
  53. package/generators.json +100 -1
  54. package/package.json +1 -1
  55. package/src/agentcore-gateway/gateway-connection/schema.json +26 -0
  56. package/src/agentcore-gateway/mcp-connection/schema.json +26 -0
  57. package/src/agentcore-gateway/schema.json +65 -0
  58. package/src/license/schema.json +6 -0
  59. package/src/preset/schema.json +5 -0
  60. package/src/py/agent/gateway-connection/schema.json +26 -0
  61. package/src/py/dynamodb/agent-connection/schema.json +22 -0
  62. package/src/py/dynamodb/fast-api-connection/schema.json +18 -0
  63. package/src/py/dynamodb/mcp-server-connection/schema.json +22 -0
  64. package/src/py/dynamodb/schema.json +70 -0
  65. package/src/ts/agent/gateway-connection/schema.json +26 -0
  66. package/src/ts/dynamodb/agent-connection/schema.json +22 -0
  67. package/src/ts/dynamodb/mcp-server-connection/schema.json +22 -0
  68. package/src/ts/dynamodb/schema.json +70 -0
  69. package/src/ts/dynamodb/smithy-connection/schema.json +18 -0
  70. package/src/ts/dynamodb/trpc-connection/schema.json +18 -0
@@ -0,0 +1,449 @@
1
+ ---
2
+ title: py#dynamodb
3
+ description: Create a Python DynamoDB project
4
+ generator: py#dynamodb
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 Snippet from '@components/snippet.astro';
12
+
13
+ This generator creates a new Python project backed by [Amazon DynamoDB](https://aws.amazon.com/dynamodb/), using [PynamoDB](https://pynamodb.readthedocs.io/) for entity modelling. It generates the application code and infrastructure needed to provision and manage a DynamoDB table using AWS CDK or Terraform, with single-table design support and built-in local development via DynamoDB Local.
14
+
15
+ ## Usage
16
+
17
+ ### Generate a DynamoDB Project
18
+
19
+ <RunGenerator generator="py#dynamodb" />
20
+
21
+ ### Options
22
+
23
+ <GeneratorParameters generator="py#dynamodb" />
24
+
25
+ ## Generator Output
26
+
27
+ The generator creates the following project structure in the `<directory>/<name>` directory:
28
+
29
+ <FileTree>
30
+ - \<name>
31
+ - \_\_init\_\_.py Package exports
32
+ - client.py DynamoDB client and table name resolution
33
+ - entities
34
+ - base.py Base PynamoDB model with GSI declarations
35
+ - example.py Example entity definition
36
+ - \_\_init\_\_.py Entity exports
37
+ - config.json Table configuration including GSI definitions and local development settings
38
+ - project.json Project configuration and build targets
39
+ </FileTree>
40
+
41
+ The local development scripts are shared across all DynamoDB projects (both TypeScript and Python) and generated once into:
42
+
43
+ <FileTree>
44
+ - packages/common/scripts/src/dynamodb
45
+ - create-local-table.ts Creates the DynamoDB table in the local DynamoDB Local instance
46
+ - pull-image.ts Pulls the DynamoDB Local image
47
+ - start-container.ts Starts the DynamoDB Local container
48
+ </FileTree>
49
+
50
+ ### Infrastructure
51
+
52
+ <Snippet name="dynamodb/infrastructure" />
53
+
54
+ ## Local Development
55
+
56
+ ### Starting Local DynamoDB
57
+
58
+ <Snippet name="dynamodb/serve-local-start" />
59
+
60
+ ### Data Modelling
61
+
62
+ The generated project uses [PynamoDB](https://pynamodb.readthedocs.io/) for entity modelling. All entities **must** inherit from the generated `BaseModel` — it resolves the correct DynamoDB table name at runtime, reading from AWS AppConfig when deployed or from `config.json` when running locally via DynamoDB Local. Without this, PynamoDB will not know which table to use. `BaseModel` also uses [PynamoDB's polymorphism support](https://pynamodb.readthedocs.io/en/stable/polymorphism.html) to store multiple entity types in a single table, following [DynamoDB's single-table design](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/data-modeling-foundations.html).
63
+
64
+ Add or update entity files under `<name>/entities/`, using the generated example entity as a starting point:
65
+
66
+ ```python title="packages/my_table/my_table/entities/example.py"
67
+ from collections.abc import Iterator
68
+ from datetime import UTC, datetime
69
+ from pynamodb.attributes import UnicodeAttribute
70
+ from .base import BaseModel
71
+
72
+
73
+ class ExampleModel(BaseModel, discriminator='ExampleModel'):
74
+ """
75
+ Key design:
76
+ pk=EXAMPLE#<id>, sk=EXAMPLE#<id>
77
+ gsi1pk=CATEGORY#<cat>, gsi1sk=EXAMPLE#<id> <- list items by category
78
+ gsi2pk=EXAMPLE, gsi2sk=<created_at> <- list all items by date
79
+ """
80
+
81
+ name = UnicodeAttribute()
82
+ category = UnicodeAttribute()
83
+ created_at = UnicodeAttribute()
84
+ updated_at = UnicodeAttribute()
85
+
86
+ @classmethod
87
+ def make_pk(cls, id: str) -> str:
88
+ return f'EXAMPLE#{id}'
89
+
90
+ @classmethod
91
+ def create(cls, id: str, name: str, category: str) -> 'ExampleModel':
92
+ now = datetime.now(UTC).isoformat()
93
+ item = cls(
94
+ pk=cls.make_pk(id),
95
+ sk=cls.make_pk(id),
96
+ gsi1pk=f'CATEGORY#{category}',
97
+ gsi1sk=cls.make_pk(id),
98
+ gsi2pk='EXAMPLE',
99
+ gsi2sk=now,
100
+ name=name,
101
+ category=category,
102
+ created_at=now,
103
+ updated_at=now,
104
+ )
105
+ item.save()
106
+ return item
107
+
108
+ # ── Primary index ─────────────────────────────────────────────────────────
109
+ @classmethod
110
+ def get_by_id(cls, id: str) -> 'ExampleModel':
111
+ return cls.get(cls.make_pk(id), cls.make_pk(id))
112
+
113
+ # ── gsi1_index: partition=category, sort=id ───────────────────────────────
114
+ @classmethod
115
+ def list_by_category(cls, category: str) -> Iterator['ExampleModel']:
116
+ return cls.gsi1_index.query(f'CATEGORY#{category}')
117
+
118
+ # ── gsi2_index: partition=type, sort=created_at ───────────────────────────
119
+ @classmethod
120
+ def list_created_between(cls, start: datetime, end: datetime) -> Iterator['ExampleModel']:
121
+ return cls.gsi2_index.query(
122
+ 'EXAMPLE',
123
+ range_key_condition=ExampleModel.gsi2sk.between(
124
+ start.isoformat(), end.isoformat(),
125
+ ),
126
+ scan_index_forward=False,
127
+ )
128
+ ```
129
+
130
+ For more details, see the [PynamoDB tutorial](https://pynamodb.readthedocs.io/en/stable/tutorial.html).
131
+
132
+ #### Designing Around Access Patterns
133
+
134
+ In DynamoDB, schema design starts with your queries, not your data shape. Before writing any model, list every access pattern your application needs, then design `pk`, `sk`, and GSI key values so each pattern is answered by a single table request — no JOINs, no sequential reads.
135
+
136
+ The generated `ExampleModel` demonstrates this for three patterns:
137
+
138
+ - **Get by ID** — primary index, `pk=EXAMPLE#<id>`, `sk=EXAMPLE#<id>`
139
+ - **List by category** — `gsi1`, `pk=CATEGORY#<category>`
140
+ - **List by creation date** — `gsi2`, `pk=EXAMPLE`, sort key between ISO timestamps
141
+
142
+ The **type prefix** convention (e.g. `EXAMPLE#`, `CATEGORY#`) is deliberate: it makes items self-describing when browsing the table, prevents accidental key collisions between entity types that share an index, and allows sort key prefix filtering using `begins_with`.
143
+
144
+ Before writing a new entity, define its key patterns upfront in a docstring. The `OrderModel` in the next section follows this convention:
145
+
146
+ ```python
147
+ class OrderModel(BaseModel, discriminator='OrderModel'):
148
+ """
149
+ Key design:
150
+ pk=ORDER#<order_id>, sk=ORDER#<order_id>
151
+ gsi1pk=USER#<user_id>, gsi1sk=ORDER#<order_id> <- list orders for a user
152
+ gsi2pk=ORDER, gsi2sk=<created_at> <- list all orders by date
153
+ """
154
+ ```
155
+
156
+ #### Storing Multiple Entity Types
157
+
158
+ PynamoDB's `DiscriminatorAttribute` stores a type label (`entity_type`) in every item. When querying via `BaseModel`, this label is used to instantiate each result as its correct subclass automatically — so a single query can return a mix of `UserModel`, `OrderModel`, and any other entity type registered in the same table.
159
+
160
+ :::caution[PynamoDB is not purpose-built for single-table design]
161
+ PynamoDB's `DiscriminatorAttribute` was designed for class hierarchy polymorphism within a single homogeneous table — not for storing structurally different entity types in one table. The pattern used here works, but keep these caveats in mind:
162
+
163
+ - **Key collision is your responsibility.** PynamoDB does not prevent two entity types from writing the same `pk`/`sk` pair. Always use unique type prefixes (e.g. `ORDER#`, `USER#`).
164
+ - **Cross-entity queries require `BaseModel`.** PynamoDB automatically adds a discriminator filter when querying a specific subclass, so items of other entity types at the same key will never be returned. To retrieve a mix of types from a shared GSI partition key, always query via `BaseModel`.
165
+ - **Shared table settings.** All entities share the `Meta` configuration (table name, region, credentials) defined in `BaseModel`.
166
+ :::
167
+
168
+ Below is a complete two-entity example — a `UserModel` with associated `OrderModel` records stored in the same table:
169
+
170
+ ```python title="packages/my_table/my_table/entities/user.py"
171
+ from collections.abc import Iterator
172
+ from datetime import UTC, datetime
173
+ from pynamodb.attributes import UnicodeAttribute
174
+ from .base import BaseModel
175
+
176
+
177
+ class UserModel(BaseModel, discriminator='UserModel'):
178
+ """
179
+ Key design:
180
+ pk=USER#<user_id>, sk=USER#<user_id>
181
+ gsi2pk=USER, gsi2sk=<created_at> <- list all users by date
182
+ """
183
+
184
+ username = UnicodeAttribute()
185
+ email = UnicodeAttribute()
186
+ created_at = UnicodeAttribute()
187
+
188
+ @classmethod
189
+ def make_pk(cls, user_id: str) -> str:
190
+ return f'USER#{user_id}'
191
+
192
+ @classmethod
193
+ def create(cls, user_id: str, username: str, email: str) -> 'UserModel':
194
+ now = datetime.now(UTC).isoformat()
195
+ item = cls(
196
+ pk=cls.make_pk(user_id),
197
+ sk=cls.make_pk(user_id),
198
+ gsi2pk='USER',
199
+ gsi2sk=now,
200
+ username=username,
201
+ email=email,
202
+ created_at=now,
203
+ )
204
+ item.save()
205
+ return item
206
+
207
+ @classmethod
208
+ def get_by_id(cls, user_id: str) -> 'UserModel':
209
+ return cls.get(cls.make_pk(user_id), cls.make_pk(user_id))
210
+
211
+ @classmethod
212
+ def list_recent(cls, limit: int | None = None) -> Iterator['UserModel']:
213
+ return cls.gsi2_index.query('USER', limit=limit, scan_index_forward=False)
214
+ ```
215
+
216
+ ```python title="packages/my_table/my_table/entities/order.py"
217
+ from collections.abc import Iterator
218
+ from datetime import UTC, datetime
219
+ from pynamodb.attributes import UnicodeAttribute
220
+ from .base import BaseModel
221
+
222
+
223
+ class OrderModel(BaseModel, discriminator='OrderModel'):
224
+ """
225
+ Key design:
226
+ pk=ORDER#<order_id>, sk=ORDER#<order_id>
227
+ gsi1pk=USER#<user_id>, gsi1sk=ORDER#<order_id> <- list orders by user
228
+ gsi2pk=ORDER, gsi2sk=<created_at> <- list all orders by date
229
+ """
230
+
231
+ user_id = UnicodeAttribute()
232
+ total = UnicodeAttribute()
233
+ created_at = UnicodeAttribute()
234
+
235
+ @classmethod
236
+ def make_pk(cls, order_id: str) -> str:
237
+ return f'ORDER#{order_id}'
238
+
239
+ @classmethod
240
+ def create(cls, order_id: str, user_id: str, total: str) -> 'OrderModel':
241
+ now = datetime.now(UTC).isoformat()
242
+ item = cls(
243
+ pk=cls.make_pk(order_id),
244
+ sk=cls.make_pk(order_id),
245
+ gsi1pk=f'USER#{user_id}',
246
+ gsi1sk=cls.make_pk(order_id),
247
+ gsi2pk='ORDER',
248
+ gsi2sk=now,
249
+ user_id=user_id,
250
+ total=total,
251
+ created_at=now,
252
+ )
253
+ item.save()
254
+ return item
255
+
256
+ @classmethod
257
+ def get_by_id(cls, order_id: str) -> 'OrderModel':
258
+ return cls.get(cls.make_pk(order_id), cls.make_pk(order_id))
259
+
260
+ # ── gsi1_index: partition=user, sort=order_id ────────────────────────────
261
+ @classmethod
262
+ def list_by_user(cls, user_id: str) -> Iterator['OrderModel']:
263
+ return cls.gsi1_index.query(f'USER#{user_id}')
264
+
265
+ # ── gsi2_index: partition=type, sort=created_at ───────────────────────────
266
+ @classmethod
267
+ def list_recent(cls, limit: int | None = None) -> Iterator['OrderModel']:
268
+ return cls.gsi2_index.query('ORDER', limit=limit, scan_index_forward=False)
269
+ ```
270
+
271
+ Export the new entities from `__init__.py`:
272
+
273
+ ```python title="packages/my_table/my_table/entities/__init__.py"
274
+ from .user import UserModel
275
+ from .order import OrderModel
276
+ from .example import ExampleModel
277
+ ```
278
+
279
+ #### GSI Overloading
280
+
281
+ `BaseModel` provides two shared GSIs (`gsi1_index`, `gsi2_index`). Both `UserModel` and `OrderModel` above write to `gsi2` — but with different `gsi2pk` values (`USER` vs `ORDER`). This is **GSI overloading**: reusing a single physical index to serve multiple independent access patterns without consuming extra GSI capacity.
282
+
283
+ - **`UserModel`** — `gsi2pk=USER`, `gsi2sk=<created_at>` → list all users by date
284
+ - **`OrderModel`** — `gsi2pk=ORDER`, `gsi2sk=<created_at>` → list all orders by date
285
+
286
+ `gsi1` can also be overloaded when multiple entity types share the same parent. If you later add a `ReviewModel` that also belongs to a user, you can assign it `gsi1pk=USER#<user_id>` with a `REVIEW#<id>` sort key — no additional GSI needed. Querying `gsi1` via `BaseModel` then returns both orders and reviews for that user in one request, with PynamoDB instantiating each item as its correct subclass:
287
+
288
+ ```python
289
+ from .base import BaseModel
290
+ from .order import OrderModel
291
+ from .review import ReviewModel
292
+
293
+ user_id = 'user-123'
294
+ items = list(BaseModel.gsi1_index.query(f'USER#{user_id}'))
295
+
296
+ orders = [i for i in items if isinstance(i, OrderModel)]
297
+ reviews = [i for i in items if isinstance(i, ReviewModel)]
298
+ ```
299
+
300
+ To retrieve only one entity type from an overloaded GSI, use a sort key prefix condition:
301
+
302
+ ```python
303
+ orders_only = list(BaseModel.gsi1_index.query(
304
+ f'USER#{user_id}',
305
+ range_key_condition=BaseModel.gsi1sk.startswith('ORDER#'),
306
+ ))
307
+ ```
308
+
309
+ #### One-to-Many Relationships
310
+
311
+ In a **one-to-many** relationship the child entity stores a reference to its parent in a GSI partition key, making the relationship traversable in both directions without duplicating data. The `UserModel` / `OrderModel` example above is exactly this pattern:
312
+
313
+ - **Get a single order by ID** — primary table: `pk=ORDER#<id>`, `sk=ORDER#<id>`
314
+ - **List all orders for a user** — `gsi1`: `pk=USER#<user_id>`
315
+
316
+ An alternative to GSI-based lookups is the **item collection** pattern: give child items the same `pk` as their parent and use the sort key to differentiate them. This lets you retrieve the parent and all its children in a single primary-table query, without a GSI:
317
+
318
+ ```python title="packages/my_table/my_table/entities/order.py (item collection variant)"
319
+ class OrderModel(BaseModel, discriminator='OrderModel'):
320
+ """
321
+ Key design (item collection):
322
+ pk=USER#<user_id>, sk=ORDER#<order_id> <- co-located under the parent user
323
+ """
324
+ ...
325
+ ```
326
+
327
+ ```python
328
+ # Retrieve the user and all their orders in one primary-table query
329
+ # BaseModel dispatches each item to its correct subclass via DiscriminatorAttribute
330
+ items = list(BaseModel.query(f'USER#{user_id}'))
331
+ user = next(i for i in items if isinstance(i, UserModel))
332
+ orders = [i for i in items if isinstance(i, OrderModel)]
333
+ ```
334
+
335
+ The tradeoff: item collections place all children under a single partition key, which is optimal for most workloads but can create a hot partition at extreme write throughput. The GSI approach (used in the examples above) keeps each entity in its own partition and is generally safer to start with.
336
+
337
+ #### Many-to-Many Relationships
338
+
339
+ Many-to-many relationships require a **junction entity** using the [adjacency list pattern](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-adjacency-graphs.html): a dedicated item that records each link, with its GSI key inverting the direction so the relationship can be traversed both ways.
340
+
341
+ Consider `ArticleModel` and `TagModel`, where an article can have many tags and a tag can apply to many articles:
342
+
343
+ ```python title="packages/my_table/my_table/entities/article_tag.py"
344
+ from collections.abc import Iterator
345
+ from pynamodb.attributes import UnicodeAttribute
346
+ from .base import BaseModel
347
+
348
+
349
+ class ArticleTagModel(BaseModel, discriminator='ArticleTag'):
350
+ """
351
+ Junction entity for the Article ↔ Tag many-to-many relationship.
352
+
353
+ Key design:
354
+ pk=ARTICLE#<article_id>, sk=TAG#<tag_name> <- list tags for an article
355
+ gsi1pk=TAG#<tag_name>, gsi1sk=ARTICLE#<article_id> <- list articles for a tag
356
+ """
357
+
358
+ article_id = UnicodeAttribute()
359
+ tag_name = UnicodeAttribute()
360
+
361
+ @classmethod
362
+ def add(cls, article_id: str, tag_name: str) -> 'ArticleTagModel':
363
+ item = cls(
364
+ pk=f'ARTICLE#{article_id}',
365
+ sk=f'TAG#{tag_name}',
366
+ gsi1pk=f'TAG#{tag_name}',
367
+ gsi1sk=f'ARTICLE#{article_id}',
368
+ article_id=article_id,
369
+ tag_name=tag_name,
370
+ )
371
+ item.save()
372
+ return item
373
+
374
+ @classmethod
375
+ def remove(cls, article_id: str, tag_name: str) -> None:
376
+ cls.get(f'ARTICLE#{article_id}', f'TAG#{tag_name}').delete()
377
+
378
+ # ── Primary index: pk=article, sk=tag ─────────────────────────────────────
379
+ @classmethod
380
+ def list_tags_for_article(cls, article_id: str) -> Iterator['ArticleTagModel']:
381
+ return cls.query(f'ARTICLE#{article_id}')
382
+
383
+ # ── gsi1_index: pk=tag, sk=article ────────────────────────────────────────
384
+ @classmethod
385
+ def list_articles_for_tag(cls, tag_name: str) -> Iterator['ArticleTagModel']:
386
+ return cls.gsi1_index.query(f'TAG#{tag_name}')
387
+ ```
388
+
389
+ Because `ArticleTagModel` uses `pk=ARTICLE#<article_id>` — the same partition as the article itself — you can retrieve an article and all its tags in a single primary-table query:
390
+
391
+ ```python
392
+ from .base import BaseModel
393
+ from .article import ArticleModel
394
+ from .article_tag import ArticleTagModel
395
+
396
+ items = list(BaseModel.query('ARTICLE#article-123'))
397
+ article = next(i for i in items if isinstance(i, ArticleModel))
398
+ tags = [i.tag_name for i in items if isinstance(i, ArticleTagModel)]
399
+ ```
400
+
401
+ For further reading on DynamoDB data modelling, see the [DynamoDB data modelling guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/data-modeling.html) and [Creating a single-table design with Amazon DynamoDB](https://aws.amazon.com/blogs/compute/creating-a-single-table-design-with-amazon-dynamodb/).
402
+
403
+ ### Using the DynamoDB Client
404
+
405
+ The generated `client.py` exports two key utilities:
406
+
407
+ - `is_local()` — returns `True` when `SERVE_LOCAL=true`, used to switch between local and AWS behaviour.
408
+ - `get_table_name()` — returns the DynamoDB table name. When `SERVE_LOCAL=true`, reads the table name from `serveLocal.tableName` in `config.json`; otherwise fetches the name from AWS AppConfig using the `RUNTIME_CONFIG_APP_ID` environment variable and caches it for subsequent calls.
409
+
410
+ `BaseModel` in `entities/base.py` uses both to configure PynamoDB automatically:
411
+
412
+ - **Connection** — `BaseModel.Meta` sets `host` from `config.json` and hardcodes `region`, `aws_access_key_id`, and `aws_secret_access_key` when `is_local()` is `True`, pointing PynamoDB at the local DynamoDB instance. In AWS, these are left unset so PynamoDB uses the default credential chain.
413
+ - **Table name** — `BaseModel._get_connection()` calls `get_table_name()` before each operation, so the correct table is resolved at runtime without any manual configuration.
414
+
415
+ ### Stopping Local DynamoDB
416
+
417
+ <Snippet name="dynamodb/serve-local-windows" />
418
+
419
+ ## Adding/Removing Global Secondary Indexes
420
+
421
+ GSIs are defined in `config.json` at the project root under the `tableConfig.globalSecondaryIndexes` key. Add an entry for each GSI, then reflect the change in `BaseModel` by adding or removing the corresponding `GlobalSecondaryIndex` class and attributes in `<name>/entities/base.py`:
422
+
423
+ <Snippet name="dynamodb/gsi-config" parentHeading="Adding/Removing Global Secondary Indexes" />
424
+
425
+ ## Connecting to the Table
426
+
427
+ In any Python project, add the DynamoDB package as a workspace dependency and import entity classes directly:
428
+
429
+ ```python
430
+ from my_db_package.entities import ExampleModel
431
+
432
+ item = ExampleModel.get_by_id('123')
433
+ ```
434
+
435
+ :::note[Runtime config]
436
+ When running in AWS, `get_table_name()` fetches the table name from AWS AppConfig using the `RUNTIME_CONFIG_APP_ID` environment variable. Projects built with this plugin already have this variable configured automatically. For other Python projects, ensure `RUNTIME_CONFIG_APP_ID` is set in the runtime environment with the AppConfig application ID provisioned by your infrastructure. For more information, see the <Link path="guides/runtime-config">Runtime Configuration guide</Link>.
437
+ :::
438
+
439
+ ### Connection Generators
440
+
441
+ For specific project types, use the `connection` generator to automatically wire up local development dependencies so DynamoDB Local starts automatically alongside your project, and add the DynamoDB package as a workspace dependency:
442
+
443
+ - <Link path="guides/connection/py-fast-api-dynamodb">FastAPI → DynamoDB</Link>
444
+ - <Link path="guides/connection/py-agent-dynamodb">Python Agent → DynamoDB</Link>
445
+ - <Link path="guides/connection/py-mcp-server-dynamodb">Python MCP Server → DynamoDB</Link>
446
+
447
+ ## Deploying your Table
448
+
449
+ <Snippet name="dynamodb/deploying-table" parentHeading="Deploying your Table" />
@@ -117,12 +117,16 @@ def dynamic_resource(item_id: str) -> str:
117
117
 
118
118
  ### Inspector
119
119
 
120
- The generator configures a target named `<your-server-name>-inspect`, which starts the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) with the configuration to connect to your MCP server using STDIO transport.
120
+ The generator configures a target named `<your-server-name>-inspect`, which starts your MCP server locally (via the `<your-server-name>-serve-local` target, including any connected dependencies such as a local database) and launches the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) pre-configured to connect to it over Streamable HTTP transport.
121
121
 
122
122
  <NxCommands commands={['your-server-name-inspect your-project']} />
123
123
 
124
124
  This will start the inspector at `http://localhost:6274`. Get started by clicking on the "Connect" button.
125
125
 
126
+ :::tip
127
+ To inspect the server using STDIO transport instead, use the `<your-server-name>-inspect-stdio` target, which launches the inspector against a STDIO instance of your server.
128
+ :::
129
+
126
130
  ### STDIO
127
131
 
128
132
  The easiest way to test and use an MCP server is by using the inspector or configuring it with an AI assistant (as above).
@@ -152,6 +152,21 @@ The user identity module automatically adds the necessary <Link path="guides/rea
152
152
  </Fragment>
153
153
  </Infrastructure>
154
154
 
155
+ :::caution[Remove localhost callback URLs for production]
156
+ The generated User Pool client allows your CloudFront distribution URL as an OAuth callback/logout URL, plus `http://localhost:4200` and `http://localhost:4300` for local development against the deployed pool.
157
+
158
+ It is recommended to **remove the `http://localhost` callback/logout URLs for production stages**, keeping the allowlist limited to your real application origins.
159
+
160
+ <Infrastructure>
161
+ <Fragment slot="cdk">
162
+ Edit the callback/logout URLs in `packages/common/constructs/src/core/user-identity.ts`.
163
+ </Fragment>
164
+ <Fragment slot="terraform">
165
+ Edit the `callback_urls`/`logout_urls` in `packages/common/terraform/src/core/user-identity/identity/identity.tf`.
166
+ </Fragment>
167
+ </Infrastructure>
168
+ :::
169
+
155
170
  ### Granting Access to Authenticated Users
156
171
 
157
172
  In order to grant authenticated users access to perform certain actions, such as granting permissions to invoke an API, you can add IAM policy statements to the identity pool authenticated role:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: React Website
3
3
  description: Reference documentation for a React Website
4
- generator: ts#react-website
4
+ generator: ts#website
5
5
  when:
6
6
  framework:
7
7
  - react
@@ -30,7 +30,7 @@ The default `uxProvider` is [Cloudscape](http://cloudscape.design/). You can als
30
30
 
31
31
  You can generate a new React Website in two ways:
32
32
 
33
- <RunGenerator generator="ts#website" />
33
+ <RunGenerator generator="ts#website" requiredParameters={{ framework: 'react' }} />
34
34
 
35
35
  ### Options
36
36
 
@@ -126,6 +126,14 @@ waf -> cloudfront
126
126
  cloudfront -> s3
127
127
  ```
128
128
 
129
+ #### Security Headers
130
+
131
+ The CloudFront distribution applies a response headers policy that sets `Strict-Transport-Security`, `X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy` and a `Content-Security-Policy` on all responses.
132
+
133
+ A default `Content-Security-Policy` is enforced. It restricts scripts and framing to mitigate XSS and clickjacking, while permitting HTTPS and WSS connections so the website can call AWS service endpoints (such as API Gateway, Cognito and Bedrock AgentCore) whose URLs are only known at deploy time. To adjust the policy (for example to tighten `connect-src` to your specific origins), edit the `content_security_policy` value in your generated `static-website.ts` (CDK) or `static-website.tf` (Terraform).
134
+
135
+ `runtime-config.json` is served with `Cache-Control: no-cache` so that browsers always fetch the latest configuration after a redeploy, rather than using a stale cached copy.
136
+
129
137
  ## Implementing your React Website
130
138
 
131
139
  The [React documentation](https://react.dev/learn) is a good place to start to learn the basics of building with React.
@@ -185,7 +193,7 @@ Configuration from your infrastructure is provided to your website via <Link hre
185
193
 
186
194
  <Infrastructure>
187
195
  <Fragment slot="cdk">
188
- The `RuntimeConfig` CDK construct can be used to add and retrieve configuration in your CDK infrastructure. The CDK constructs generated by `@aws/nx-plugin` generators (such as <Link path="guides/trpc">`ts#trpc-api`</Link> and <Link path="guides/fastapi">`py#fast-api`</Link>) will automatically add appropriate values to the `RuntimeConfig`.
196
+ The `RuntimeConfig` CDK construct can be used to add and retrieve configuration in your CDK infrastructure. The CDK constructs generated by `@aws/nx-plugin` generators (such as <Link path="guides/trpc">`ts#api`</Link> and <Link path="guides/fastapi">`py#api`</Link>) will automatically add appropriate values to the `RuntimeConfig`.
189
197
 
190
198
  Your website CDK construct will deploy the `connection` namespace of the runtime configuration as a `runtime-config.json` file to the root of your S3 bucket.
191
199
 
@@ -214,7 +222,7 @@ With CDK, the website construct can be declared at any point in your stack. Runt
214
222
  :::
215
223
  </Fragment>
216
224
  <Fragment slot="terraform">
217
- With Terraform, runtime configuration is managed through the runtime-config modules. The Terraform modules generated by `@aws/nx-plugin` generators (such as <Link path="guides/trpc">`ts#trpc-api`</Link> and <Link path="guides/fastapi">`py#fast-api`</Link>) will automatically add appropriate values to the runtime configuration.
225
+ With Terraform, runtime configuration is managed through the runtime-config modules. The Terraform modules generated by `@aws/nx-plugin` generators (such as <Link path="guides/trpc">`ts#api`</Link> and <Link path="guides/fastapi">`py#api`</Link>) will automatically add appropriate values to the runtime configuration.
218
226
 
219
227
  Your website Terraform module will deploy the `connection` namespace of the runtime configuration as a `runtime-config.json` file to the root of your S3 bucket.
220
228
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: tRPC
3
3
  description: Reference documentation for tRPC
4
- generator: ts#trpc-api
4
+ generator: ts#api
5
5
  when:
6
6
  framework: [trpc]
7
7
  infra: [rest-lambda, http-lambda]
@@ -25,11 +25,11 @@ The tRPC API generator creates a new tRPC API with AWS CDK or Terraform infrastr
25
25
 
26
26
  You can generate a new tRPC API in two ways:
27
27
 
28
- <RunGenerator generator="ts#trpc-api" />
28
+ <RunGenerator generator="ts#api" requiredParameters={{ framework: 'trpc' }} />
29
29
 
30
30
  ### Options
31
31
 
32
- <GeneratorParameters generator="ts#trpc-api" />
32
+ <GeneratorParameters generator="ts#api" />
33
33
 
34
34
  <Snippet name="api/api-choice-note" />
35
35
 
@@ -138,7 +138,7 @@ The example `echo` procedure is generated for you in `src/procedures/echo.ts`:
138
138
  export const echo = publicProcedure
139
139
  .input(EchoInputSchema)
140
140
  .output(EchoOutputSchema)
141
- .query((opts) => ({ result: opts.input.message }));
141
+ .query((opts) => ({ message: opts.input.message }));
142
142
  ```
143
143
 
144
144
  To break down the above:
@@ -478,7 +478,7 @@ export interface IIdentityContext {
478
478
 
479
479
  Note that we define an additional _optional_ property on the context. tRPC manages ensuring that this is defined in procedures which have correctly configured this middleware.
480
480
 
481
- Next, the middleware itself. The generator wires the authorizer to accept ID tokens, so `cognito:username` is the canonical username claim; we fall back to `username` for access tokens in case you reconfigure the authorizer:
481
+ Next, the middleware itself:
482
482
 
483
483
  ```ts
484
484
  import { initTRPC, TRPCError } from '@trpc/server';
@@ -503,7 +503,7 @@ export const createIdentityPlugin = () => {
503
503
  | undefined;
504
504
 
505
505
  const sub = claims?.sub;
506
- const username = claims?.['cognito:username'] ?? claims?.username;
506
+ const username = claims?.username;
507
507
 
508
508
  if (!sub || !username) {
509
509
  throw new TRPCError({
@@ -541,8 +541,8 @@ export const me = publicProcedure
541
541
  }));
542
542
  ```
543
543
 
544
- :::tip[Verifying the token]
545
- You don't need `aws-jwt-verify` or any other JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, audience, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
544
+ :::tip[No token verification required]
545
+ You don't need `aws-jwt-verify` or any other JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, scopes, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
546
546
  :::
547
547
  </OptionFilter>
548
548
 
@@ -269,11 +269,49 @@ This command uses `tsx --watch` to automatically restart the server when files c
269
269
 
270
270
  ### Chat with Your Agent
271
271
 
272
- The generator configures a `<your-agent-name>-chat` Nx target that depends on `<your-agent-name>-serve-local`. Running it starts the agent locally and drops you into an interactive terminal chat:
272
+ The generator configures a `<your-agent-name>-chat` Nx target that drops you into an interactive terminal chat with your agent.
273
+
274
+ The chat target runs standalone. By default it connects to your locally running agent, so start `<your-agent-name>-serve-local` first (in a separate terminal):
275
+
276
+ <NxCommands commands={['run your-project:agent-serve-local']} />
277
+
278
+ Then, in another terminal, start the chat:
273
279
 
274
280
  <NxCommands commands={['run your-project:agent-chat']} />
275
281
 
276
- For **HTTP** (tRPC over WebSocket) agents, the generator also emits a tiny `scripts/<your-agent-name>/chat.ts` that wraps the generated `<Agent>Client.local({ url })` so you can customize it as you evolve the agent's input shape.
282
+ The generator emits a `scripts/<your-agent-name>/chat.ts` for every protocol. You can customize it as you evolve the agent's input shape. It connects to the local agent by default, or to your deployed agent when `RUNTIME_CONFIG_APP_ID` is set (see [Chat with your deployed agent](#chat-with-your-deployed-agent) below).
283
+
284
+ <OptionFilter when={{ infra: 'agentcore' }} description="Deployed agent chat details">
285
+ #### Chat with your deployed agent
286
+
287
+ To chat with your agent deployed to Bedrock AgentCore, set the `RUNTIME_CONFIG_APP_ID` environment variable to the AppConfig application id of the deployment (output as `RuntimeConfigApplicationId` by the deployed stack). The chat script resolves your agent's runtime ARN from runtime configuration and connects to the deployed endpoint:
288
+
289
+ <Tabs syncKey="auth">
290
+ <TabItem label="IAM" _filter={{ auth: 'iam' }}>
291
+ For IAM-authenticated agents, requests are signed with [SigV4](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html) using your default AWS credentials. Ensure the environment has AWS credentials with permission to invoke the runtime:
292
+
293
+ <NxCommands commands={['run your-project:agent-chat']} env={{ RUNTIME_CONFIG_APP_ID: '<app-id>' }} />
294
+ </TabItem>
295
+
296
+ <TabItem label="Cognito" _filter={{ auth: 'cognito' }}>
297
+ For Cognito-authenticated agents, provide a Cognito access token via the `AGENT_ACCESS_TOKEN` environment variable, which is sent as a bearer token:
298
+
299
+ <NxCommands commands={['run your-project:agent-chat']} env={{ RUNTIME_CONFIG_APP_ID: '<app-id>', AGENT_ACCESS_TOKEN: '<access-token>' }} />
300
+
301
+ You can obtain an access token using the AWS CLI's `cognito-idp admin-initiate-auth` command, for example:
302
+
303
+ ```bash
304
+ aws cognito-idp admin-initiate-auth \
305
+ --user-pool-id <user-pool-id> \
306
+ --client-id <user-pool-client-id> \
307
+ --auth-flow ADMIN_NO_SRP_AUTH \
308
+ --auth-parameters USERNAME=<username>,PASSWORD=<password> \
309
+ --query 'AuthenticationResult.AccessToken' \
310
+ --output text
311
+ ```
312
+ </TabItem>
313
+ </Tabs>
314
+ </OptionFilter>
277
315
 
278
316
  <OptionFilter when={{ infra: 'agentcore' }} description="Bedrock AgentCore Runtime deployment details">
279
317
  ## Deploying Your Agent to Bedrock AgentCore Runtime