@jaypie/mcp 0.8.156 → 0.8.158

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.
@@ -13,7 +13,7 @@ import { RELEASE_NOTES_HELP } from './help.js';
13
13
  /**
14
14
  * Docs Suite - Documentation services (skill, version, release_notes)
15
15
  */
16
- const BUILD_VERSION_STRING = "@jaypie/mcp@0.8.156#2f9f851b"
16
+ const BUILD_VERSION_STRING = "@jaypie/mcp@0.8.158#55ed6fbc"
17
17
  ;
18
18
  // =============================================================================
19
19
  // ASSET PATHS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaypie/mcp",
3
- "version": "0.8.156",
3
+ "version": "0.8.158",
4
4
  "description": "Jaypie MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,22 @@
1
+ ---
2
+ version: 1.2.85
3
+ date: 2026-09-13
4
+ summary: Per-build tags skip DynamoDB tables so migration-owned indexes survive redeploys
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`constructTagger`** adds `buildDate`, `buildHex`, `buildTime`, `commit`, and
10
+ `version` with `excludeResourceTypes` for `AWS::DynamoDB::GlobalTable` and
11
+ `AWS::DynamoDB::Table`. These tags changed every synth, forcing a table update
12
+ on every deploy. A table whose GSIs a migration created rejects that update
13
+ with `Invalid AttributeDefinitions`, so every deploy after the first rolled
14
+ back (#548).
15
+ - **`JaypieInfrastructureStack`** applies the same exclusion to `stackSha`.
16
+
17
+ ## Upgrade
18
+
19
+ Tables deployed with earlier versions carry the build tags. The first deploy on
20
+ this version removes them, which is a table update. A table with no
21
+ migration-owned indexes updates normally. A table with migration-owned indexes
22
+ fails that update: recreate the table or declare its indexes in CDK.
@@ -0,0 +1,42 @@
1
+ ---
2
+ version: 1.2.86
3
+ date: 2026-09-13
4
+ summary: CDK owns DynamoDB indexes; external secrets and SSOSync secret props keep credentials out of templates
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`JaypieMigration`** grants `DescribeTable`, `DescribeTimeToLive`, and
10
+ `DescribeContinuousBackups` only. `UpdateTable`, `UpdateTimeToLive`, and
11
+ `UpdateContinuousBackups` are removed: a GSI a migration creates blocks every
12
+ later CloudFormation update to the table (#552).
13
+ - **`JaypieMigration`** warns at synth when a `JaypieDynamoDb` in `tables`
14
+ declares no `indexes`. Acknowledge
15
+ `@jaypie/constructs:migrationTableWithoutIndexes` for tables that need none.
16
+ - **`JaypieDynamoDb`** sorts `indexes` by name, so
17
+ `indexes: getAllRegisteredIndexes()` synthesizes the same template regardless
18
+ of import order, and exposes the sorted list as `indexes`.
19
+ - **`constructTagger`** and **`JaypieInfrastructureStack`** apply per-build tags
20
+ to DynamoDB tables again, reverting 1.2.85.
21
+ - **`JaypieSecret`** and **`JaypieEnvSecret`** accept `external: true`, which
22
+ creates an empty secret whose value never enters the template, assets
23
+ bucket, `cdk.out`, or `cdk diff`. An `ExternalArn` output described by the
24
+ `envKey` locates the secret for CI `put-secret-value` (#551).
25
+ - **`JaypieSsoSyncApplication`** accepts `googleCredentialsSecret` and
26
+ `scimEndpointAccessTokenSecret` and deploys SSOSync as "App only" with six
27
+ secret ARNs in `CrossStackConfig`. Literal credentials still work and warn
28
+ at synth (`@jaypie/constructs:ssoSyncLiteralCredentials`).
29
+
30
+ ## Upgrade
31
+
32
+ - Declare every registered index in `JaypieDynamoDb` `indexes`.
33
+ - A table with GSIs created outside CDK fails the first deploy on this version,
34
+ because build tags update the table. CloudFormation cannot adopt those
35
+ indexes: declare them and recreate the table. See `skill("dynamodb")`.
36
+ - Migrations that call `UpdateTable`, `UpdateTimeToLive`, or
37
+ `UpdateContinuousBackups` fail with `AccessDenied`. Move those settings into
38
+ `JaypieDynamoDb`.
39
+ - Add indexes to an existing table one per deploy.
40
+ - Moving `JaypieSsoSyncApplication` to secret props deletes the SAR-created
41
+ `SSOSync*` secrets. Sync fails until CI writes the new secret values. See
42
+ `skill("secrets")`.
@@ -0,0 +1,13 @@
1
+ ---
2
+ version: 0.8.157
3
+ date: 2026-09-13
4
+ summary: Document that tables with migration-owned indexes need stable CloudFormation properties
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`dynamodb`** adds "Keep Table Properties Stable": any CloudFormation update
10
+ to a table fails once a migration creates a GSI.
11
+ - **`migrations`** notes migration-owned index behavior and recovery for stacks
12
+ deployed before `@jaypie/constructs` 1.2.85.
13
+ - **`cdk`** notes that per-build tags skip DynamoDB tables (#548).
@@ -0,0 +1,19 @@
1
+ ---
2
+ version: 0.8.158
3
+ date: 2026-09-13
4
+ summary: Indexes belong in CDK; external secrets keep credentials out of templates
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`dynamodb`** replaces "Keep Table Properties Stable" with "Share Indexes
10
+ Between CDK and Runtime" (`indexes: getAllRegisteredIndexes()`) and "Indexes
11
+ Created Outside CDK" (failure and recovery). Local Development creates the
12
+ table with `createTable()` from the registry and runs the migration seed,
13
+ replacing the AWS CLI script that created no GSIs (#552).
14
+ - **`migrations`** documents describe-only grants, the synth warning for tables
15
+ without indexes, and a seed module shared by the migration and local scripts.
16
+ - **`cdk`** removes the note that per-build tags skip DynamoDB tables.
17
+ - **`secrets`** adds External Secrets (`external: true` with a CI
18
+ `put-secret-value` step) and SSOSync Credentials, and notes that env-sourced
19
+ values are written into the template (#551).
package/skills/cdk.md CHANGED
@@ -131,7 +131,7 @@ workspaces/
131
131
  `cdk-{PROJECT_SPONSOR}-{PROJECT_KEY}-{PROJECT_ENV}-{PROJECT_NONCE}[-{key}]`,
132
132
  so naming stays consistent across sandbox, personal, and production deploys.
133
133
  2. **Account & region** resolved from `CDK_DEFAULT_ACCOUNT` / `CDK_DEFAULT_REGION` (overridable via `env`).
134
- 3. **Standard tags** applied to the stack and propagated to every taggable child resource: `env`, `project`, `sponsor`, `nonce`, `commit`, `buildHex`, `buildDate`, `buildTime`, `version`, `service`, `creation`, `role`, `stack`.
134
+ 3. **Standard tags** applied to the stack and propagated to every taggable child resource: `env`, `project`, `sponsor`, `nonce`, `commit`, `buildHex`, `buildDate`, `buildTime`, `version`, `service`, `creation`, `role`, `stack`. Per-build tags update every table on every deploy, so a table must declare its indexes in CDK (see `skill("dynamodb")`).
135
135
 
136
136
  ```typescript
137
137
  import { Construct } from "constructs";
@@ -235,7 +235,7 @@ const handler = new JaypieLambda(this, "ApiHandler", {
235
235
  | 1 table | `grantReadWriteData()` plus `Query`/`Scan` on `index/*` | `DYNAMODB_TABLE_NAME` set automatically |
236
236
  | 2+ tables | `grantReadWriteData()` plus `Query`/`Scan` on `index/*` for each | No auto env var — set `CDK_ENV_TABLE` manually |
237
237
 
238
- CDK grants index ARNs only for indexes declared in CDK. The explicit `index/*` grant covers GSIs created by migrations (`UpdateTable`) and indexes on imported tables.
238
+ The explicit `index/*` grant also covers indexes on imported tables, which CDK does not know about.
239
239
 
240
240
  Single table example — at runtime, use `process.env.DYNAMODB_TABLE_NAME`:
241
241
 
@@ -270,9 +270,9 @@ const { items: chats } = await queryByScope({ model: "chat", scope: APEX });
270
270
 
271
271
  GSIs are defined using `fabricIndex()` from `@jaypie/fabric`. **Do not create all GSIs upfront** — start with zero and add only what your access patterns require. The most common first GSI is `indexModel` for listing entities by model.
272
272
 
273
- **Important:** DynamoDB allows only **one GSI to be added per deployment**. If you need multiple GSIs, add them sequentially across separate deploys. For production tables, the AWS CLI is often better suited for adding GSIs than CDK (which may try to replace the table).
273
+ **Indexes belong in CDK.** Declare every registered `fabricIndex()` in `JaypieDynamoDb` `indexes` (see [Share Indexes Between CDK and Runtime](#share-indexes-between-cdk-and-runtime)). A new table is created with all declared GSIs in one deploy. DynamoDB adds only **one GSI per update** to an existing table, so add indexes to a deployed table one per deploy.
274
274
 
275
- **Grants:** CDK adds `${tableArn}/index/*` to grants only for indexes declared in CDK. GSIs created by migrations or the CLI are invisible to CDK, so a role needs `Query` and `Scan` on the index ARN explicitly. `JaypieLambda` and `JaypieMigration` grant this for every table in `tables`; hand-rolled roles must add it.
275
+ **Grants:** `JaypieLambda` and `JaypieMigration` grant `Query` and `Scan` on `${tableArn}/index/*` for every table in `tables`, which also covers indexes on imported tables. Hand-rolled roles must add it.
276
276
 
277
277
  All GSIs use a composite sort key of `scope#updatedAt` (stored as `{indexName}Sk`). Queries use `begins_with` on the sk to filter by scope; omitting scope lists across all scopes.
278
278
 
@@ -474,17 +474,75 @@ const table = new JaypieDynamoDb(this, "myApp", {
474
474
 
475
475
  Wire tables to Lambda using the `tables` prop — see `skill("cdk")` for details.
476
476
 
477
+ ### Share Indexes Between CDK and Runtime
478
+
479
+ The model registry is the single source of indexes. Register models in a module that both the CDK workspace and the runtime import, then pass the registry to the construct:
480
+
481
+ ```typescript
482
+ // packages/models/src/index.ts
483
+ import { fabricIndex, registerModel } from "@jaypie/fabric";
484
+
485
+ registerModel({ model: "record", indexes: [fabricIndex(), fabricIndex("alias")] });
486
+ registerModel({ model: "session", indexes: [fabricIndex()], ttl: "30 days" });
487
+ ```
488
+
489
+ ```typescript
490
+ // workspaces/cdk/lib/data-stack.ts
491
+ import "@project/models"; // registers models
492
+ import { JaypieDynamoDb } from "@jaypie/constructs";
493
+ import { getAllRegisteredIndexes } from "@jaypie/fabric";
494
+
495
+ const table = new JaypieDynamoDb(this, "myApp", {
496
+ indexes: getAllRegisteredIndexes(),
497
+ });
498
+ ```
499
+
500
+ `JaypieDynamoDb` sorts indexes by name, so import order does not change the template. The registry is module state: the CDK workspace and the models module must resolve the same `@jaypie/fabric` copy, or `getAllRegisteredIndexes()` returns an empty list.
501
+
502
+ ### Indexes Created Outside CDK
503
+
504
+ A GSI created through `UpdateTable` (a migration, the console, or the CLI) blocks every later CloudFormation update to the table:
505
+
506
+ ```
507
+ Invalid AttributeDefinitions Expected the following attributes to be present: [indexModel, ...]
508
+ ```
509
+
510
+ CloudFormation sends only the attributes the template declares, and DynamoDB rejects a request that omits attributes an existing index references. Tag, TTL, point-in-time recovery, and billing changes all fail. `JaypieMigration` grants no `UpdateTable` for this reason.
511
+
512
+ CloudFormation cannot adopt an existing GSI. Recover by declaring the indexes in `JaypieDynamoDb` and recreating the table. Outside production, delete and redeploy the stack. In production, deploy a new CDK-declared table beside the old one, copy items with `scanTable` and `updateEntity` (see [Migration: v0.4.x to v0.5.0](#migration-v04x-to-v050)), and switch `tables` to the new table.
513
+
477
514
  ## Local Development
478
515
 
479
516
  Use docker-compose for local DynamoDB. The `@jaypie/dynamodb` MCP tool can generate a `docker-compose.yml` with custom ports.
480
517
 
518
+ `createTable()` builds the table with every registered GSI, the same indexes CDK declares. Import the models module, create the table, then run the seed the deployed migration runs (see `skill("migrations")`):
519
+
520
+ ```typescript
521
+ // scripts/dynamo-create-table.ts
522
+ import "@project/models"; // registers models
523
+ import { createTable, initClient } from "@jaypie/dynamodb";
524
+
525
+ import { seed } from "../packages/api/src/migrations/seed/seed.js";
526
+
527
+ initClient({
528
+ credentials: { accessKeyId: "local", secretAccessKey: "local" },
529
+ endpoint: "http://127.0.0.1:9060",
530
+ tableName: "jaypie-local",
531
+ });
532
+
533
+ await createTable();
534
+ await seed();
535
+ ```
536
+
537
+ `createTable()` leaves an existing table unchanged and adds no indexes to it. After registering a new index, run `npm run dynamo:remove && npm run dynamo:init` to rebuild the local table.
538
+
481
539
  ### Suggested package.json Scripts
482
540
 
483
541
  ```json
484
542
  {
485
543
  "scripts": {
486
544
  "dynamo:init": "docker compose up -d && npm run dynamo:create-table",
487
- "dynamo:create-table": "AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local aws dynamodb create-table --table-name jaypie-local --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST --endpoint-url http://127.0.0.1:9060 2>/dev/null || true",
545
+ "dynamo:create-table": "tsx scripts/dynamo-create-table.ts",
488
546
  "dynamo:remove": "docker compose down -v",
489
547
  "dynamo:start": "docker compose up -d",
490
548
  "dynamo:stop": "docker compose down"
@@ -47,25 +47,38 @@ new JaypieMigration(this, "SeedData", {
47
47
  - **Role**: Tagged as `CDK.ROLE.PROCESSING`
48
48
  - **Execution**: Uses `cr.Provider` with both `onEventHandler` and `isCompleteHandler` pointing to the same Lambda. The `onEventHandler` returns `PhysicalResourceId` immediately; the migration code runs in `isCompleteHandler` invocations, which are polled by Step Functions until `IsComplete: true`. `Delete` requests skip the migration entirely.
49
49
  - **Dependencies**: Use `dependencies` to ensure tables and other resources exist before the migration executes
50
- - **Permissions**: Tables passed via `tables` get data-plane (`grantReadWriteData`), `Query` and `Scan` on `${tableArn}/index/*`, plus control-plane access (`DescribeTable`, `UpdateTable`, `UpdateTimeToLive`, `UpdateContinuousBackups`) scoped to the table ARN and its indexes — migrations that add GSIs, toggle TTL, or change backups work without extra IAM, and later migrations can query the GSIs earlier ones created
50
+ - **Permissions**: Tables passed via `tables` get data-plane (`grantReadWriteData`), `Query` and `Scan` on `${tableArn}/index/*`, plus `DescribeTable`, `DescribeTimeToLive`, and `DescribeContinuousBackups` scoped to the table ARN and its indexes. Migrations read and write data. They do not change table shape
51
+ - **Indexes**: CDK owns indexes, TTL, and backups. Declare every registered `fabricIndex()` in `JaypieDynamoDb` `indexes` (see `skill("dynamodb")`). A GSI created outside CDK blocks every later CloudFormation update to the table, so `JaypieMigration` grants no `UpdateTable`. Synth warns (`@jaypie/constructs:migrationTableWithoutIndexes`) when a `JaypieDynamoDb` in `tables` declares no indexes; acknowledge it for tables that need none
51
52
 
52
53
  ## Migration Lambda Handler
53
54
 
54
55
  Use `migrationHandler` from `jaypie` so errors propagate as CFN failures and the waiter protocol is handled automatically.
55
56
 
56
- ```typescript
57
- // src/migrations/seed/index.ts
58
- import { initClient, seedEntities, APEX } from "@jaypie/dynamodb";
59
- import { migrationHandler } from "jaypie";
57
+ Keep the seed in its own module so the local table script runs the same seed (see Local Development in `skill("dynamodb")`):
60
58
 
61
- export const handler = migrationHandler(async (event) => {
62
- await initClient();
59
+ ```typescript
60
+ // src/migrations/seed/seed.ts
61
+ import { APEX, seedEntities } from "@jaypie/dynamodb";
63
62
 
64
- await seedEntities([
63
+ export async function seed() {
64
+ return seedEntities([
65
65
  { alias: "config-main", model: "config", name: "Main Config", scope: APEX },
66
66
  { alias: "vocab-en", model: "vocabulary", name: "English", scope: APEX },
67
67
  ]);
68
+ }
69
+ ```
68
70
 
71
+ ```typescript
72
+ // src/migrations/seed/index.ts
73
+ import "@project/models"; // registers models; indexEntity requires them
74
+ import { initClient } from "@jaypie/dynamodb";
75
+ import { migrationHandler } from "jaypie";
76
+
77
+ import { seed } from "./seed.js";
78
+
79
+ export const handler = migrationHandler(async () => {
80
+ await initClient();
81
+ await seed();
69
82
  return { status: "complete" };
70
83
  });
71
84
  ```
package/skills/secrets.md CHANGED
@@ -71,7 +71,7 @@ Synth fails fast whenever a declared secret source produces no secret string, so
71
71
  ```typescript
72
72
  new JaypieSecret(this, "ApiKey", { value: process.env.MISSING }); // throws ConfigurationError
73
73
  new JaypieSecret(this, "ApiKey", { envKey: "MISSING" }); // throws ConfigurationError
74
- new JaypieSecret(this, "Placeholder"); // allowed: empty secret, no source declared
74
+ new JaypieSecret(this, "Placeholder"); // allowed: CDK generates a random value
75
75
  ```
76
76
 
77
77
  `JaypieEnvSecret` applies the same guard, except in consumer environments, where the secret is imported rather than created.
@@ -91,6 +91,8 @@ jobs:
91
91
  - run: npx cdk deploy
92
92
  ```
93
93
 
94
+ An env-sourced value is written into the synthesized template. Copies persist in the deployed stack template, the CDK assets bucket, `cdk.out`, and `cdk diff` output. Use [External Secrets](#external-secrets) for credentials that must stay out of those places.
95
+
94
96
  ## Runtime: Retrieving Secrets
95
97
 
96
98
  Use `getEnvSecret` to fetch secrets in Lambda:
@@ -202,6 +204,66 @@ new JaypieEnvSecret(this, "AdminSeed", {
202
204
 
203
205
  With `excludePunctuation: true` and `includeSpace: false`, the generated value contains only base62 characters.
204
206
 
207
+ ## External Secrets
208
+
209
+ `external: true` creates an empty secret (no `SecretString` or `GenerateSecretString`), so the value never enters the template, the assets bucket, `cdk.out`, or `cdk diff`. CI sets the value after deploy. `envKey` still names the runtime variable (`SECRET_ANTHROPIC_API_KEY`) but is never read at synth. Combining `external` with `value` or `generateSecretString` throws `ConfigurationError`.
210
+
211
+ ```typescript
212
+ const anthropicSecret = new JaypieSecret(this, "ANTHROPIC_API_KEY", {
213
+ external: true,
214
+ });
215
+
216
+ new JaypieLambda(this, "Handler", {
217
+ code: "dist/lambda",
218
+ handler: "index.handler",
219
+ secrets: [anthropicSecret],
220
+ });
221
+ ```
222
+
223
+ The construct outputs the secret ARN with the `envKey` (or construct id) as the output description. Set the value in a step after `cdk deploy`, and keep the value out of the deploy step's environment:
224
+
225
+ ```yaml
226
+ - name: Set external secrets
227
+ env:
228
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
229
+ STACK_NAME: my-stack
230
+ run: |
231
+ ARN=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" \
232
+ --query "Stacks[0].Outputs[?Description=='ANTHROPIC_API_KEY'].OutputValue" \
233
+ --output text)
234
+ printf '%s' "$ANTHROPIC_API_KEY" > "$RUNNER_TEMP/secret"
235
+ aws secretsmanager put-secret-value --secret-id "$ARN" \
236
+ --secret-string "file://$RUNNER_TEMP/secret" > /dev/null
237
+ rm -f "$RUNNER_TEMP/secret"
238
+ ```
239
+
240
+ - A new external secret has no value until the step runs; `getEnvSecret` fails until then.
241
+ - CloudFormation never writes the value, so a hand-rotated secret is not overwritten by a later deploy.
242
+ - Switching an existing secret to `external` removes `SecretString` from the template. Run the value step in the same workflow.
243
+ - Each `put-secret-value` creates a secret version. Compare with `get-secret-value` first to skip unchanged writes.
244
+ - In `JaypieEnvSecret` consumer environments the secret is imported; set the value on the provider stack.
245
+
246
+ ### SSOSync Credentials
247
+
248
+ `JaypieSsoSyncApplication` writes `googleCredentials` and `scimEndpointAccessToken` into the template and warns at synth (`@jaypie/constructs:ssoSyncLiteralCredentials`). Pass secrets instead:
249
+
250
+ ```typescript
251
+ new JaypieSsoSyncApplication(this, "SsoSync", {
252
+ googleCredentialsSecret: new JaypieSecret(this, "SsoSyncGoogleCredentials", {
253
+ external: true,
254
+ }),
255
+ scimEndpointAccessTokenSecret: new JaypieSecret(this, "SsoSyncScimToken", {
256
+ external: true,
257
+ }),
258
+ });
259
+ ```
260
+
261
+ - The application deploys SSOSync as "App only". The construct creates secrets for the admin email, SCIM endpoint URL, region, and identity store ID (still read from `CDK_ENV_SSOSYNC_*`), then passes all six ARNs in `CrossStackConfig`.
262
+ - Both secret props are required together, and neither combines with the literal props.
263
+ - Secrets need a complete ARN: created in the stack or imported with `Secret.fromSecretCompleteArn`. SSOSync reads them with the AWS managed key.
264
+ - Switching an existing application from "App + secrets" deletes the SAR-created `SSOSync*` secrets. Sync fails until CI writes the new secret values. Do not name new secrets `SSOSync*`.
265
+ - SSOSync accepts secret ARNs in `us`, `us-gov`, `ap`, `ca`, `cn`, `eu`, and `sa` regions only.
266
+
205
267
  ## Seeds and API Keys in Workflows
206
268
 
207
269
  Seeds are secrets used to derive deterministic values (like API keys) at runtime. The generated-secret pattern is ideal for seeds because: