wontopos-mcp 1.0.5 → 1.0.7

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 (2) hide show
  1. package/index.mjs +70 -12
  2. package/package.json +2 -2
package/index.mjs CHANGED
@@ -27,15 +27,26 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
27
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
28
28
  import { z } from "zod";
29
29
  import { Client, WosError } from "wontopos";
30
+ import { createRequire } from "node:module";
30
31
 
31
- const VERSION = "1.0.5";
32
+ // Read the version from package.json rather than restating it: a hand-kept copy
33
+ // silently drifts (1.0.6 shipped announcing itself as "1.0.5" in the MCP
34
+ // handshake, so hosts logged — and bug reports cited — the wrong build).
35
+ const VERSION = createRequire(import.meta.url)("./package.json").version;
32
36
  const flag = (v) => ["1", "true", "yes"].includes((v ?? "").trim().toLowerCase());
33
37
  const READ_ONLY = flag(process.env.WONTOPOS_READ_ONLY);
34
38
  // Store pinning (default ON): every call goes to WONTOPOS_USER_ID and the tools
35
39
  // do not even expose a user_id argument — a prompt-injected or confused model
36
40
  // cannot reach a sibling store. Opt out per install with ALLOW_STORE_OVERRIDE.
37
41
  const ALLOW_STORE_OVERRIDE = flag(process.env.WONTOPOS_ALLOW_STORE_OVERRIDE);
38
- const STORE = process.env.WONTOPOS_USER_ID || "default";
42
+ // Whether the store was CHOSEN or fallen back to. Worth tracking, because the
43
+ // fallback is silent in the worst way: if WONTOPOS_USER_ID never reaches this
44
+ // process (a typo in the host config, an --env that did not take), everything goes
45
+ // to `default` — and since every account is seeded with a `default` store, the boot
46
+ // check below SUCCEEDS, so nothing looks wrong while the configured store sits
47
+ // untouched. Say it out loud instead.
48
+ const STORE_FROM_ENV = (process.env.WONTOPOS_USER_ID || "").trim();
49
+ const STORE = STORE_FROM_ENV || "default";
39
50
  // Cap what a tool result may inject into the model's context. The SDK's 64MB cap
40
51
  // protects the process; this protects the conversation (a 2MB memory must not
41
52
  // become 2MB of context). Measured in UTF-8 BYTES, not JS chars: `str.length`
@@ -54,6 +65,22 @@ function truncateToBytes(s, maxBytes) {
54
65
  return s.slice(0, read);
55
66
  }
56
67
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
68
+ const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
69
+ /** Canonicalize event_date to the full RFC3339 the API requires.
70
+ * The API accepts only a complete date-TIME: a bare "2026-03-14" — the form a
71
+ * model naturally emits, and the form this tool's own example suggested — came
72
+ * back as an opaque 400 "Invalid request body or parameters." Normalize here
73
+ * instead of asking every agent to guess the shape.
74
+ * Returns null when the value is not a date at all, so the caller can refuse it. */
75
+ function normalizeEventDate(raw) {
76
+ const s = String(raw).trim();
77
+ if (Number.isNaN(Date.parse(s))) return null;
78
+ // Date-only is completed by string append, never through Date: parsing and
79
+ // re-serializing would re-interpret it in the host's zone and can shift the
80
+ // stated day by one, which is exactly the fact the user was pinning down.
81
+ if (DATE_ONLY_RE.test(s)) return `${s}T00:00:00Z`;
82
+ return new Date(s).toISOString();
83
+ }
57
84
 
58
85
  // First env var with a NON-EMPTY value wins. `??` only skips null/undefined, so
59
86
  // an empty WONTOPOS_API_KEY="" would shadow a valid WOS_API_KEY — the exact bug
@@ -167,7 +194,10 @@ server.registerTool(
167
194
  "('Sunwoo prefers dark mode in the console'), not a chat log. For your own side, store only " +
168
195
  "commitments you actually SAID to the user (speaker 'me') — never internal reasoning or thinking " +
169
196
  "output. Do NOT store fetched material: web search results, crawled pages, file contents, and " +
170
- "tool outputs are session context, not memories. Identical content is deduplicated (status 'duplicate').",
197
+ "tool outputs are session context, not memories. A memory close enough to one already stored is " +
198
+ "rejected as status 'duplicate' and is NOT saved — the match is by meaning, not by exact text, so " +
199
+ "when a new fact only narrows or contrasts an existing one ('allergic to shellfish' next to " +
200
+ "'allergic to peanuts'), write it as one sentence that states both, or supersede the old one with update.",
171
201
  inputSchema: {
172
202
  content: z.string().describe("The memory, as one self-contained sentence."),
173
203
  speaker: z
@@ -177,7 +207,7 @@ server.registerTool(
177
207
  event_date: z
178
208
  .string()
179
209
  .nullish()
180
- .describe("RFC3339 date when the event actually happened, if different from now."),
210
+ .describe("When the event actually happened, if different from now. A plain date ('2026-03-14') or a full RFC3339 timestamp; a bare date is read as UTC midnight."),
181
211
  ...storeArg,
182
212
  },
183
213
  },
@@ -187,11 +217,12 @@ server.registerTool(
187
217
  throw new Error("content too long (max 8192 bytes) — a memory is one fact; backfill large text with the SDK's add_bulk.");
188
218
  if (speaker && speaker !== "me")
189
219
  throw new Error("speaker must be 'me' (something the agent itself said) or omitted (the user's fact).");
190
- if (event_date && Number.isNaN(Date.parse(event_date)))
191
- throw new Error("event_date is not a parseable date — use RFC3339, e.g. 2026-07-01.");
220
+ const eventDate = event_date ? normalizeEventDate(event_date) : null;
221
+ if (event_date && !eventDate)
222
+ throw new Error("event_date is not a parseable date — use a date (2026-07-01) or RFC3339 (2026-07-01T09:00:00Z).");
192
223
  const metadata = {};
193
224
  if (speaker) metadata.speaker = speaker;
194
- if (event_date) metadata.event_date = event_date;
225
+ if (eventDate) metadata.event_date = eventDate;
195
226
  return mem.add(content, uid(user_id), metadata);
196
227
  })
197
228
  );
@@ -306,10 +337,24 @@ server.registerTool(
306
337
  "'deep_recall' searches then expands around the top match — thorough answer to one specific " +
307
338
  "question; 'timeline' returns memories ordered by when events happened — 'when did X', " +
308
339
  "histories; 'gather' expands around the top few matches — collect everything related to a " +
309
- "topic. Heavier than search, so reach for it when recall/search came back thin or the user " +
310
- "asks for a full picture.",
340
+ "topic; 'equilibrium' re-widens retrieval when a long session has narrowed it to whatever " +
341
+ "the current turn resembles — for replies that loop or flatten, not for a factual lookup; " +
342
+ "'tone_stabilizer' returns the assistant's OWN earlier words so it can come back to its " +
343
+ "usual register (needs turns stored with speaker \"me\"). " +
344
+ "Heavier than search, so reach for it when recall/search came back thin or the user asks " +
345
+ "for a full picture. Call the API's GET /api/v1/engram for the live catalogue.",
311
346
  inputSchema: {
312
- name: z.enum(["deep_recall", "timeline", "gather"]),
347
+ // A plain string, NOT an enum: the enum was pinned to the original three, so
348
+ // every engram the service added afterwards was uncallable from here until
349
+ // this package was republished. The API validates the name and answers 400
350
+ // with the list it accepts, so an unknown name fails clearly and a new engram
351
+ // works the day it ships.
352
+ name: z
353
+ .string()
354
+ .min(1)
355
+ .describe(
356
+ "Engram name: deep_recall | timeline | gather | equilibrium | tone_stabilizer (the service is the authority; an unknown name returns the accepted list)."
357
+ ),
313
358
  query: z.string().describe("What to retrieve, phrased like the user's need."),
314
359
  ...storeArg,
315
360
  },
@@ -367,13 +412,26 @@ await server.connect(transport);
367
412
  return inner?.(msg, extra);
368
413
  };
369
414
  }
370
- console.error(`wontopos-mcp ${VERSION} ready (store: ${STORE}${READ_ONLY ? ", read-only" : ""})`);
415
+ console.error(
416
+ `wontopos-mcp ${VERSION} ready (store: ${STORE}${STORE_FROM_ENV ? "" : " — FALLBACK: WONTOPOS_USER_ID is not set"}${READ_ONLY ? ", read-only" : ""})`
417
+ );
371
418
 
372
419
  // Boot check (one metered stats call, after connect so host startup never waits):
373
420
  // "ready" above is a claim — verify it. A wrong key can do nothing → exit loudly.
374
421
  // A missing store is recoverable (the agent can create_store) → warn and serve.
375
422
  mem.stats().then(
376
- (s) => console.error(`wontopos-mcp: store "${STORE}" verified (${s?.total_memories ?? "?"} memories).`),
423
+ (s) => {
424
+ console.error(`wontopos-mcp: store "${STORE}" verified (${s?.total_memories ?? "?"} memories).`);
425
+ // Verified, but on the fallback store — the case a misconfiguration hides in.
426
+ // The seeded `default` always exists, so this check passes and the operator has
427
+ // no other signal that the store they configured is not the one in use.
428
+ if (!STORE_FROM_ENV) {
429
+ console.error(
430
+ 'wontopos-mcp: WONTOPOS_USER_ID is not set, so this server is using the seeded "default" store. ' +
431
+ "If you meant a different store, set WONTOPOS_USER_ID in the MCP server's env and restart."
432
+ );
433
+ }
434
+ },
377
435
  (e) => {
378
436
  const status = e instanceof WosError ? e.status : 0;
379
437
  if (status === 401) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
- "wontopos": "^2.2.13",
35
+ "wontopos": "^2.2.22",
36
36
  "zod": "^3.23.0"
37
37
  }
38
38
  }