@mnemoverse/mcp-memory-server 0.10.2 → 0.12.0

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.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * MCP prompts: three named entry points to the memory tools.
3
+ *
4
+ * Clients that support the prompts capability show them as commands (Claude
5
+ * Code as `/mcp__mnemoverse__<name>`, Claude.ai connectors in the slash menu).
6
+ * Each returns one user message that points the model at the right tool. They
7
+ * add no capability beyond the tools; a client without prompt support ignores
8
+ * them.
9
+ *
10
+ * Moved here from the hosted connector (mnemoverse-mcp-remote,
11
+ * src/prompts/index.ts) in step 3c of the ADR-025 plan, with the same names,
12
+ * arguments and wording, so the connector can register these instead of its
13
+ * copy. One difference is deliberate: `domain` in save_insight is printed as
14
+ * an exact JSON literal (src/names.ts), like every domain this package names,
15
+ * because the model has to send it back to memory_write byte for byte. For a
16
+ * plain domain the message is identical to the connector's. The other
17
+ * arguments are trimmed, as the connector trims them, because a blank topic is
18
+ * not a request.
19
+ */
20
+ import { z } from "zod";
21
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
22
+ import { exactLiteral, withDomainEscapeLegend } from "./names.js";
23
+ /**
24
+ * Register the three memory prompts on `server`. They call nothing: each
25
+ * renders a message that asks the model to use memory_read or memory_write.
26
+ */
27
+ export function registerMemoryPrompts(server) {
28
+ server.registerPrompt("recall", {
29
+ title: "Recall from memory",
30
+ description: "Search your Mnemoverse memory for saved information about a specific topic.",
31
+ argsSchema: {
32
+ topic: z
33
+ .string()
34
+ .trim()
35
+ .min(1)
36
+ .describe("What to recall — a topic, project, person, or open question."),
37
+ },
38
+ }, ({ topic }) => ({
39
+ messages: [
40
+ {
41
+ role: "user",
42
+ content: {
43
+ type: "text",
44
+ text: `Use memory_read to search my saved Mnemoverse memories for this specific topic: "${topic}". Summarize only the returned memory text. If nothing relevant is stored, say so plainly.`,
45
+ },
46
+ },
47
+ ],
48
+ }));
49
+ server.registerPrompt("save_insight", {
50
+ title: "Save an insight to memory",
51
+ description: "Store a new insight, fact, or decision in your Mnemoverse long-term memory.",
52
+ argsSchema: {
53
+ insight: z
54
+ .string()
55
+ .trim()
56
+ .min(1)
57
+ .describe("The insight, fact, or decision to remember."),
58
+ domain: z
59
+ .string()
60
+ .optional()
61
+ .describe("Optional domain/category to file it under (e.g. 'project-x')."),
62
+ },
63
+ }, ({ insight, domain }) => {
64
+ // The domain is an identifier the model must reproduce exactly, so it is
65
+ // printed as a JSON literal: a quote, backslash, newline or invisible
66
+ // character inside it is escaped instead of closing the quotes early
67
+ // (Copilot on #148). A plain domain prints exactly as the connector's
68
+ // `"${domain}"` did. A domain too long to print exactly is refused
69
+ // rather than named inexactly or dropped, since dropping it would file
70
+ // the insight somewhere the user did not ask for.
71
+ let under = "";
72
+ if (domain) {
73
+ const exact = exactLiteral(domain);
74
+ if (exact === null) {
75
+ throw new McpError(ErrorCode.InvalidParams, "save_insight: this domain is too long to be quoted exactly in the message. Pass a shorter domain.");
76
+ }
77
+ under = ` under the domain ${exact.literal}`;
78
+ }
79
+ const text = `Use the memory_write tool to store this insight in my long-term memory${under}: "${insight}". Then confirm exactly what was stored.`;
80
+ return {
81
+ messages: [
82
+ {
83
+ role: "user",
84
+ content: {
85
+ type: "text",
86
+ text: withDomainEscapeLegend(text, domain),
87
+ },
88
+ },
89
+ ],
90
+ };
91
+ });
92
+ server.registerPrompt("what_do_you_know", {
93
+ title: "What do you know about…",
94
+ description: "Get a briefing of what your memory holds about a subject.",
95
+ argsSchema: {
96
+ subject: z.string().trim().min(1).describe("The subject to brief on."),
97
+ },
98
+ }, ({ subject }) => ({
99
+ messages: [
100
+ {
101
+ role: "user",
102
+ content: {
103
+ type: "text",
104
+ text: `Use memory_read to look up what I have explicitly stored about "${subject}". Give me a concise briefing based only on the returned memory text and flag gaps where nothing is stored.`,
105
+ },
106
+ },
107
+ ],
108
+ }));
109
+ }
110
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAElE;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAiB;IACrD,MAAM,CAAC,cAAc,CACnB,QAAQ,EACR;QACE,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EAAE,6EAA6E;QAC1F,UAAU,EAAE;YACV,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,IAAI,EAAE;iBACN,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,8DAA8D,CAAC;SAC5E;KACF,EACD,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAe;gBACrB,OAAO,EAAE;oBACP,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,oFAAoF,KAAK,4FAA4F;iBAC5L;aACF;SACF;KACF,CAAC,CACH,CAAC;IAEF,MAAM,CAAC,cAAc,CACnB,cAAc,EACd;QACE,KAAK,EAAE,2BAA2B;QAClC,WAAW,EAAE,6EAA6E;QAC1F,UAAU,EAAE;YACV,OAAO,EAAE,CAAC;iBACP,MAAM,EAAE;iBACR,IAAI,EAAE;iBACN,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,6CAA6C,CAAC;YAC1D,MAAM,EAAE,CAAC;iBACN,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,+DAA+D,CAAC;SAC7E;KACF,EACD,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE;QACtB,yEAAyE;QACzE,sEAAsE;QACtE,qEAAqE;QACrE,sEAAsE;QACtE,mEAAmE;QACnE,uEAAuE;QACvE,kDAAkD;QAClD,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,aAAa,EACvB,mGAAmG,CACpG,CAAC;YACJ,CAAC;YACD,KAAK,GAAG,qBAAqB,KAAK,CAAC,OAAO,EAAE,CAAC;QAC/C,CAAC;QACD,MAAM,IAAI,GAAG,yEAAyE,KAAK,MAAM,OAAO,0CAA0C,CAAC;QACnJ,OAAO;YACL,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,MAAe;oBACrB,OAAO,EAAE;wBACP,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,sBAAsB,CAAC,IAAI,EAAE,MAAM,CAAC;qBAC3C;iBACF;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,cAAc,CACnB,kBAAkB,EAClB;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE,2DAA2D;QACxE,UAAU,EAAE;YACV,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,0BAA0B,CAAC;SACvE;KACF,EACD,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QAChB,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAe;gBACrB,OAAO,EAAE;oBACP,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,mEAAmE,OAAO,6GAA6G;iBAC9L;aACF;SACF;KACF,CAAC,CACH,CAAC;AACJ,CAAC"}
package/dist/render.d.ts CHANGED
@@ -75,8 +75,82 @@ export type RecentItem = {
75
75
  */
76
76
  export declare function safeInline(s: unknown, cap?: number): string;
77
77
  /**
78
- * ` [by X]` / ` [by X · external]` — agent identity only, never the human
79
- * `principal` (may be an email / PII), even though the response carries it.
78
+ * `agent_name || agent || client_env`, exactly as the wire sent it: no
79
+ * sanitising, no suffix. The single raw value `authorName` and
80
+ * `formatAuthorTag` below BOTH derive from, so the bare structured field and
81
+ * the quoted text tag cannot end up naming two different agents. The same
82
+ * raw value is what src/tools.ts passes to `withDomainEscapeLegend` as an
83
+ * author candidate, so its independent recomputation of `exactLiteral` finds
84
+ * the same literal `formatAuthorTag` printed (I66-3, issue #66).
85
+ *
86
+ * Returns `""`, not `null`/`undefined`, when there is no renderable name at
87
+ * all: an absent provenance, every field empty, or the field that won the
88
+ * `||` chain arriving with the wrong wire type. A numeric `agent_name` is a
89
+ * real shape a hostile or buggy connector sends, not a hypothetical one; see
90
+ * "a non-string where the type promised a string" below, and CN-032's own
91
+ * history with this exact field.
92
+ */
93
+ export declare function rawAuthorName(p?: Provenance | null): string;
94
+ /**
95
+ * "X" / "X · external": agent identity only, never the human `principal`
96
+ * (may be an email / PII), even though the response carries it. Empty string
97
+ * when there is no renderable name.
98
+ *
99
+ * Feeds `structuredContent.author` (via `structuredItem`, below), not the
100
+ * text tag; `formatAuthorTag` now quotes `rawAuthorName`'s value itself
101
+ * (I66-1) rather than building its bracketed text from this one, so the two
102
+ * no longer share a derivation the way the original S4 extraction intended,
103
+ * and instead share `rawAuthorName` as their common raw input.
104
+ *
105
+ * Until owner decision I66-2 (issue #66, 2026-09-23), this ran the raw name
106
+ * through `safeInline`, the same ASCII-only, bracket-stripping sanitiser the
107
+ * text tag used, which erased Cyrillic/CJK/Arabic names from the DATA just
108
+ * as completely as it erased them from the text: a second, previously
109
+ * unreported instance of the bug the issue reports for the tag, found with
110
+ * zero non-Latin fixtures anywhere in this file's history. It now runs
111
+ * through `structuredText` (src/names.ts) instead, the same
112
+ * control/bidi/zero-width-only normalisation `reason` already gets on this
113
+ * surface, so a name survives here whenever it survives on the page.
114
+ *
115
+ * This deliberately drops bracket-stripping on this field: a bare JSON
116
+ * value cannot be "closed early" by a literal `]` or `"` the way a
117
+ * hand-built sentence can (the MCP SDK serialises the field; this code does
118
+ * not concatenate it into one). The property CN-032 actually needs here, a
119
+ * reader must not be shown something that is not there or have text
120
+ * hidden/reordered inside it, is exactly what `structuredText` still strips
121
+ * (control, bidi, zero-width). The `[by "…"]` TEXT tag carries the
122
+ * anti-injection burden for the rendered surface instead, via
123
+ * `exactLiteral`'s quoting.
124
+ */
125
+ export declare function authorName(p?: Provenance | null): string;
126
+ /**
127
+ * ` [by "X"]` / ` [by "X" · external]`: agent identity only, never the
128
+ * human `principal` (may be an email / PII), even though the response
129
+ * carries it.
130
+ *
131
+ * Quoted as an exact JSON literal (`exactLiteral`, src/names.ts), the same
132
+ * treatment `formatDomainTag` below already gives `@domain`, for EVERY
133
+ * name, including a plain ASCII one like "sigma", not only names that
134
+ * happen to need escaping (owner decision I66-1, issue #66, 2026-09-24:
135
+ * full symmetry with domains, no second unquoted branch). Before this, a
136
+ * Cyrillic, CJK or Arabic `agent_name` sanitised through `safeInline`'s
137
+ * ASCII-only charset to an empty string, and the tag disappeared with no
138
+ * trace, the exact defect `formatDomainTag` was already fixed for in 0.8.1,
139
+ * reached here in a later release (issue #66).
140
+ *
141
+ * Operates on `rawAuthorName`, not `authorName`'s sanitised value: quoting
142
+ * the raw wire name is what lets `exactLiteral` print and escape characters
143
+ * a sanitiser would have dropped, and what lets `withDomainEscapeLegend`'s
144
+ * independent recomputation (src/tools.ts) find the same literal this
145
+ * function printed.
146
+ *
147
+ * `" · external"` sits OUTSIDE the quotes (I66-3): it is a server-added
148
+ * qualifier, not part of the name, so it is not part of what gets escaped
149
+ * and not part of what the escape-legend candidate has to match.
150
+ *
151
+ * The cap is `MAX_DOMAIN_TAG_LITERAL` itself, not a second constant with the
152
+ * same value (I66-4): a name too long to print exactly gets the same
153
+ * disclosed, name-free fallback `formatDomainTag` uses, reworded for "name".
80
154
  */
81
155
  export declare function formatAuthorTag(p?: Provenance | null): string;
82
156
  /**
@@ -159,11 +233,48 @@ export declare function formatDateTag(createdAt?: string): string;
159
233
  * it; we simply do not put it in front of a reader yet.
160
234
  */
161
235
  export declare function formatReadItem(item: ReadItem, index: number): string;
236
+ /**
237
+ * The `structuredContent` twin of {@link formatReadItem} (S4, structured-output
238
+ * plan): the same item, shaped for memory_read's `outputSchema` instead of for
239
+ * a line of text.
240
+ *
241
+ * PRECONDITION, enforced by the caller (src/tools.ts) before this is ever
242
+ * invoked: `item.atom_id`, `item.content` and `item.domain` are all strings.
243
+ * core's MemoryItemSchema sends all three on every item; a response that
244
+ * doesn't is caught by the handler's item guard and answered with
245
+ * `unreadableAnswerReply` before `structuredItem` is reached, so the casts
246
+ * below are a documented precondition, not a runtime assumption made here.
247
+ *
248
+ * `content` is carried EXACTLY, uncapped and unnormalised (decision OD-11,
249
+ * owner, 2026-09-23): unlike the text line, which goes through `capResult`
250
+ * for the 25K-token result-size cap, `structuredContent` is not capped
251
+ * anywhere else in this package either (memory_write's `reason` is the only
252
+ * normalised structured field, and that's control/bidi/zero-width hygiene on
253
+ * a diagnostic string, not a length cap on the memory itself); a client
254
+ * reading structured data reads `content` as the stored memory, and a
255
+ * silently shorter value there would be a different kind of lie than a
256
+ * truncated text block with a notice at the end.
257
+ */
258
+ export declare function structuredItem(item: ReadItem): {
259
+ memory_id: string;
260
+ content: string;
261
+ domain: string;
262
+ created_at?: string;
263
+ author?: string;
264
+ };
162
265
  /**
163
266
  * One memory_list_recent line — a feed entry, not a search hit: no
164
267
  * relevance, date leads because the feed is ORDERED by it.
165
268
  */
166
269
  export declare function formatRecentItem(item: RecentItem, index: number): string;
270
+ /**
271
+ * The shape of a continuation token this client is willing to pass on
272
+ * (CN-032): the server-supplied cursor is opaque, so this is an allowlist
273
+ * of bytes, not a format. One constant for BOTH surfaces, the text (below)
274
+ * and `structuredContent.next_cursor` (src/tools.ts), so they cannot
275
+ * disagree about which token is passable.
276
+ */
277
+ export declare const CURSOR_RE: RegExp;
167
278
  /**
168
279
  * Full feed page: items newest-first + how to continue / that it's over.
169
280
  *
@@ -175,4 +286,4 @@ export declare function formatRecentItem(item: RecentItem, index: number): strin
175
286
  * 2026-08-08). Once-per-answer still holds — withDomainEscapeLegend is
176
287
  * at-most-once by construction (src/names.ts).
177
288
  */
178
- export declare function formatRecentPage(items: RecentItem[], nextCursor?: string | null): string;
289
+ export declare function formatRecentPage(items: RecentItem[], nextCursor?: unknown): string;
package/dist/render.js CHANGED
@@ -10,8 +10,8 @@
10
10
  * shapes without atom_id/created_at degrade gracefully: those parts of
11
11
  * the line are simply omitted.
12
12
  */
13
- import { MAX_DOMAIN_TAG_LITERAL, exactLiteral } from "./names.js";
14
- import { parseAsUtc } from "./time.js";
13
+ import { MAX_DOMAIN_TAG_LITERAL, exactLiteral, structuredText } from "./names.js";
14
+ import { parseAsUtc, utcInstant } from "./time.js";
15
15
  /**
16
16
  * Sanitize a string for inline interpolation into tool output (CN-032: hostile
17
17
  * connectors choose their own agent_name). The single implementation, imported
@@ -60,17 +60,115 @@ export function safeInline(s, cap = 200) {
60
60
  .slice(0, cap);
61
61
  }
62
62
  /**
63
- * ` [by X]` / ` [by X · external]` — agent identity only, never the human
64
- * `principal` (may be an email / PII), even though the response carries it.
63
+ * `agent_name || agent || client_env`, exactly as the wire sent it: no
64
+ * sanitising, no suffix. The single raw value `authorName` and
65
+ * `formatAuthorTag` below BOTH derive from, so the bare structured field and
66
+ * the quoted text tag cannot end up naming two different agents. The same
67
+ * raw value is what src/tools.ts passes to `withDomainEscapeLegend` as an
68
+ * author candidate, so its independent recomputation of `exactLiteral` finds
69
+ * the same literal `formatAuthorTag` printed (I66-3, issue #66).
70
+ *
71
+ * Returns `""`, not `null`/`undefined`, when there is no renderable name at
72
+ * all: an absent provenance, every field empty, or the field that won the
73
+ * `||` chain arriving with the wrong wire type. A numeric `agent_name` is a
74
+ * real shape a hostile or buggy connector sends, not a hypothetical one; see
75
+ * "a non-string where the type promised a string" below, and CN-032's own
76
+ * history with this exact field.
65
77
  */
66
- export function formatAuthorTag(p) {
78
+ export function rawAuthorName(p) {
67
79
  if (!p)
68
80
  return "";
69
- const raw = p.agent_name || p.agent || p.client_env || "";
70
- const who = safeInline(raw, 64);
81
+ const raw = p.agent_name || p.agent || p.client_env;
82
+ return typeof raw === "string" ? raw : "";
83
+ }
84
+ /**
85
+ * "X" / "X · external": agent identity only, never the human `principal`
86
+ * (may be an email / PII), even though the response carries it. Empty string
87
+ * when there is no renderable name.
88
+ *
89
+ * Feeds `structuredContent.author` (via `structuredItem`, below), not the
90
+ * text tag; `formatAuthorTag` now quotes `rawAuthorName`'s value itself
91
+ * (I66-1) rather than building its bracketed text from this one, so the two
92
+ * no longer share a derivation the way the original S4 extraction intended,
93
+ * and instead share `rawAuthorName` as their common raw input.
94
+ *
95
+ * Until owner decision I66-2 (issue #66, 2026-09-23), this ran the raw name
96
+ * through `safeInline`, the same ASCII-only, bracket-stripping sanitiser the
97
+ * text tag used, which erased Cyrillic/CJK/Arabic names from the DATA just
98
+ * as completely as it erased them from the text: a second, previously
99
+ * unreported instance of the bug the issue reports for the tag, found with
100
+ * zero non-Latin fixtures anywhere in this file's history. It now runs
101
+ * through `structuredText` (src/names.ts) instead, the same
102
+ * control/bidi/zero-width-only normalisation `reason` already gets on this
103
+ * surface, so a name survives here whenever it survives on the page.
104
+ *
105
+ * This deliberately drops bracket-stripping on this field: a bare JSON
106
+ * value cannot be "closed early" by a literal `]` or `"` the way a
107
+ * hand-built sentence can (the MCP SDK serialises the field; this code does
108
+ * not concatenate it into one). The property CN-032 actually needs here, a
109
+ * reader must not be shown something that is not there or have text
110
+ * hidden/reordered inside it, is exactly what `structuredText` still strips
111
+ * (control, bidi, zero-width). The `[by "…"]` TEXT tag carries the
112
+ * anti-injection burden for the rendered surface instead, via
113
+ * `exactLiteral`'s quoting.
114
+ */
115
+ export function authorName(p) {
116
+ // Present exactly when the text tag prints the name (Sigma, review round
117
+ // 3 on #66): the same exactLiteral check under MAX_DOMAIN_TAG_LITERAL
118
+ // decides, so a name the tag refuses ("(name cannot be printed exactly)")
119
+ // is withheld here too, and a name the tag prints is carried whole (the
120
+ // structuredText normalisation, never a truncated prefix). The old cap of
121
+ // 64 code points, inherited from safeInline, let the data hold a shorter
122
+ // name than the page showed, with nothing marking the cut.
123
+ const raw = rawAuthorName(p);
124
+ if (!raw || !exactLiteral(raw, MAX_DOMAIN_TAG_LITERAL))
125
+ return "";
126
+ // One stated exception: a name made only of the characters structuredText
127
+ // removes (whitespace, control, bidi, zero-width) is printed exactly in
128
+ // the tag, as an escaped literal with the legend, but leaves nothing to
129
+ // carry as a plain data value; the data omits the field rather than carry
130
+ // "" or the raw characters.
131
+ const who = structuredText(raw, MAX_DOMAIN_TAG_LITERAL);
71
132
  if (!who)
72
133
  return "";
73
- return p.is_external ? ` [by ${who} · external]` : ` [by ${who}]`;
134
+ return p?.is_external === true ? `${who} · external` : who;
135
+ }
136
+ /**
137
+ * ` [by "X"]` / ` [by "X" · external]`: agent identity only, never the
138
+ * human `principal` (may be an email / PII), even though the response
139
+ * carries it.
140
+ *
141
+ * Quoted as an exact JSON literal (`exactLiteral`, src/names.ts), the same
142
+ * treatment `formatDomainTag` below already gives `@domain`, for EVERY
143
+ * name, including a plain ASCII one like "sigma", not only names that
144
+ * happen to need escaping (owner decision I66-1, issue #66, 2026-09-24:
145
+ * full symmetry with domains, no second unquoted branch). Before this, a
146
+ * Cyrillic, CJK or Arabic `agent_name` sanitised through `safeInline`'s
147
+ * ASCII-only charset to an empty string, and the tag disappeared with no
148
+ * trace, the exact defect `formatDomainTag` was already fixed for in 0.8.1,
149
+ * reached here in a later release (issue #66).
150
+ *
151
+ * Operates on `rawAuthorName`, not `authorName`'s sanitised value: quoting
152
+ * the raw wire name is what lets `exactLiteral` print and escape characters
153
+ * a sanitiser would have dropped, and what lets `withDomainEscapeLegend`'s
154
+ * independent recomputation (src/tools.ts) find the same literal this
155
+ * function printed.
156
+ *
157
+ * `" · external"` sits OUTSIDE the quotes (I66-3): it is a server-added
158
+ * qualifier, not part of the name, so it is not part of what gets escaped
159
+ * and not part of what the escape-legend candidate has to match.
160
+ *
161
+ * The cap is `MAX_DOMAIN_TAG_LITERAL` itself, not a second constant with the
162
+ * same value (I66-4): a name too long to print exactly gets the same
163
+ * disclosed, name-free fallback `formatDomainTag` uses, reworded for "name".
164
+ */
165
+ export function formatAuthorTag(p) {
166
+ const raw = rawAuthorName(p);
167
+ if (!raw)
168
+ return "";
169
+ const exact = exactLiteral(raw, MAX_DOMAIN_TAG_LITERAL);
170
+ const printed = exact ? exact.literal : "(name cannot be printed exactly)";
171
+ return ` [by ${printed}${p?.is_external === true ? " · external" : ""}]`;
74
172
  }
75
173
  /**
76
174
  * ` @"domain"` — which store the memory actually came from, printed so it can
@@ -170,6 +268,45 @@ export function formatReadItem(item, index) {
170
268
  const head = `${index + 1}. ${content}${concepts}${formatDomainTag(item?.domain)}${formatAuthorTag(item?.provenance)}${formatDateTag(item?.created_at)}`;
171
269
  return item?.atom_id ? `${head}\n id: ${item.atom_id}` : head;
172
270
  }
271
+ /**
272
+ * The `structuredContent` twin of {@link formatReadItem} (S4, structured-output
273
+ * plan): the same item, shaped for memory_read's `outputSchema` instead of for
274
+ * a line of text.
275
+ *
276
+ * PRECONDITION, enforced by the caller (src/tools.ts) before this is ever
277
+ * invoked: `item.atom_id`, `item.content` and `item.domain` are all strings.
278
+ * core's MemoryItemSchema sends all three on every item; a response that
279
+ * doesn't is caught by the handler's item guard and answered with
280
+ * `unreadableAnswerReply` before `structuredItem` is reached, so the casts
281
+ * below are a documented precondition, not a runtime assumption made here.
282
+ *
283
+ * `content` is carried EXACTLY, uncapped and unnormalised (decision OD-11,
284
+ * owner, 2026-09-23): unlike the text line, which goes through `capResult`
285
+ * for the 25K-token result-size cap, `structuredContent` is not capped
286
+ * anywhere else in this package either (memory_write's `reason` is the only
287
+ * normalised structured field, and that's control/bidi/zero-width hygiene on
288
+ * a diagnostic string, not a length cap on the memory itself); a client
289
+ * reading structured data reads `content` as the stored memory, and a
290
+ * silently shorter value there would be a different kind of lie than a
291
+ * truncated text block with a notice at the end.
292
+ */
293
+ export function structuredItem(item) {
294
+ const author = authorName(item.provenance);
295
+ const created = utcInstant(item.created_at);
296
+ return {
297
+ memory_id: item.atom_id,
298
+ content: item.content,
299
+ domain: item.domain,
300
+ // The rule the text already applies through formatDateTag: a value that
301
+ // does not parse as a date is no creation instant, whatever its type, and
302
+ // the field promises a UTC ISO-8601 instant. A value that states its
303
+ // offset is carried as sent; an offset-less one (UTC by contract) is
304
+ // re-emitted as the UTC instant the text renders, since a consumer
305
+ // would otherwise read it as local time (src/time.ts, utcInstant).
306
+ ...(created !== null ? { created_at: created } : {}),
307
+ ...(author ? { author } : {}),
308
+ };
309
+ }
173
310
  /**
174
311
  * One memory_list_recent line — a feed entry, not a search hit: no
175
312
  * relevance, date leads because the feed is ORDERED by it.
@@ -183,6 +320,14 @@ export function formatRecentItem(item, index) {
183
320
  const head = `${index + 1}. ${date ? `[${date}] ` : ""}${content}${concepts}${formatDomainTag(item?.domain)}${formatAuthorTag(item?.provenance)}`;
184
321
  return item?.atom_id ? `${head}\n id: ${item.atom_id}` : head;
185
322
  }
323
+ /**
324
+ * The shape of a continuation token this client is willing to pass on
325
+ * (CN-032): the server-supplied cursor is opaque, so this is an allowlist
326
+ * of bytes, not a format. One constant for BOTH surfaces, the text (below)
327
+ * and `structuredContent.next_cursor` (src/tools.ts), so they cannot
328
+ * disagree about which token is passable.
329
+ */
330
+ export const CURSOR_RE = /^[A-Za-z0-9_=-]{1,512}$/;
186
331
  /**
187
332
  * Full feed page: items newest-first + how to continue / that it's over.
188
333
  *
@@ -194,6 +339,9 @@ export function formatRecentItem(item, index) {
194
339
  * 2026-08-08). Once-per-answer still holds — withDomainEscapeLegend is
195
340
  * at-most-once by construction (src/names.ts).
196
341
  */
342
+ // `nextCursor` is typed loosely on purpose: it is a server-supplied wire
343
+ // value, and a number where a string was promised must fail the gate below
344
+ // rather than be coerced into it by the regex test (review, 2026-09-23).
197
345
  export function formatRecentPage(items, nextCursor) {
198
346
  const lines = items.map((it, i) => formatRecentItem(it, i));
199
347
  // Defense-in-depth (CN-032 posture): the cursor is server-supplied and
@@ -218,7 +366,7 @@ export function formatRecentPage(items, nextCursor) {
218
366
  if (nextCursor == null) {
219
367
  tail = `\n\n(end of feed — nothing older)`;
220
368
  }
221
- else if (/^[A-Za-z0-9_=-]{1,512}$/.test(nextCursor)) {
369
+ else if (typeof nextCursor === "string" && CURSOR_RE.test(nextCursor)) {
222
370
  tail = `\n\nMore older entries exist — pass cursor: ${nextCursor}`;
223
371
  }
224
372
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
@@ -52,6 +52,29 @@ export interface WriteArgs {
52
52
  concepts?: string[];
53
53
  domain?: string;
54
54
  }
55
+ /**
56
+ * Supplier-vouched authorship for a write (STEP4-5, owner 2026-09-24),
57
+ * mirroring core's `ProvenanceWriteSchema` field for field
58
+ * (mnemoverse-core src/mnemo/api/schemas.py:123-150): `principal`, `agent`,
59
+ * `agent_name`, `client_env` and `is_external`, all optional.
60
+ *
61
+ * Core honours this ONLY for a SERVICE/supplier caller (`author` in the
62
+ * write body) and IGNORES it entirely for an OIDC end-user, who is stamped
63
+ * solely from their own verified token (`routes._get_provenance`,
64
+ * mnemoverse-core src/mnemo/api/routes.py:211-276), so a caller supplying
65
+ * this for a plain end-user key gets no error, just a silently dropped
66
+ * value. Core also RE-NORMALISES every field server-side (an allow-list on
67
+ * `client_env`, length caps, `is_external` coerced to a real boolean), so
68
+ * this package does not re-validate the shape beyond the `typeof` guard at
69
+ * the one call site that reads it: that is core's job, not this one's.
70
+ */
71
+ export interface WriteAuthor {
72
+ principal?: string;
73
+ agent?: string;
74
+ agent_name?: string;
75
+ client_env?: string;
76
+ is_external?: boolean;
77
+ }
55
78
  /**
56
79
  * The scope actually searched: what core receives, and therefore the only value
57
80
  * any message about the result may describe.
@@ -66,9 +89,17 @@ export declare function searchedScope(domain?: string): string | undefined;
66
89
  export declare function readRequestBody(a: ReadArgs): Record<string, unknown>;
67
90
  /** memory_list_recent. */
68
91
  export declare function recentRequestBody(a: RecentArgs): Record<string, unknown>;
69
- /** memory_write. `"general"` is the server-side default made explicit, and is
70
- * what 0.8.0 sent — NOT a normalisation of the caller's value. */
71
- export declare function writeRequestBody(a: WriteArgs): Record<string, unknown>;
92
+ /**
93
+ * memory_write. `"general"` is the server-side default made explicit, and is
94
+ * what 0.8.0 sent — NOT a normalisation of the caller's value.
95
+ *
96
+ * `author` (STEP4-5): sent verbatim, exactly as the caller's `writeAuthor()`
97
+ * dependency returned it, when present: this function does no field-level
98
+ * normalisation of it (core re-normalises server-side, see {@link WriteAuthor}).
99
+ * Omitted entirely, not sent as `undefined`, when there is none, so every
100
+ * existing write body this function ever produced stays byte-identical.
101
+ */
102
+ export declare function writeRequestBody(a: WriteArgs, author?: WriteAuthor): Record<string, unknown>;
72
103
  /**
73
104
  * Refuse a docs placeholder key from CONFIGURATION ALONE, before any tool
74
105
  * call sends it anywhere.
package/dist/requests.js CHANGED
@@ -71,13 +71,22 @@ export function recentRequestBody(a) {
71
71
  cursor: a.cursor || undefined,
72
72
  };
73
73
  }
74
- /** memory_write. `"general"` is the server-side default made explicit, and is
75
- * what 0.8.0 sent — NOT a normalisation of the caller's value. */
76
- export function writeRequestBody(a) {
74
+ /**
75
+ * memory_write. `"general"` is the server-side default made explicit, and is
76
+ * what 0.8.0 sent — NOT a normalisation of the caller's value.
77
+ *
78
+ * `author` (STEP4-5): sent verbatim, exactly as the caller's `writeAuthor()`
79
+ * dependency returned it, when present: this function does no field-level
80
+ * normalisation of it (core re-normalises server-side, see {@link WriteAuthor}).
81
+ * Omitted entirely, not sent as `undefined`, when there is none, so every
82
+ * existing write body this function ever produced stays byte-identical.
83
+ */
84
+ export function writeRequestBody(a, author) {
77
85
  return {
78
86
  content: a.content,
79
87
  concepts: a.concepts || [],
80
88
  domain: a.domain || "general",
89
+ ...(author === undefined ? {} : { author }),
81
90
  };
82
91
  }
83
92
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"requests.js","sourceRoot":"","sources":["../src/requests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,wDAAwD;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AA2BvC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAe;IAC3C,OAAO,MAAM,IAAI,SAAS,CAAC;AAC7B,CAAC;AAED;mEACmE;AACnE,MAAM,UAAU,eAAe,CAAC,CAAW;IACzC,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;QACnB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/B,oBAAoB,EAAE,IAAI;QAC1B,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC;AACJ,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,iBAAiB,CAAC,CAAa;IAC7C,OAAO;QACL,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,SAAS;QAC3B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,SAAS;QAC3B,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,SAAS;QAC7C,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;KAC9B,CAAC;AACJ,CAAC;AAED;kEACkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,CAAY;IAC3C,OAAO;QACL,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,EAAE;QAC1B,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;KAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAW;IAEX,MAAM,qBAAqB,GAAG,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjE,MAAM,qBAAqB,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7D,IAAI,CAAC,qBAAqB,IAAI,CAAC,qBAAqB;QAAE,OAAO,SAAS,CAAC;IACvE,OAAO;QACL,QAAQ,EACN,mEAAmE;YACnE,kEAAkE;YAClE,sEAAsE;YACtE,mEAAmE;YACnE,yCAAyC,QAAQ,kBAAkB;YACnE,qEAAqE;YACrE,mEAAmE;YACnE,gCAAgC;QAClC,UAAU,EACR,+DAA+D;YAC/D,yDAAyD;YACzD,gDAAgD,QAAQ,cAAc;YACtE,kEAAkE;YAClE,mDAAmD;KACtD,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"requests.js","sourceRoot":"","sources":["../src/requests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,wDAAwD;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAmDvC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAe;IAC3C,OAAO,MAAM,IAAI,SAAS,CAAC;AAC7B,CAAC;AAED;mEACmE;AACnE,MAAM,UAAU,eAAe,CAAC,CAAW;IACzC,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;QACnB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/B,oBAAoB,EAAE,IAAI;QAC1B,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC;AACJ,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,iBAAiB,CAAC,CAAa;IAC7C,OAAO;QACL,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,SAAS;QAC3B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,SAAS;QAC3B,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,SAAS;QAC7C,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;KAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAY,EAAE,MAAoB;IACjE,OAAO;QACL,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,EAAE;QAC1B,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;QAC7B,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAW;IAEX,MAAM,qBAAqB,GAAG,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjE,MAAM,qBAAqB,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7D,IAAI,CAAC,qBAAqB,IAAI,CAAC,qBAAqB;QAAE,OAAO,SAAS,CAAC;IACvE,OAAO;QACL,QAAQ,EACN,mEAAmE;YACnE,kEAAkE;YAClE,sEAAsE;YACtE,mEAAmE;YACnE,yCAAyC,QAAQ,kBAAkB;YACnE,qEAAqE;YACrE,mEAAmE;YACnE,gCAAgC;QAClC,UAAU,EACR,+DAA+D;YAC/D,yDAAyD;YACzD,gDAAgD,QAAQ,cAAc;YACtE,kEAAkE;YAClE,mDAAmD;KACtD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MCP resource: one saved memory by id, as `memory://item/{memory_id}`.
3
+ *
4
+ * Clients that support resources (Claude.ai connectors attach them through an
5
+ * @-mention) can pull one specific memory into context. The ids are the `id:`
6
+ * lines of memory_read and memory_list_recent results, so this is the "open
7
+ * this memory" companion to the recall tools. There is no listing: the
8
+ * template is advertised, and reading needs a known id.
9
+ *
10
+ * Moved here from the hosted connector (mnemoverse-mcp-remote,
11
+ * src/resources/index.ts) in step 3c of the ADR-025 plan, with the same URI
12
+ * template, name and output, so the connector can register this instead of
13
+ * its copy. It returns only `memory_id`, `content` and `domain`: the engine's
14
+ * point read also carries importance, valence, access counts and metadata,
15
+ * which a model opening a memory does not need.
16
+ *
17
+ * Two limits, both the engine's. The point read looks only in the caller's
18
+ * own store (GET /memory/atoms/{id} takes no domain), so a memory read from a
19
+ * shared room cannot be opened here; the description says so. And a Vault
20
+ * secret is safe to open: its value lives in a column no read returns.
21
+ */
22
+ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
+ import { type MemoryToolDeps } from "./tools.js";
24
+ /**
25
+ * Register the `memory://item/{memory_id}` resource on `server`. It reaches the
26
+ * API only through `deps.apiFetch`, like the tools.
27
+ */
28
+ export declare function registerMemoryResources(server: McpServer, deps: MemoryToolDeps): void;