@mnemoverse/mcp-memory-server 0.3.0 → 0.3.2
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/.github/workflows/release.yml +138 -0
- package/.github/workflows/verify-configs.yml +55 -0
- package/CONTRIBUTING.md +199 -0
- package/README.md +50 -21
- package/dist/index.js +82 -40
- package/dist/index.js.map +1 -1
- package/docs/configs/claude-code-cli.sh +1 -1
- package/docs/configs/claude-desktop.json +1 -1
- package/docs/configs/cursor-deep-link.txt +1 -1
- package/docs/configs/cursor.json +1 -1
- package/docs/configs/vscode-deep-link.txt +1 -1
- package/docs/configs/vscode.json +1 -1
- package/docs/configs/windsurf.json +1 -1
- package/docs/snippets/claude-code.md +9 -0
- package/docs/snippets/claude-desktop.md +20 -0
- package/docs/snippets/cursor.md +20 -0
- package/docs/snippets/vscode.md +21 -0
- package/docs/snippets/windsurf.md +20 -0
- package/package.json +2 -1
- package/scripts/generate-configs.mjs +196 -7
- package/scripts/install-hooks.mjs +76 -0
- package/server.json +10 -9
- package/smithery.yaml +1 -1
- package/src/configs/source.json +2 -2
- package/src/index.ts +114 -72
package/src/index.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
|
|
8
|
+
// Version is read at runtime from package.json so there is exactly one place
|
|
9
|
+
// to bump on each release. Works both from `dist/` during local dev and from
|
|
10
|
+
// `node_modules/@mnemoverse/mcp-memory-server/dist/` after an npm install.
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const pkg = require("../package.json") as { version: string };
|
|
13
|
+
|
|
7
14
|
const API_URL =
|
|
8
15
|
process.env.MNEMOVERSE_API_URL || "https://core.mnemoverse.com/api/v1";
|
|
9
16
|
const API_KEY = process.env.MNEMOVERSE_API_KEY || "";
|
|
@@ -21,10 +28,23 @@ if (!API_KEY) {
|
|
|
21
28
|
process.exit(1);
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Fetch from the Mnemoverse core API with authentication.
|
|
33
|
+
*
|
|
34
|
+
* Generic so call sites can declare the expected response shape:
|
|
35
|
+
*
|
|
36
|
+
* const r = await apiFetch<{ stored: boolean; atom_id: string }>("/memory/write", { ... });
|
|
37
|
+
*
|
|
38
|
+
* Handles 204 No Content and empty bodies defensively — FastAPI DELETE
|
|
39
|
+
* handlers may switch to 204 in the future even though today they return
|
|
40
|
+
* a JSON body.
|
|
41
|
+
*
|
|
42
|
+
* @throws Error with message `Mnemoverse API error {status}: {body}` on non-2xx.
|
|
43
|
+
*/
|
|
44
|
+
async function apiFetch<T = unknown>(
|
|
25
45
|
path: string,
|
|
26
46
|
options: RequestInit = {},
|
|
27
|
-
): Promise<
|
|
47
|
+
): Promise<T> {
|
|
28
48
|
const res = await fetch(`${API_URL}${path}`, {
|
|
29
49
|
...options,
|
|
30
50
|
headers: {
|
|
@@ -39,16 +59,31 @@ async function apiFetch(
|
|
|
39
59
|
throw new Error(`Mnemoverse API error ${res.status}: ${text}`);
|
|
40
60
|
}
|
|
41
61
|
|
|
42
|
-
return
|
|
62
|
+
// 204 No Content or empty body — return an empty object cast as T so
|
|
63
|
+
// call sites using optional chaining still work without crashing.
|
|
64
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
65
|
+
return {} as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (await res.json()) as T;
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
/**
|
|
46
72
|
* Truncate a result string to MAX_RESULT_CHARS, appending a notice if truncated.
|
|
47
73
|
* Required by Claude Connectors Directory submission policy.
|
|
74
|
+
*
|
|
75
|
+
* Defensive against splitting UTF-16 surrogate pairs: if the character right
|
|
76
|
+
* before the cut point is a high surrogate (U+D800–U+DBFF), drop it so the
|
|
77
|
+
* result stays well-formed. Otherwise an emoji or non-BMP character at the
|
|
78
|
+
* boundary can produce a lone surrogate and corrupt downstream JSON encoding.
|
|
48
79
|
*/
|
|
49
80
|
function capResult(text: string): string {
|
|
50
81
|
if (text.length <= MAX_RESULT_CHARS) return text;
|
|
51
|
-
|
|
82
|
+
let truncated = text.slice(0, MAX_RESULT_CHARS - 200);
|
|
83
|
+
const lastCode = truncated.charCodeAt(truncated.length - 1);
|
|
84
|
+
if (lastCode >= 0xd800 && lastCode <= 0xdbff) {
|
|
85
|
+
truncated = truncated.slice(0, -1);
|
|
86
|
+
}
|
|
52
87
|
return (
|
|
53
88
|
truncated +
|
|
54
89
|
`\n\n[…truncated to fit 25K token limit. Use a more specific query or smaller top_k to see all results.]`
|
|
@@ -59,7 +94,7 @@ function capResult(text: string): string {
|
|
|
59
94
|
|
|
60
95
|
const server = new McpServer({
|
|
61
96
|
name: "mnemoverse-memory",
|
|
62
|
-
version:
|
|
97
|
+
version: pkg.version,
|
|
63
98
|
});
|
|
64
99
|
|
|
65
100
|
// --- Tool: memory_write ---
|
|
@@ -97,7 +132,12 @@ server.registerTool(
|
|
|
97
132
|
},
|
|
98
133
|
},
|
|
99
134
|
async ({ content, concepts, domain }) => {
|
|
100
|
-
const
|
|
135
|
+
const r = await apiFetch<{
|
|
136
|
+
stored?: boolean;
|
|
137
|
+
atom_id?: string | null;
|
|
138
|
+
importance?: number;
|
|
139
|
+
reason?: string;
|
|
140
|
+
}>("/memory/write", {
|
|
101
141
|
method: "POST",
|
|
102
142
|
body: JSON.stringify({
|
|
103
143
|
content,
|
|
@@ -106,19 +146,14 @@ server.registerTool(
|
|
|
106
146
|
}),
|
|
107
147
|
});
|
|
108
148
|
|
|
109
|
-
const
|
|
110
|
-
stored: boolean;
|
|
111
|
-
atom_id: string | null;
|
|
112
|
-
importance: number;
|
|
113
|
-
reason: string;
|
|
114
|
-
};
|
|
149
|
+
const importance = (r?.importance ?? 0).toFixed(2);
|
|
115
150
|
|
|
116
|
-
if (r
|
|
151
|
+
if (r?.stored) {
|
|
117
152
|
return {
|
|
118
153
|
content: [
|
|
119
154
|
{
|
|
120
155
|
type: "text" as const,
|
|
121
|
-
text: `Stored (importance: ${
|
|
156
|
+
text: `Stored (importance: ${importance}). ID: ${r.atom_id ?? "unknown"}`,
|
|
122
157
|
},
|
|
123
158
|
],
|
|
124
159
|
};
|
|
@@ -127,7 +162,7 @@ server.registerTool(
|
|
|
127
162
|
content: [
|
|
128
163
|
{
|
|
129
164
|
type: "text" as const,
|
|
130
|
-
text: `Filtered — ${r
|
|
165
|
+
text: `Filtered — ${r?.reason ?? "unknown reason"} (importance: ${importance})`,
|
|
131
166
|
},
|
|
132
167
|
],
|
|
133
168
|
};
|
|
@@ -168,7 +203,15 @@ server.registerTool(
|
|
|
168
203
|
},
|
|
169
204
|
},
|
|
170
205
|
async ({ query, top_k, domain }) => {
|
|
171
|
-
const
|
|
206
|
+
const r = await apiFetch<{
|
|
207
|
+
items?: Array<{
|
|
208
|
+
content?: string;
|
|
209
|
+
relevance?: number;
|
|
210
|
+
concepts?: string[];
|
|
211
|
+
domain?: string;
|
|
212
|
+
}>;
|
|
213
|
+
search_time_ms?: number;
|
|
214
|
+
}>("/memory/read", {
|
|
172
215
|
method: "POST",
|
|
173
216
|
body: JSON.stringify({
|
|
174
217
|
query,
|
|
@@ -178,17 +221,9 @@ server.registerTool(
|
|
|
178
221
|
}),
|
|
179
222
|
});
|
|
180
223
|
|
|
181
|
-
const
|
|
182
|
-
items: Array<{
|
|
183
|
-
content: string;
|
|
184
|
-
relevance: number;
|
|
185
|
-
concepts: string[];
|
|
186
|
-
domain: string;
|
|
187
|
-
}>;
|
|
188
|
-
search_time_ms: number;
|
|
189
|
-
};
|
|
224
|
+
const items = Array.isArray(r?.items) ? r.items : [];
|
|
190
225
|
|
|
191
|
-
if (
|
|
226
|
+
if (items.length === 0) {
|
|
192
227
|
return {
|
|
193
228
|
content: [
|
|
194
229
|
{ type: "text" as const, text: "No memories found for this query." },
|
|
@@ -196,15 +231,17 @@ server.registerTool(
|
|
|
196
231
|
};
|
|
197
232
|
}
|
|
198
233
|
|
|
199
|
-
const lines =
|
|
200
|
-
(item
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
234
|
+
const lines = items.map((item, i) => {
|
|
235
|
+
const relevance = ((item?.relevance ?? 0) * 100).toFixed(0);
|
|
236
|
+
const content = item?.content ?? "(empty)";
|
|
237
|
+
const concepts = Array.isArray(item?.concepts) && item.concepts.length > 0
|
|
238
|
+
? ` (${item.concepts.join(", ")})`
|
|
239
|
+
: "";
|
|
240
|
+
return `${i + 1}. [${relevance}%] ${content}${concepts}`;
|
|
241
|
+
});
|
|
206
242
|
|
|
207
|
-
const
|
|
243
|
+
const searchMs = (r?.search_time_ms ?? 0).toFixed(0);
|
|
244
|
+
const text = lines.join("\n\n") + `\n\n(${searchMs}ms)`;
|
|
208
245
|
|
|
209
246
|
return {
|
|
210
247
|
content: [
|
|
@@ -238,24 +275,28 @@ server.registerTool(
|
|
|
238
275
|
annotations: {
|
|
239
276
|
title: "Rate Memory Helpfulness",
|
|
240
277
|
readOnlyHint: false,
|
|
241
|
-
|
|
278
|
+
// Feedback permanently mutates the memory's valence and importance
|
|
279
|
+
// scores on the backend — per MCP spec, that is a destructive update
|
|
280
|
+
// to the stored state (cf. ToolAnnotations.destructiveHint), even
|
|
281
|
+
// though the caller intends it as quality signal rather than delete.
|
|
282
|
+
destructiveHint: true,
|
|
242
283
|
idempotentHint: false,
|
|
243
284
|
openWorldHint: true,
|
|
244
285
|
},
|
|
245
286
|
},
|
|
246
287
|
async ({ atom_ids, outcome }) => {
|
|
247
|
-
const
|
|
288
|
+
const r = await apiFetch<{ updated_count?: number }>("/memory/feedback", {
|
|
248
289
|
method: "POST",
|
|
249
290
|
body: JSON.stringify({ atom_ids, outcome }),
|
|
250
291
|
});
|
|
251
292
|
|
|
252
|
-
const
|
|
293
|
+
const count = r?.updated_count ?? 0;
|
|
253
294
|
|
|
254
295
|
return {
|
|
255
296
|
content: [
|
|
256
297
|
{
|
|
257
298
|
type: "text" as const,
|
|
258
|
-
text: `Feedback recorded for ${
|
|
299
|
+
text: `Feedback recorded for ${count} memor${count === 1 ? "y" : "ies"}.`,
|
|
259
300
|
},
|
|
260
301
|
],
|
|
261
302
|
};
|
|
@@ -279,23 +320,25 @@ server.registerTool(
|
|
|
279
320
|
},
|
|
280
321
|
},
|
|
281
322
|
async () => {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
323
|
+
const r = await apiFetch<{
|
|
324
|
+
total_atoms?: number;
|
|
325
|
+
episodes?: number;
|
|
326
|
+
prototypes?: number;
|
|
327
|
+
hebbian_edges?: number;
|
|
328
|
+
domains?: string[];
|
|
329
|
+
avg_valence?: number;
|
|
330
|
+
avg_importance?: number;
|
|
331
|
+
}>("/memory/stats");
|
|
332
|
+
|
|
333
|
+
const domains = Array.isArray(r?.domains) && r.domains.length > 0
|
|
334
|
+
? r.domains.join(", ")
|
|
335
|
+
: "general";
|
|
293
336
|
|
|
294
337
|
const text = [
|
|
295
|
-
`Memories: ${r
|
|
296
|
-
`Associations: ${r
|
|
297
|
-
`Domains: ${
|
|
298
|
-
`Avg quality: valence ${r
|
|
338
|
+
`Memories: ${r?.total_atoms ?? 0} (${r?.episodes ?? 0} episodes, ${r?.prototypes ?? 0} prototypes)`,
|
|
339
|
+
`Associations: ${r?.hebbian_edges ?? 0} Hebbian edges`,
|
|
340
|
+
`Domains: ${domains}`,
|
|
341
|
+
`Avg quality: valence ${(r?.avg_valence ?? 0).toFixed(2)}, importance ${(r?.avg_importance ?? 0).toFixed(2)}`,
|
|
299
342
|
].join("\n");
|
|
300
343
|
|
|
301
344
|
return { content: [{ type: "text" as const, text }] };
|
|
@@ -326,15 +369,15 @@ server.registerTool(
|
|
|
326
369
|
},
|
|
327
370
|
},
|
|
328
371
|
async ({ atom_id }) => {
|
|
329
|
-
const result = await apiFetch(`/memory/atoms/${encodeURIComponent(atom_id)}`, {
|
|
330
|
-
method: "DELETE",
|
|
331
|
-
});
|
|
332
|
-
|
|
333
372
|
// Core API returns { deleted: <count>, atom_id }. count == 0 means
|
|
334
|
-
// the atom didn't exist (or was already removed). count >= 1 means
|
|
335
|
-
|
|
373
|
+
// the atom didn't exist (or was already removed). count >= 1 means
|
|
374
|
+
// it was deleted.
|
|
375
|
+
const r = await apiFetch<{ deleted?: number; atom_id?: string }>(
|
|
376
|
+
`/memory/atoms/${encodeURIComponent(atom_id)}`,
|
|
377
|
+
{ method: "DELETE" },
|
|
378
|
+
);
|
|
336
379
|
|
|
337
|
-
if (!r
|
|
380
|
+
if (!r?.deleted) {
|
|
338
381
|
return {
|
|
339
382
|
content: [
|
|
340
383
|
{
|
|
@@ -385,24 +428,23 @@ server.registerTool(
|
|
|
385
428
|
openWorldHint: true,
|
|
386
429
|
},
|
|
387
430
|
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
method: "DELETE",
|
|
397
|
-
});
|
|
431
|
+
// The `confirm: z.literal(true)` in the input schema is the safety
|
|
432
|
+
// interlock — Zod rejects any call without confirm === true before it
|
|
433
|
+
// reaches this handler, so no runtime re-check is needed here.
|
|
434
|
+
async ({ domain }) => {
|
|
435
|
+
const r = await apiFetch<{ deleted?: number; domain?: string }>(
|
|
436
|
+
`/memory/domain/${encodeURIComponent(domain)}`,
|
|
437
|
+
{ method: "DELETE" },
|
|
438
|
+
);
|
|
398
439
|
|
|
399
|
-
const
|
|
440
|
+
const count = r?.deleted ?? 0;
|
|
441
|
+
const domainName = r?.domain ?? domain;
|
|
400
442
|
|
|
401
443
|
return {
|
|
402
444
|
content: [
|
|
403
445
|
{
|
|
404
446
|
type: "text" as const,
|
|
405
|
-
text: `Deleted ${
|
|
447
|
+
text: `Deleted ${count} ${count === 1 ? "memory" : "memories"} from domain "${domainName}".`,
|
|
406
448
|
},
|
|
407
449
|
],
|
|
408
450
|
};
|