wontopos-mcp 1.0.19 → 1.0.20

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 (3) hide show
  1. package/README.md +9 -0
  2. package/index.mjs +24 -7
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -224,6 +224,15 @@ advertise different limits.
224
224
 
225
225
  ## Changelog
226
226
 
227
+ - **1.0.20** - a trimmed reply no longer misreports its own size. When a result is
228
+ larger than the 128KB cap the server cuts the arrays inside it, and the sibling
229
+ `count` field kept the number from before the cut: a model was handed 100 memories
230
+ beside `"count": 200` and reasonably read that as JSON truncated mid-structure. The
231
+ count now says what is actually there. The advice that follows it - "ask for this
232
+ page again with limit=N" - took N from the longest array anywhere in the reply, so
233
+ it could name a nested list that had nothing to do with the page; it reads the page's
234
+ own list now. Underneath, the `wontopos` SDK pin moves to **2.2.38**.
235
+
227
236
  - **1.0.19** - out of beta, and the release that made `recall` keep the other half of
228
237
  its promise. It opens by offering "recent turns", and nothing in this server could
229
238
  ever write one: turns go through their own API route, every tool here wrote
package/index.mjs CHANGED
@@ -214,22 +214,33 @@ const RECALL_TURNS = 20;
214
214
 
215
215
  function droppableLists(root) {
216
216
  const out = [];
217
- const stack = [["", root]];
217
+ const stack = [["", root, null]];
218
218
  let budget = 20000;
219
219
  while (stack.length && budget-- > 0) {
220
- const [name, node] = stack.pop();
220
+ const [name, node, owner] = stack.pop();
221
221
  if (Array.isArray(node)) {
222
- if (node.length > 1 && node.some((e) => e && typeof e === "object")) out.push([name || "items", node]);
223
- for (const v of node) if (v && typeof v === "object") stack.push([name, v]);
222
+ // `owner` is the object holding this array, so a sibling `count` can be corrected
223
+ // when the array is cut. Without it the trimmer handed a model 100 memories beside
224
+ // "count": 200 — and the schemas call that field authoritative, so the model reads
225
+ // the JSON as truncated mid-structure and either retries or reports the store wrong.
226
+ if (node.length > 1 && node.some((e) => e && typeof e === "object")) out.push([name || "items", node, owner]);
227
+ for (const v of node) if (v && typeof v === "object") stack.push([name, v, null]);
224
228
  continue;
225
229
  }
226
230
  for (const [k, v] of Object.entries(node)) {
227
- if (v && typeof v === "object") stack.push([Array.isArray(v) ? k : name, v]);
231
+ if (v && typeof v === "object") stack.push([Array.isArray(v) ? k : name, v, Array.isArray(v) ? node : null]);
228
232
  }
229
233
  }
230
234
  return out;
231
235
  }
232
236
 
237
+ /** After an array is cut, make its sibling `count` say what is actually there. Only the
238
+ * field literally named `count`, and only when it already matched the old length —
239
+ * `total_found` and friends mean something else and are left alone. */
240
+ function syncCount(owner, arr, before) {
241
+ if (owner && typeof owner.count === "number" && owner.count === before) owner.count = arr.length;
242
+ }
243
+
233
244
  function truncationKey(obj) {
234
245
  let k = "_truncated";
235
246
  while (Object.prototype.hasOwnProperty.call(obj, k)) k = `_${k}`;
@@ -285,12 +296,14 @@ function fitToCap(result) {
285
296
  let guard = 64;
286
297
  while (byteLen(text) > MAX_RESULT_BYTES && guard-- > 0) {
287
298
  let cutAny = false;
288
- for (const [name, arr] of lists) {
299
+ for (const [name, arr, owner] of lists) {
289
300
  if (arr.length <= 1) continue;
301
+ const before = arr.length;
290
302
  const keep = Math.max(1, Math.floor(arr.length / 2));
291
303
  dropped[name || "items"] = (dropped[name || "items"] ?? 0) + (arr.length - keep);
292
304
  if (KEEP_TAIL.has(name)) arr.splice(0, arr.length - keep);
293
305
  else arr.length = keep;
306
+ syncCount(owner, arr, before);
294
307
  cutAny = true;
295
308
  }
296
309
  if (!cutAny) break;
@@ -300,7 +313,11 @@ function fitToCap(result) {
300
313
  const paged = ["next_cursor", "next_before", "next_skip_ids"].some(
301
314
  (k) => !Array.isArray(clone) && clone[k] != null
302
315
  );
303
- const kept = lists.reduce((n, [, arr]) => Math.max(n, arr.length), 0);
316
+ // The page's own list, not the longest array anywhere in the reply: `kept` used
317
+ // to be a Math.max over every list, so the advice could name the length of a
318
+ // nested array and send the model back for a page that overflows again.
319
+ const pageList = lists.find(([name]) => !KEEP_TAIL.has(name) && dropped[name || "items"]);
320
+ const kept = pageList ? pageList[1].length : 0;
304
321
  const note =
305
322
  `dropped ${Object.entries(dropped).map(([k, n]) => `${n} ${k}`).join(", ")} to fit the ` +
306
323
  (paged
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos-mcp",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "mcpName": "com.wontopos/mcp",
5
5
  "description": "Wontopos (WOS) long-term memory as an MCP server — one memory across Claude Code, Claude Desktop, Cursor, ChatGPT, and your own agents.",
6
6
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.30.0",
37
- "wontopos": "2.2.37",
37
+ "wontopos": "2.2.38",
38
38
  "zod": "^3.23.0"
39
39
  },
40
40
  "overrides": {