@jaypie/mcp 0.8.180 → 0.8.182

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/dist/assets.js CHANGED
@@ -55,11 +55,11 @@ function getMcpAssetPaths() {
55
55
  */
56
56
  function getMcpBuildInfo() {
57
57
  return {
58
- commit: "bd1e9e07"
58
+ commit: "91eb8a17"
59
59
  ,
60
- version: "0.8.180"
60
+ version: "0.8.182"
61
61
  ,
62
- versionString: "@jaypie/mcp@0.8.180#bd1e9e07"
62
+ versionString: "@jaypie/mcp@0.8.182#91eb8a17"
63
63
  ,
64
64
  };
65
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaypie/mcp",
3
- "version": "0.8.180",
3
+ "version": "0.8.182",
4
4
  "description": "Jaypie MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,21 @@
1
+ ---
2
+ version: 0.6.14
3
+ date: 2026-10-02
4
+ summary: Adds patchEntity, a targeted UpdateItem that sets or removes listed fields without moving GSI entries or overwriting other fields
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`patchEntity({ id, set?, remove?, condition?, names?, values? })`** (#606).
10
+ Sends a targeted `UpdateItem` that sets and removes only the listed
11
+ attributes. `updatedAt` and every GSI key stay unchanged, so the entity
12
+ keeps its place in `queryByScope` and other index queries, and concurrent
13
+ writes to other fields are never overwritten with a stale copy. Returns the
14
+ entity as stored after the write.
15
+ - Protected attributes (`id`, `model`, `updatedAt`, `archivedAt`, `deletedAt`,
16
+ `index*`, and every pk/sk field of a registered index) throw
17
+ `BadRequestError`; change those with `updateEntity`. A missing entity throws
18
+ `NotFoundError`; a failed `condition` throws `ConflictError`.
19
+ - **Docs:** `updateEntity` and `queryByScope` documentation explains that each
20
+ `updateEntity` moves the entity's `indexModel` entry, and recommends
21
+ `patchEntity` for counters, heartbeats, and "last X" fields.
@@ -0,0 +1,9 @@
1
+ ---
2
+ version: 1.2.117
3
+ date: 2026-10-05
4
+ summary: Pulls @jaypie/llm 1.3.58
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **`@jaypie/llm` 1.3.58**: a `format` request answered with no content fails over instead of resolving with `content: undefined`, and throws `LlmIncompleteError` when every model comes back empty; Gemini block reasons and `OTHER` finishes count as incomplete.
@@ -0,0 +1,28 @@
1
+ ---
2
+ version: 1.3.58
3
+ date: 2026-10-05
4
+ summary: A format request answered with no content fails over instead of resolving with undefined content, and throws LlmIncompleteError when every model comes back empty
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ ### Empty structured output (issue #608)
10
+
11
+ `operate` with `format` resolved successfully when the provider returned no content (observed: Gemini, 0 output tokens, `fallbackUsed: false`, no warning). The caller received `content: undefined`.
12
+
13
+ - Empty content (`undefined`, `null`, or whitespace) on a `format` request settles `status: "incomplete"` with `error.reason: "incomplete"`, `error.detail` `Model returned no content: <stop reason>`, and `stopReason: "other"`. It logs at `warn` with the provider's stop reason.
14
+ - Adapters with a corrective turn (Fireworks, Google) try it once before settling empty. Salvage and fresh-context conversion are skipped: there is no text to read.
15
+ - A fallback chain moves past an empty response like any cut-off. An earlier partial is kept over a later empty response.
16
+ - When only empty responses come back, `operate` throws `LlmIncompleteError` (status 502) whatever `incomplete` says: there is no partial to return.
17
+ - Requests without `format` are unchanged.
18
+
19
+ ### Gemini stop reasons
20
+
21
+ - A blocked prompt returns no candidate; `promptFeedback.blockReason` is now reported as the stop reason, so it fails over and names its cause.
22
+ - `OTHER`, `LANGUAGE`, `IMAGE_SAFETY`, and `MALFORMED_FUNCTION_CALL` are incomplete finishes. `IMAGE_SAFETY` normalizes to `content_filter`.
23
+
24
+ ## Testing
25
+
26
+ - `OperateLoop` specs cover empty and whitespace content with `format`, the corrective turn on empty content, settling empty once turns run out, no conversion of empty content, and unchanged behavior without `format`.
27
+ - `Llm` class specs cover failover on empty content, throwing for a lone model and a whole chain, and a partial kept over a later empty response.
28
+ - `runWithFallback`, `GoogleAdapter`, and `incompleteReason` specs cover `isEmptyResult`, the block reason, and the new Gemini finishes.
@@ -0,0 +1,10 @@
1
+ ---
2
+ version: 0.8.181
3
+ date: 2026-10-02
4
+ summary: Release notes for @jaypie/dynamodb 0.6.14 and @jaypie/testkit 1.2.81; dynamodb skill documents patchEntity
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **Release notes**: `@jaypie/dynamodb` 0.6.14 and `@jaypie/testkit` 1.2.81
10
+ - **`dynamodb` skill**: documents `patchEntity`, the protected attributes, and why each `updateEntity` moves an entity's `indexModel` entry
@@ -0,0 +1,10 @@
1
+ ---
2
+ version: 0.8.182
3
+ date: 2026-10-05
4
+ summary: Release notes for @jaypie/llm 1.3.58 and jaypie 1.2.117; llm skill documents empty format responses
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - **Release notes**: `@jaypie/llm` 1.3.58 and `jaypie` 1.2.117
10
+ - **`llm` skill**: documents how an empty `format` response fails over and throws
@@ -0,0 +1,9 @@
1
+ ---
2
+ version: 1.2.81
3
+ date: 2026-10-02
4
+ summary: Adds the patchEntity mock for @jaypie/dynamodb
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - `patchEntity` mock in the `@jaypie/dynamodb` mock; resolves to `{ id, ...set }` by default.
@@ -127,7 +127,7 @@ interface StorableEntity {
127
127
  ### Entity Operations
128
128
 
129
129
  ```typescript
130
- import { APEX, createEntity, getEntity, updateEntity, deleteEntity, archiveEntity, destroyEntity } from "@jaypie/dynamodb";
130
+ import { APEX, createEntity, getEntity, patchEntity, updateEntity, deleteEntity, archiveEntity, destroyEntity } from "@jaypie/dynamodb";
131
131
 
132
132
  // Create entity — indexEntity auto-populates GSI keys, createdAt, updatedAt
133
133
  const record = await createEntity({
@@ -148,9 +148,12 @@ const record = await createEntity({
148
148
  // Get by primary key (id only)
149
149
  const item = await getEntity({ id: "abc-123" });
150
150
 
151
- // Update — sets updatedAt, re-indexes
151
+ // Update — writes the whole item, sets updatedAt, re-indexes (moves GSI entries)
152
152
  await updateEntity({ entity: { ...item, name: "Updated Name" } });
153
153
 
154
+ // Patch — sets only the listed fields; updatedAt and GSI keys unchanged
155
+ await patchEntity({ id: "abc-123", set: { lastUsedAt: new Date() } });
156
+
154
157
  // Conditional update — write-time guard; throws ConflictError if it fails
155
158
  await updateEntity({
156
159
  entity: { ...item, status: "complete" },
@@ -176,13 +179,55 @@ await destroyEntity({ id: "abc-123" });
176
179
  |----------|-------------|
177
180
  | `createEntity({ entity, preserveTimestamps? })` | Create entity; returns `null` if `id` exists (conditional `attribute_not_exists(id)`) |
178
181
  | `getEntity({ id })` | Get by primary key (id only) |
179
- | `updateEntity({ entity, condition?, names?, preserveTimestamps?, values? })` | Update (sets `updatedAt`, re-indexes). Pass `condition` (a ConditionExpression, with `names`/`values` bindings) to guard the write; throws `ConflictError` (409) and leaves the item unchanged when the condition fails |
182
+ | `updateEntity({ entity, condition?, names?, preserveTimestamps?, values? })` | Update (writes the whole item, sets `updatedAt`, re-indexes; each call moves the entity's GSI entries). Pass `condition` (a ConditionExpression, with `names`/`values` bindings) to guard the write; throws `ConflictError` (409) and leaves the item unchanged when the condition fails |
183
+ | `patchEntity({ id, set?, remove?, condition?, names?, values? })` | Targeted `UpdateItem`: sets/removes only the listed attributes; leaves `updatedAt` and every GSI key unchanged. Throws `BadRequestError` (400) for index keys and timestamps, `NotFoundError` (404) when absent, `ConflictError` (409) when `condition` fails. See [Patching Fields Without Re-indexing](#patching-fields-without-re-indexing) |
180
184
  | `transitionEntity({ id, from?, set })` | Conditionally update by status: reads the entity, merges `set`, writes guarded by `#status = from`; throws `ConflictError` (409) on a race, `NotFoundError` (404) when absent. Validates `from`/`set.status` against the model's `status` vocabulary |
181
185
  | `deleteEntity({ id })` | Soft delete (`deletedAt`, `#deleted` suffix on GSI pk) |
182
186
  | `archiveEntity({ id })` | Archive (`archivedAt`, `#archived` suffix on GSI pk) |
183
187
  | `destroyEntity({ id })` | Hard delete (permanent) |
184
188
  | `transactWriteEntities({ entities, conditionalCreate?, condition?, preserveTimestamps? })` | Write many entities atomically; `conditionalCreate: true` guards every `Put` with `attribute_not_exists(id)` (`condition` for a custom expression), throwing `ConflictError` (409) when a conditional check fails |
185
189
 
190
+ ### Patching Fields Without Re-indexing
191
+
192
+ `updateEntity` writes the whole item with `PutItem` and sets `updatedAt` to the write time. For entities that are written often (counters, heartbeats, "last X" fields), that has two consequences:
193
+
194
+ 1. **The index entry moves.** `updatedAt` is part of every GSI sort key (`scope#updatedAt`), so each `updateEntity` moves the entity's `indexModel` entry (and every other index entry). DynamoDB moves a GSI entry by deleting it and inserting it again, asynchronously, and GSI reads are always eventually consistent. A `queryByScope` (or any other index query) that runs between the delete and the insert does not return the entity.
195
+ 2. **Stale fields overwrite newer ones.** The `Put` writes every attribute as the caller read it. When two read-modify-write calls overlap, the later one undoes the earlier one (for example, a status change or a credential rotation).
196
+
197
+ `patchEntity` sends a targeted `UpdateItem` that sets and removes only the listed attributes. It does not change `updatedAt` or any index key, so the entity stays in place in every index, and it never writes attributes the caller did not list.
198
+
199
+ ```typescript
200
+ import { patchEntity } from "@jaypie/dynamodb";
201
+
202
+ // Record a heartbeat without moving the entity in queryByScope
203
+ await patchEntity({
204
+ id: endpoint.id,
205
+ set: { lastDeliveryAt: new Date() },
206
+ });
207
+
208
+ // Set and remove in one write, guarded by a condition
209
+ const patched = await patchEntity({
210
+ id: endpoint.id,
211
+ set: { lastDeliveryAt: new Date(), lastStatus: 200 },
212
+ remove: ["lastError"],
213
+ condition: "#status = :active",
214
+ names: { "#status": "status" },
215
+ values: { ":active": "active" },
216
+ });
217
+ // patched: the full entity as stored after the write
218
+ ```
219
+
220
+ | Use | When |
221
+ |-----|------|
222
+ | `patchEntity` | Counters, heartbeats, "last X" timestamps, and any field that is not an index key and should not reorder listings |
223
+ | `updateEntity` | Changes to index keys (`scope`, `alias`, `category`, `type`, `xid`, custom index fields) or changes that should advance `updatedAt` |
224
+
225
+ - **Protected attributes.** `set` and `remove` cannot name `id`, `model`, `updatedAt`, `archivedAt`, `deletedAt`, any `index*` attribute, or any `pk`/`sk` field of a registered index (`scope`, `alias`, `category`, `type`, `xid`, and custom index fields). These throw `BadRequestError` (400) before any write, because changing them requires re-indexing through `updateEntity`. An empty patch also throws `BadRequestError`.
226
+ - **Existence.** Every patch is guarded by `attribute_exists(id)`, so it never creates an item. A missing entity throws `NotFoundError` (404).
227
+ - **Conditions.** `condition` is combined with the existence guard as `attribute_exists(id) AND (condition)`. When it fails, the item is left unchanged and `ConflictError` (409) is thrown. Placeholders `#s<n>`, `:s<n>`, and `#r<n>` are reserved for the generated expression; passing them in `names`/`values` throws `BadRequestError`.
228
+ - **Ordering.** `updatedAt` does not change, so listings sorted by `updatedAt` do not reflect a patch. Use `updateEntity` when a change should move the entity to the top of a listing.
229
+ - **Values.** `Date` values are stored as ISO 8601 strings. TTL is not changed.
230
+
186
231
  ### Preserving Timestamps (Imports)
187
232
 
188
233
  Every write stamps `updatedAt` with the write time, and every `scope#updatedAt` sort key follows it. Migrations, restores, and replays pass `preserveTimestamps: true` to `createEntity`, `updateEntity`, `transactWriteEntities`, or `seedEntities` (as an option) to keep the entity's own `createdAt`/`updatedAt`. Missing values fall back to now. TTL handling, date serialization, and conditional-write error mapping still apply, so raw `PutCommand` workarounds are unnecessary.
@@ -329,6 +374,8 @@ All queries return `{ items, lastEvaluatedKey }` and support pagination. `scope`
329
374
  import { APEX, queryByScope, queryByAlias, queryByCategory, queryByType, queryByXid } from "@jaypie/dynamodb";
330
375
 
331
376
  // List by model (scope optional)
377
+ // Each updateEntity moves the entity's indexModel entry; patch frequently
378
+ // written fields with patchEntity so listings stay stable
332
379
  const { items } = await queryByScope({ model: "record", scope: APEX });
333
380
 
334
381
  // List across all scopes
package/skills/llm.md CHANGED
@@ -857,6 +857,8 @@ try {
857
857
 
858
858
  An incomplete `content` is the raw partial text, a string even when `format` was requested: a truncated answer is not the object the format promises. Check `status` (or use `incomplete: "throw"`) before reading `content` as the formatted object.
859
859
 
860
+ A `format` request answered with **no content** (zero output tokens, an empty candidate, a blocked prompt) settles incomplete with `error.detail` `Model returned no content: <stop reason>`, logged at `warn`, and `stopReason: "other"`. An adapter that supports the corrective turn tries it once first. A chain moves past an empty response like any cut-off, and an earlier partial is kept over a later empty one. When nothing but empty responses come back, `operate` throws `LlmIncompleteError` whatever `incomplete` says: there is no partial to return (issue #608).
861
+
860
862
  ### Provider Options in a Chain
861
863
 
862
864
  `providerOptions` belongs to the model it was written for. Per-call `providerOptions` reach the primary only (including its linger pass); a fallback receives only the `providerOptions` on its own entry, or none. Options shaped for one provider (OpenAI `reasoning`, Gemini `thinkingConfig`) would otherwise break every other provider in the chain. Prefer first-class `effort` and `temperature`, which translate per provider.
@@ -1068,7 +1070,7 @@ exactly like a provider rate limit.
1068
1070
  | ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1069
1071
  | `max_turns` | 429 | The model asked for another tool call after `turns` ran out. Nothing failed; the run did not converge. |
1070
1072
  | `tool_errors` | 502 | Tool execution failed six times in a row and the loop stopped. |
1071
- | `incomplete` | 502 | The provider cut the model off before it finished (an output token ceiling, a content filter). `content` holds the partial text; `error.detail` names the provider's reason and `stopReason` normalizes it. A fallback chain moves past it. |
1073
+ | `incomplete` | 502 | The provider cut the model off before it finished (an output token ceiling, a content filter). `content` holds the partial text; `error.detail` names the provider's reason and `stopReason` normalizes it. A fallback chain moves past it. With `format`, empty content settles here too and throws once the chain is spent. |
1072
1074
 
1073
1075
  ```typescript
1074
1076
  import { LlmResponseErrorReason } from "@jaypie/llm";