wontopos-mcp 1.0.7 → 1.0.8

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 +27 -0
  2. package/index.mjs +72 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -107,6 +107,33 @@ automatic retries, redirect refusal, response caps, and key hygiene apply as-is.
107
107
 
108
108
  ## Changelog
109
109
 
110
+ - **1.0.8** - two bugs found by driving the server over stdio rather than reading
111
+ it. A store id with stray whitespace **split the store in two**: the client was
112
+ built from the raw env while the boot check, the ready line and `create_store`
113
+ used the trimmed value, so `create_store` made `alice` while every read and write
114
+ went to `" alice \n"` — a different collection — and the console still printed
115
+ `store "alice" verified`. And an oversized result was capped by slicing the
116
+ serialized JSON, handing the model a fragment with no closing bracket and a final
117
+ memory cut mid-sentence (measured: 131,041 bytes of unparseable text from a 764 KB
118
+ search). Now whole entries are dropped instead, the reply stays valid JSON, and
119
+ what was dropped is stated in the payload — a truncation nobody can see is worse
120
+ than a short answer.
121
+ - **1.0.7** - the version in the MCP handshake is read from `package.json` instead
122
+ of a hand-kept copy (1.0.6 announced itself as "1.0.5", so hosts logged and bug
123
+ reports cited the wrong build). `event_date` accepts a plain date: the API needs a
124
+ full RFC3339 date-TIME and answered a bare `2026-03-14` with an opaque 400 — the
125
+ form a model naturally emits, and the one this tool's own example suggested. A
126
+ date-only value is completed by string append, never through `Date`, so the stated
127
+ day cannot shift a zone. `remember` now says dedup matches by MEANING, not exact
128
+ text, and what to do when a new fact only narrows an existing one.
129
+ - **1.0.6** - `engram` takes a plain string, not an enum pinned to the original
130
+ three: every engram the service added afterwards was uncallable from here until
131
+ this package was republished. The API validates the name and answers 400 with the
132
+ list it accepts, so a new engram works the day it ships. The store fallback is no
133
+ longer silent — if `WONTOPOS_USER_ID` never reaches the process, everything goes to
134
+ `default`, and since every account is seeded with a `default` store the boot check
135
+ SUCCEEDS while the configured store sits untouched. Both the ready line and the
136
+ boot check now say so out loud.
110
137
  - **1.0.5** - correctness on the caps: they are now measured in UTF-8 **bytes**,
111
138
  not JS chars, so "128 KB" holds for Korean/CJK (a char cap let ~384 KB of
112
139
  Korean through). Result 128 KB, error 2 KB, content 8 KB, query 8 KB — all
package/index.mjs CHANGED
@@ -107,7 +107,14 @@ const model = process.env.WONTOPOS_MODEL?.trim();
107
107
 
108
108
  const mem = new Client({
109
109
  apiKey,
110
- userId: process.env.WONTOPOS_USER_ID || "default",
110
+ // STORE, not the raw env: the value is trimmed once at the top and everything —
111
+ // the boot check, the "ready" line, create_store — speaks in terms of that. Reading
112
+ // the env again here re-introduced the untrimmed value, so a config with a stray
113
+ // space or newline (easy in JSON/YAML env blocks, and heredocs add one) SPLIT the
114
+ // store in two: create_store made "alice" while every read and write went to
115
+ // " alice \n", which the API sanitizes into a different collection entirely — and
116
+ // the console still printed 'store "alice" verified'. One value, one store.
117
+ userId: STORE,
111
118
  // A stalled API must fail fast and in-band, not dangle until the MCP host
112
119
  // gives up. 20s covers a slow engram; override with WONTOPOS_TIMEOUT_MS.
113
120
  timeoutMs,
@@ -117,20 +124,75 @@ const mem = new Client({
117
124
 
118
125
  const server = new McpServer({ name: "wontopos", version: VERSION });
119
126
 
127
+ /** The result fields that hold lists of memories. Trimming happens here first,
128
+ * because dropping whole entries keeps the reply parseable. */
129
+ const LIST_FIELDS = ["memories", "self_memories", "results", "turns", "context"];
130
+
131
+ /** Shrink a result until it fits the byte cap, WITHOUT breaking the JSON.
132
+ *
133
+ * Cutting the serialized string mid-array (what this used to do) hands the model
134
+ * a fragment: no closing bracket, and a final memory sliced mid-sentence that
135
+ * reads as if that is all it said. Measured: a 764KB search came back as 131,041
136
+ * bytes of unparseable text. Drop whole entries instead and say how many went,
137
+ * so what arrives is valid JSON and its incompleteness is a stated fact.
138
+ * Returns the text to send. */
139
+ function fitToCap(result) {
140
+ const ser = (v) => JSON.stringify(v, null, 1);
141
+ let text = ser(result);
142
+ if (byteLen(text) <= MAX_RESULT_BYTES) return text;
143
+
144
+ // Trim the largest list field, longest-first, halving until it fits.
145
+ if (result && typeof result === "object") {
146
+ const clone = Array.isArray(result) ? [...result] : { ...result };
147
+ const dropped = {};
148
+ const lists = Array.isArray(clone)
149
+ ? [["", clone]]
150
+ : LIST_FIELDS.filter((f) => Array.isArray(clone[f]) && clone[f].length).map((f) => [f, clone[f]]);
151
+ if (lists.length) {
152
+ // Shrink every list proportionally until the whole payload fits. A plain
153
+ // loop, not a clever estimate: entry sizes vary wildly (one 8KB memory next
154
+ // to twenty short ones) so measuring is the only honest stop condition.
155
+ let guard = 64;
156
+ while (byteLen(text) > MAX_RESULT_BYTES && guard-- > 0) {
157
+ let cutAny = false;
158
+ for (const [name, arr] of lists) {
159
+ if (arr.length <= 1) continue;
160
+ const keep = Math.max(1, Math.floor(arr.length / 2));
161
+ dropped[name || "items"] = (dropped[name || "items"] ?? 0) + (arr.length - keep);
162
+ arr.length = keep;
163
+ cutAny = true;
164
+ }
165
+ if (!cutAny) break;
166
+ text = ser(Array.isArray(clone) ? clone : { ...clone, _truncated: dropped });
167
+ }
168
+ if (byteLen(text) <= MAX_RESULT_BYTES) {
169
+ const note =
170
+ `dropped ${Object.entries(dropped).map(([k, n]) => `${n} ${k}`).join(", ")} to fit the ` +
171
+ `${MAX_RESULT_BYTES}-byte cap — narrow the query, lower limit, or page with cursor.`;
172
+ // An array result (search) has nowhere to hang a note, so the cut would be
173
+ // SILENT — the model would read a short list as the whole answer. Append a
174
+ // sentinel entry instead: still valid JSON, still an array, and the reader
175
+ // is told what is missing. A truncation nobody can see is the worse bug.
176
+ return Array.isArray(clone) ? ser([...clone, { _truncated: note }]) : ser({ ...clone, _truncated: note });
177
+ }
178
+ }
179
+ }
180
+ // Last resort — a single entry too big to drop (one 400KB memory): the string
181
+ // cut, still labelled. Reserve the note's REAL byte length, not a guessed 160:
182
+ // the note grew past that guess and pushed the reply back over the cap, which is
183
+ // what the existing byte-cap tests caught.
184
+ const note =
185
+ `\n…[response truncated: ${byteLen(text)} bytes exceeded the ${MAX_RESULT_BYTES}-byte cap. ` +
186
+ "Narrow the query, lower limit, or page with cursor. NOTE: this text is a fragment, not valid JSON.]";
187
+ return truncateToBytes(text, MAX_RESULT_BYTES - byteLen(note)) + note;
188
+ }
189
+
120
190
  /** Wrap a handler: JSON out, typed WosError in-band so the agent can react. */
121
191
  function run(fn) {
122
192
  return async (args) => {
123
193
  try {
124
194
  const result = await fn(args ?? {});
125
- let text = JSON.stringify(result, null, 1);
126
- const bytes = byteLen(text);
127
- if (bytes > MAX_RESULT_BYTES) {
128
- // Leave room for the note itself so the whole reply stays under the cap.
129
- text =
130
- truncateToBytes(text, MAX_RESULT_BYTES - 160) +
131
- `\n…[response truncated: ${bytes} bytes exceeded the ${MAX_RESULT_BYTES}-byte cap. ` +
132
- "Narrow the query, lower limit, or page with cursor.]";
133
- }
195
+ const text = fitToCap(result);
134
196
  return { content: [{ type: "text", text }] };
135
197
  } catch (e) {
136
198
  // WosError.message already carries "[status] ... (request_id: ...)".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "mcpName": "com.wontopos/mcp",
5
5
  "description": "(beta) 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": [