@mnemoverse/mcp-memory-server 0.9.0 → 0.10.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.
- package/README.md +1 -1
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +668 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +712 -124
- package/dist/index.js.map +1 -1
- package/dist/names.d.ts +26 -1
- package/dist/names.js +52 -5
- package/dist/names.js.map +1 -1
- package/dist/render.d.ts +34 -1
- package/dist/render.js +63 -18
- package/dist/render.js.map +1 -1
- package/dist/scope.js +5 -22
- package/dist/scope.js.map +1 -1
- package/dist/time.d.ts +39 -0
- package/dist/time.js +48 -0
- package/dist/time.js.map +1 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://arxiv.org/abs/2603.08965)
|
|
8
8
|
[](https://glama.ai/mcp/servers/mnemoverse/mcp-memory-server)
|
|
9
9
|
|
|
10
|
-
Hosted memory for AI agents that learns which facts matter. Feedback
|
|
10
|
+
Hosted memory for AI agents that learns which facts matter. Feedback re-ranks recall — a Rescorla-Wagner update on the prediction error, not a similarity score — so what helped rises and what misled sinks, with a bounded recency tie-breaker for fresh memories. The engine also ships consolidation (HDBSCAN clustering, with Von Restorff protection so distinctive memories survive compression). One API key works across Claude, Cursor, VS Code, ChatGPT, and any MCP client.
|
|
11
11
|
|
|
12
12
|
Memory that persists across sessions, projects, and tools — and improves with use. Hosted, so there's no infrastructure to run, and not locked to a single cloud.
|
|
13
13
|
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed API call TELLS THE AGENT TO DO.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS. Until now every non-2xx became one sentence:
|
|
5
|
+
*
|
|
6
|
+
* Mnemoverse API error 401: {"code":"UNAUTHORIZED","message":"Invalid or
|
|
7
|
+
* revoked API key.","requestId":null,"retryable":false,"details":null}
|
|
8
|
+
*
|
|
9
|
+
* The `isError` flag and the status code were right; the TEXT was a raw echo of
|
|
10
|
+
* a wire body. Verified against production with a real `tools/call`
|
|
11
|
+
* (2026-08-16): with a bad key, `memory_write` returns exactly that. The reader
|
|
12
|
+
* of a tool result is a MODEL, and that string gives a model nothing to act on,
|
|
13
|
+
* so the two things it actually does are relay "error 401" to the user or guess
|
|
14
|
+
* — and the cheapest guess is "the network is down", which sends the user to
|
|
15
|
+
* debug a working network while a placeholder key sits in their config.
|
|
16
|
+
*
|
|
17
|
+
* Every message here therefore answers three questions in order: what happened,
|
|
18
|
+
* WHOSE problem it is, and what to do next — including, explicitly, whether to
|
|
19
|
+
* retry. "Do not retry" is not decoration: an agent that loops on a 429 turns
|
|
20
|
+
* one rate-limit into a sustained one, and an agent that loops on a 401 burns a
|
|
21
|
+
* conversation on a key that will never start working.
|
|
22
|
+
*
|
|
23
|
+
* THE RAW BODY IS NEVER DROPPED. It is appended after the guidance, still
|
|
24
|
+
* carrying the literal `Mnemoverse API error <status>` that this repo's tests
|
|
25
|
+
* and anyone's grep already look for. Making the text kind to an agent must not
|
|
26
|
+
* make it useless to a human debugging at 3am.
|
|
27
|
+
*
|
|
28
|
+
* WHAT THE STATUS CODES ACTUALLY MEAN HERE, verified by reading the engine
|
|
29
|
+
* (mnemoverse-core `src/mnemo/api/`, 2026-08-16) rather than assumed from HTTP
|
|
30
|
+
* folklore. The distinctions below are the entire point of this file:
|
|
31
|
+
*
|
|
32
|
+
* 401 auth.py — "Invalid or revoked API key." / "Missing API key." The key.
|
|
33
|
+
* 403 routes.py, rooms_routes.py, auth.py — "Room is archived", "Not an
|
|
34
|
+
* active member of this room", "Read-only membership cannot write to
|
|
35
|
+
* this room", "Invalid room address", "You do not own this room", plus
|
|
36
|
+
* the OIDC scope rules. NOT a bad key: the caller was identified and
|
|
37
|
+
* then refused. Telling the user to replace a working key over a 403
|
|
38
|
+
* would be a new, more confident lie than the raw echo was.
|
|
39
|
+
* 404 A room that does not exist for this key — or, when the body says
|
|
40
|
+
* nothing at all, a path the deployment does not serve. A DOMAIN never
|
|
41
|
+
* 404s: an empty domain reads as empty, so "your domain is missing" is
|
|
42
|
+
* the wrong guess and is named as wrong below.
|
|
43
|
+
* 429 THREE different causes with opposite advice. rate_limit.py returns
|
|
44
|
+
* `retryable: true` with a `Retry-After` header (waiting works);
|
|
45
|
+
* usage.py (daily quota) and subscription_guard.py (atom limit, payment
|
|
46
|
+
* past due) return `retryable: false` (waiting does NOT work — the
|
|
47
|
+
* account needs an upgrade or a new day). A blanket "wait and retry"
|
|
48
|
+
* would be wrong for two of the three.
|
|
49
|
+
* 5xx auth.py returns 503 `retryable: true` when its DB pool is gone. Ours,
|
|
50
|
+
* not theirs.
|
|
51
|
+
*
|
|
52
|
+
* A 2xx CAN FAIL TOO, and gets the same treatment for the same reason. A reply
|
|
53
|
+
* whose body this client cannot parse is not a status at all, so none of the
|
|
54
|
+
* sentences above fit it — and it used to borrow the TRANSPORT sentence, which
|
|
55
|
+
* asserts that nothing answered. `explainUnreadableBody` at the bottom of this
|
|
56
|
+
* file owns that case now; the distinction it protects is "a reply arrived and
|
|
57
|
+
* was unreadable" versus "no reply arrived", which have opposite fixes.
|
|
58
|
+
*
|
|
59
|
+
* RELATION TO THE STARTUP PROBE (src/index.ts, `probeApiKeyInBackground`, added
|
|
60
|
+
* in 0.8.4). That probe treats 401 and 403 alike, and is right to: it calls
|
|
61
|
+
* `GET /memory/stats`, which addresses no room, so a 403 there can only come
|
|
62
|
+
* from the auth layer. On a TOOL CALL the same status usually comes from a room
|
|
63
|
+
* the caller named, which is why the two surfaces diverge on 403 and must not be
|
|
64
|
+
* "unified" by a later reader. They agree where it matters: same `Mnemoverse:`
|
|
65
|
+
* opener, same `MNEMOVERSE_API_KEY` variable name, same console origin.
|
|
66
|
+
*/
|
|
67
|
+
/** Everything known about one failed call, at the moment it failed. */
|
|
68
|
+
export interface ApiFailure {
|
|
69
|
+
/** HTTP status. */
|
|
70
|
+
status: number;
|
|
71
|
+
/** Response body, verbatim and unparsed. */
|
|
72
|
+
body: string;
|
|
73
|
+
/** Uppercase HTTP verb. */
|
|
74
|
+
method: string;
|
|
75
|
+
/** Path below the API base, e.g. "/memory/write". Deliberately NOT the full
|
|
76
|
+
* URL: the base can carry credentials, and the path alone is what identifies
|
|
77
|
+
* the operation. */
|
|
78
|
+
path: string;
|
|
79
|
+
/** `Retry-After` header when the response carried one. */
|
|
80
|
+
retryAfter?: string | null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The engine's error envelope, as much of it as survived parsing.
|
|
84
|
+
*
|
|
85
|
+
* Every field is optional because a non-2xx does not have to come from the
|
|
86
|
+
* engine at all: a proxy, a tunnel, or a wrong `MNEMOVERSE_API_URL` answers with
|
|
87
|
+
* HTML, plain text or nothing. A missing field means "the body did not say",
|
|
88
|
+
* never a default — inventing `retryable: false` for an unparseable body would
|
|
89
|
+
* put a retry decision on evidence that does not exist.
|
|
90
|
+
*/
|
|
91
|
+
export interface ErrorEnvelope {
|
|
92
|
+
code?: string;
|
|
93
|
+
message?: string;
|
|
94
|
+
retryable?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read the engine's envelope out of a response body.
|
|
98
|
+
*
|
|
99
|
+
* Two shapes are accepted because core produces both: its own middleware writes
|
|
100
|
+
* `{code, message, retryable, details}` at the top level, while anything raised
|
|
101
|
+
* as a FastAPI `HTTPException` arrives wrapped as `{"detail": …}` — where the
|
|
102
|
+
* detail is sometimes a string and sometimes the envelope again. The feed's
|
|
103
|
+
* 404-vs-404 test in this repo pins the nested form, so both are real.
|
|
104
|
+
*/
|
|
105
|
+
export declare function parseErrorEnvelope(body: string): ErrorEnvelope;
|
|
106
|
+
/** `Retry-After` as whole seconds, when it is a plain number. The HTTP-date
|
|
107
|
+
* form is legal too, and is deliberately NOT parsed: an unparsed header
|
|
108
|
+
* degrades into "wait a moment", which is honest, whereas a misparsed date
|
|
109
|
+
* would print a confident and wrong number of seconds. */
|
|
110
|
+
export declare function retryAfterSeconds(header: string | null | undefined): number | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* The agent-facing explanation for one failed call, with the raw body kept
|
|
113
|
+
* after it.
|
|
114
|
+
*
|
|
115
|
+
* Total by construction: every status reaches a sentence, and the fallback says
|
|
116
|
+
* that it has no specific guidance instead of inventing some.
|
|
117
|
+
*/
|
|
118
|
+
export declare function explainApiFailure(f: ApiFailure): string;
|
|
119
|
+
/**
|
|
120
|
+
* The request never got an answer at all: DNS, connectivity, a host that does
|
|
121
|
+
* not listen, or this client's own probe deadline firing.
|
|
122
|
+
*
|
|
123
|
+
* Included even though the brief was about HTTP statuses, because it is the
|
|
124
|
+
* OTHER half of the same defect and the two are easy to confuse. The 401
|
|
125
|
+
* message above tells an agent not to blame the network; this is the case where
|
|
126
|
+
* the network genuinely IS the answer, and `TypeError: fetch failed` — which is
|
|
127
|
+
* all a model saw before — is no more actionable than the raw 401 body was.
|
|
128
|
+
*
|
|
129
|
+
* A timeout is named separately because the advice differs in one word: an
|
|
130
|
+
* unreachable host is unlikely to become reachable in three seconds, whereas a
|
|
131
|
+
* request that ran out of time may well succeed on a second try.
|
|
132
|
+
*
|
|
133
|
+
* NARROWED to `fetch()` itself rejecting. Reading the BODY of a response that
|
|
134
|
+
* did arrive used to land here too, so a 200 carrying a sign-in page printed
|
|
135
|
+
* "no HTTP response came back" — see {@link explainUnreadableBody}, which owns
|
|
136
|
+
* that case now. Every sentence below asserts that nothing answered, so it may
|
|
137
|
+
* only be reached when nothing did.
|
|
138
|
+
*/
|
|
139
|
+
export declare function explainNetworkFailure(method: string, path: string, cause: unknown): string;
|
|
140
|
+
/** A 2xx-or-not response whose BODY this client could not turn into the JSON
|
|
141
|
+
* this API speaks. The status is known — a reply arrived — which is the whole
|
|
142
|
+
* difference between this and {@link ApiFailure} or a transport failure. */
|
|
143
|
+
export interface UnreadableBody {
|
|
144
|
+
/** HTTP status, as it actually arrived. */
|
|
145
|
+
status: number;
|
|
146
|
+
/** Uppercase HTTP verb. */
|
|
147
|
+
method: string;
|
|
148
|
+
/** Path below the API base. Never the full URL — the base can carry
|
|
149
|
+
* credentials, and the path alone identifies the operation. */
|
|
150
|
+
path: string;
|
|
151
|
+
/** The bytes that failed to parse, when they were read at all. Absent when
|
|
152
|
+
* the body READ itself failed, which is a different sentence below. */
|
|
153
|
+
bodyPreview?: string;
|
|
154
|
+
/** The SyntaxError, TypeError or abort that stopped the read. */
|
|
155
|
+
cause: unknown;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A reply ARRIVED and could not be read — which is not a dead network.
|
|
159
|
+
*
|
|
160
|
+
* WHY THIS IS SEPARATE FROM {@link explainNetworkFailure}. Both failures used
|
|
161
|
+
* to throw the same NetworkError, so `JSON.parse` choking on a 200 printed
|
|
162
|
+
* "the memory service could not be reached at all — POST /memory/read failed
|
|
163
|
+
* before any HTTP response came back… That is a connectivity or DNS problem".
|
|
164
|
+
* Against a captive portal, a MITM proxy, an SPA that serves its shell with a
|
|
165
|
+
* 200, or a body that stops mid-stream, every clause of that is false: a reply
|
|
166
|
+
* came, with a status, and the Raw detail underneath it quoted a SyntaxError
|
|
167
|
+
* out of the body it had just called nonexistent.
|
|
168
|
+
*
|
|
169
|
+
* The cost is the same one this module exists to remove — a confident wrong
|
|
170
|
+
* cause. "Connectivity or DNS" sends the user to debug working wifi while a
|
|
171
|
+
* portal or a mis-set MNEMOVERSE_API_URL answers every request with a page.
|
|
172
|
+
*
|
|
173
|
+
* WHAT THIS MESSAGE MAY CLAIM. Only what the status proves: something answered,
|
|
174
|
+
* and its answer is not this API's JSON. It does NOT name who answered — this
|
|
175
|
+
* client cannot tell the engine from a gateway in front of it — and it does NOT
|
|
176
|
+
* say whether the operation ran, because a body it cannot read is no evidence
|
|
177
|
+
* either way. That last clause matters most for a write.
|
|
178
|
+
*/
|
|
179
|
+
export declare function explainUnreadableBody(f: UnreadableBody): string;
|
|
180
|
+
/**
|
|
181
|
+
* {@link explainUnreadableBody} as an error, so a caller can tell "a reply
|
|
182
|
+
* arrived and was unreadable" from "no reply arrived" by TYPE rather than by
|
|
183
|
+
* matching a sentence — the same reason {@link ApiError} carries fields.
|
|
184
|
+
*/
|
|
185
|
+
export declare class UnreadableBodyError extends Error {
|
|
186
|
+
readonly status: number;
|
|
187
|
+
readonly method: string;
|
|
188
|
+
readonly path: string;
|
|
189
|
+
constructor(f: UnreadableBody);
|
|
190
|
+
}
|
|
191
|
+
/** {@link explainNetworkFailure} as an error, so `instanceof` can tell a
|
|
192
|
+
* transport failure from an HTTP one without reading either message. */
|
|
193
|
+
export declare class NetworkError extends Error {
|
|
194
|
+
readonly method: string;
|
|
195
|
+
readonly path: string;
|
|
196
|
+
constructor(method: string, path: string, cause: unknown);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A failed API call, as an ERROR OBJECT rather than a parsed string.
|
|
200
|
+
*
|
|
201
|
+
* The status and the body are fields because a caller that needs to branch on
|
|
202
|
+
* them must not do it by matching the message. `memory_list_recent` used to
|
|
203
|
+
* decide between "the endpoint is not deployed" and "this room does not exist"
|
|
204
|
+
* with `message.startsWith("Mnemoverse API error 404:")`, which coupled a
|
|
205
|
+
* behavioural branch to the exact prefix of a user-facing sentence — so
|
|
206
|
+
* improving the sentence, which is this change, would have silently flipped
|
|
207
|
+
* that branch. Structured fields make the wording free to change.
|
|
208
|
+
*/
|
|
209
|
+
export declare class ApiError extends Error {
|
|
210
|
+
readonly status: number;
|
|
211
|
+
readonly body: string;
|
|
212
|
+
readonly method: string;
|
|
213
|
+
readonly path: string;
|
|
214
|
+
/** The engine's envelope, already parsed — so a caller never re-parses. */
|
|
215
|
+
readonly envelope: ErrorEnvelope;
|
|
216
|
+
constructor(f: ApiFailure);
|
|
217
|
+
/**
|
|
218
|
+
* A 404 the ENGINE did not answer: silence, or the router's literal
|
|
219
|
+
* defaults. This is what a path the deployment does not serve looks like
|
|
220
|
+
* — the first version of this predicate tested silence alone, mistook the
|
|
221
|
+
* real framework body `{"detail":"Not Found"}` for an engine answer, and
|
|
222
|
+
* un-fired the feed's degrade branch for the exact rollout case it exists
|
|
223
|
+
* for (panel, #93).
|
|
224
|
+
*
|
|
225
|
+
* NAMED FOR WHAT IT TESTS, not for what it implies: a gateway, a proxy or a
|
|
226
|
+
* wrong MNEMOVERSE_API_URL can produce the same shapes and is
|
|
227
|
+
* indistinguishable from here. Known, accepted edge: a foreign API whose
|
|
228
|
+
* 404 nests its error under a key this parser does not read
|
|
229
|
+
* (`{"error":{…}}`) also reads as bare, so the feed degrades to its
|
|
230
|
+
* not-supported notice instead of surfacing the foreign body — bounded
|
|
231
|
+
* harm, since against a wrong base URL the very next call fails with the
|
|
232
|
+
* instructive no-envelope wording.
|
|
233
|
+
*/
|
|
234
|
+
get isBare404(): boolean;
|
|
235
|
+
}
|