@melandlabs/memory-store 0.1.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/LICENSE +17 -0
- package/README.md +377 -0
- package/dist/config-MlK9f9CM.d.ts +147 -0
- package/dist/contracts.d.ts +62 -0
- package/dist/contracts.js +27 -0
- package/dist/contracts.js.map +1 -0
- package/dist/http.d.ts +38 -0
- package/dist/http.js +545 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +442 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +29 -0
- package/dist/mcp.js +589 -0
- package/dist/mcp.js.map +1 -0
- package/dist/policies/memory-graph-correction-policy.d.ts +14 -0
- package/dist/policies/memory-graph-correction-policy.js +41 -0
- package/dist/policies/memory-graph-correction-policy.js.map +1 -0
- package/dist/policies/memory-graph-write-policy.d.ts +35 -0
- package/dist/policies/memory-graph-write-policy.js +78 -0
- package/dist/policies/memory-graph-write-policy.js.map +1 -0
- package/dist/search/unified-search.d.ts +81 -0
- package/dist/search/unified-search.js +399 -0
- package/dist/search/unified-search.js.map +1 -0
- package/dist/server/cli-http.d.ts +1 -0
- package/dist/server/cli-http.js +584 -0
- package/dist/server/cli-http.js.map +1 -0
- package/dist/server/cli-mcp.d.ts +1 -0
- package/dist/server/cli-mcp.js +603 -0
- package/dist/server/cli-mcp.js.map +1 -0
- package/dist/storage/chroma-memory-index.d.ts +76 -0
- package/dist/storage/chroma-memory-index.js +249 -0
- package/dist/storage/chroma-memory-index.js.map +1 -0
- package/dist/storage/postgres-raw-message-factory.d.ts +48 -0
- package/dist/storage/postgres-raw-message-factory.js +19 -0
- package/dist/storage/postgres-raw-message-factory.js.map +1 -0
- package/dist/storage/raw-message-store.d.ts +47 -0
- package/dist/storage/raw-message-store.js +140 -0
- package/dist/storage/raw-message-store.js.map +1 -0
- package/dist/storage/sqlite-raw-message-store.d.ts +19 -0
- package/dist/storage/sqlite-raw-message-store.js +68 -0
- package/dist/storage/sqlite-raw-message-store.js.map +1 -0
- package/dist/storage/sqlite-vector-index.d.ts +42 -0
- package/dist/storage/sqlite-vector-index.js +92 -0
- package/dist/storage/sqlite-vector-index.js.map +1 -0
- package/package.json +87 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { buildMemoryRecordEmbeddingDocument } from '@melandlabs/ai/memory';
|
|
5
|
+
import { ChromaVectorStore } from '@melandlabs/rag';
|
|
6
|
+
import { mkdirSync } from 'fs';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
import { SQLiteRawMessageManager } from '@melandlabs/sqlite';
|
|
9
|
+
|
|
10
|
+
// src/mcp.ts
|
|
11
|
+
|
|
12
|
+
// src/contracts.ts
|
|
13
|
+
function normalizeTimestampToMs(value) {
|
|
14
|
+
if (!Number.isFinite(value)) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
if (value < 1e11) {
|
|
18
|
+
return Math.floor(value * 1e3);
|
|
19
|
+
}
|
|
20
|
+
return Math.floor(value);
|
|
21
|
+
}
|
|
22
|
+
function rawMessageToMemoryRecord(message) {
|
|
23
|
+
return {
|
|
24
|
+
id: message.messageId,
|
|
25
|
+
userId: message.userId,
|
|
26
|
+
timestamp: normalizeTimestampToMs(message.timestamp),
|
|
27
|
+
text: message.archivedAt ? void 0 : message.content,
|
|
28
|
+
mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
|
|
29
|
+
embedding: message.embedding,
|
|
30
|
+
embeddingModel: message.embeddingModel,
|
|
31
|
+
embeddingContentHash: message.embeddingContentHash
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/storage/chroma-memory-index.ts
|
|
36
|
+
var DEFAULT_RAW_MESSAGES_COLLECTION = "opencontext_raw_messages";
|
|
37
|
+
var RAW_VECTOR_BACKEND_ENV_KEYS = [
|
|
38
|
+
"RAW_MESSAGE_VECTOR_STORE_BACKEND",
|
|
39
|
+
"MEMORY_VECTOR_STORE_BACKEND",
|
|
40
|
+
"VECTOR_STORE_BACKEND"
|
|
41
|
+
];
|
|
42
|
+
function getBackend(keys) {
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
const value = process.env[key]?.trim().toLowerCase();
|
|
45
|
+
if (value) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
function isRawMessageChromaEnabled() {
|
|
52
|
+
return getBackend(RAW_VECTOR_BACKEND_ENV_KEYS) === "chroma";
|
|
53
|
+
}
|
|
54
|
+
function getRawMessagesCollectionName() {
|
|
55
|
+
return process.env.CHROMA_RAW_MESSAGES_COLLECTION || process.env.CHROMA_MEMORY_COLLECTION || DEFAULT_RAW_MESSAGES_COLLECTION;
|
|
56
|
+
}
|
|
57
|
+
function getChromaStore(collectionName) {
|
|
58
|
+
return new ChromaVectorStore({
|
|
59
|
+
url: process.env.CHROMA_URL,
|
|
60
|
+
collectionName
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function normalizeTimestampToMs2(value) {
|
|
64
|
+
if (!Number.isFinite(value)) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
if (value < 1e11) {
|
|
68
|
+
return Math.floor(value * 1e3);
|
|
69
|
+
}
|
|
70
|
+
return Math.floor(value);
|
|
71
|
+
}
|
|
72
|
+
function toBooleanValue(value) {
|
|
73
|
+
return value === true || value === "true" || value === 1 || value === "1";
|
|
74
|
+
}
|
|
75
|
+
function rawMessageToChunk(message) {
|
|
76
|
+
if (!message.embedding || message.embedding.length === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const record = rawMessageToMemoryRecord(message);
|
|
80
|
+
const document = buildMemoryRecordEmbeddingDocument(record);
|
|
81
|
+
if (!document.content) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
id: message.messageId,
|
|
86
|
+
documentId: message.messageId,
|
|
87
|
+
content: document.content,
|
|
88
|
+
embedding: message.embedding,
|
|
89
|
+
metadata: {
|
|
90
|
+
sourceType: "raw_message",
|
|
91
|
+
userId: message.userId,
|
|
92
|
+
platform: message.platform,
|
|
93
|
+
botId: message.botId,
|
|
94
|
+
channel: message.channel,
|
|
95
|
+
person: message.person,
|
|
96
|
+
timestamp: normalizeTimestampToMs2(message.timestamp),
|
|
97
|
+
memoryStage: message.memoryStage,
|
|
98
|
+
embeddingModel: message.embeddingModel,
|
|
99
|
+
embeddingDimensions: message.embeddingDimensions,
|
|
100
|
+
contentHash: message.embeddingContentHash,
|
|
101
|
+
archived: Boolean(message.archivedAt),
|
|
102
|
+
role: typeof message.metadata?.role === "string" ? message.metadata.role : void 0
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function upsertRawMessagesToChroma(messages) {
|
|
107
|
+
if (!isRawMessageChromaEnabled() || messages.length === 0) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
const chunks = messages.map(rawMessageToChunk).filter((chunk) => chunk !== null);
|
|
111
|
+
if (chunks.length === 0) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const store = getChromaStore(getRawMessagesCollectionName());
|
|
115
|
+
await store.addChunks(chunks);
|
|
116
|
+
return chunks.length;
|
|
117
|
+
}
|
|
118
|
+
async function searchRawMessagesWithChroma(input) {
|
|
119
|
+
if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const store = getChromaStore(getRawMessagesCollectionName());
|
|
123
|
+
const results = await store.similaritySearch(
|
|
124
|
+
input.queryEmbedding,
|
|
125
|
+
Math.max(input.limit * 5, input.limit),
|
|
126
|
+
input.userId
|
|
127
|
+
);
|
|
128
|
+
return results.filter((result) => {
|
|
129
|
+
const metadata = result.metadata ?? {};
|
|
130
|
+
if (toBooleanValue(metadata.archived)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (input.botId && metadata.botId !== input.botId) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return result.score >= input.threshold;
|
|
137
|
+
}).map((result) => ({
|
|
138
|
+
id: result.id,
|
|
139
|
+
content: result.content,
|
|
140
|
+
similarity: result.score,
|
|
141
|
+
metadata: result.metadata ?? {}
|
|
142
|
+
})).slice(0, input.limit);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/storage/postgres-raw-message-factory.ts
|
|
146
|
+
var factory = null;
|
|
147
|
+
async function resolvePostgresFactory(env) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function hasPostgresFactory() {
|
|
151
|
+
return factory !== null;
|
|
152
|
+
}
|
|
153
|
+
var manager = null;
|
|
154
|
+
function isSQLiteRawMessageStorageAvailable(env) {
|
|
155
|
+
return resolveEnv(env).isTauriMode();
|
|
156
|
+
}
|
|
157
|
+
async function getSQLiteRawMessageManager(env) {
|
|
158
|
+
const e = resolveEnv(env);
|
|
159
|
+
if (!e.isTauriMode()) {
|
|
160
|
+
throw new Error("SQLite raw message storage is only available in Tauri mode.");
|
|
161
|
+
}
|
|
162
|
+
if (!manager) {
|
|
163
|
+
const dbPath = e.getTauriDbPath?.() ?? process.env.TAURI_DB_PATH ?? "";
|
|
164
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
165
|
+
manager = new SQLiteRawMessageManager({
|
|
166
|
+
dbPath,
|
|
167
|
+
enableVectorSearch: !isRawMessageChromaEnabled()
|
|
168
|
+
});
|
|
169
|
+
await manager.init();
|
|
170
|
+
}
|
|
171
|
+
return manager;
|
|
172
|
+
}
|
|
173
|
+
async function closeSQLiteRawMessageManager() {
|
|
174
|
+
if (!manager) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await manager.close();
|
|
178
|
+
manager = null;
|
|
179
|
+
}
|
|
180
|
+
function resolveEnv(env) {
|
|
181
|
+
if (env) return env;
|
|
182
|
+
return {
|
|
183
|
+
isTauriMode: () => process.env.IS_TAURI === "true" || typeof process.env.TAURI_MODE === "string",
|
|
184
|
+
getTauriDbPath: () => process.env.TAURI_DB_PATH ?? "",
|
|
185
|
+
getTauriDataDir: () => process.env.TAURI_DATA_DIR ?? ""
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/storage/raw-message-store.ts
|
|
190
|
+
function resolveEnv2(env) {
|
|
191
|
+
if (env) return env;
|
|
192
|
+
return {
|
|
193
|
+
isTauriMode: () => process.env.IS_TAURI === "true" || typeof process.env.TAURI_MODE === "string",
|
|
194
|
+
getTauriDbPath: () => process.env.TAURI_DB_PATH ?? "",
|
|
195
|
+
getTauriDataDir: () => process.env.TAURI_DATA_DIR ?? ""
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function createRawMessageStore(options = {}) {
|
|
199
|
+
const env = resolveEnv2(options.env);
|
|
200
|
+
return {
|
|
201
|
+
getBackend() {
|
|
202
|
+
return isSQLiteRawMessageStorageAvailable(env) ? "sqlite" : "postgres";
|
|
203
|
+
},
|
|
204
|
+
isAvailable() {
|
|
205
|
+
return isSQLiteRawMessageStorageAvailable(env) || hasPostgresFactory();
|
|
206
|
+
},
|
|
207
|
+
async getManager() {
|
|
208
|
+
if (isSQLiteRawMessageStorageAvailable(env)) {
|
|
209
|
+
return await getSQLiteRawMessageManager(env);
|
|
210
|
+
}
|
|
211
|
+
const pg = await resolvePostgresFactory();
|
|
212
|
+
if (!pg) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"No raw-message backend available. SQLite is disabled (not Tauri) and no Postgres factory is registered."
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return pg;
|
|
218
|
+
},
|
|
219
|
+
async close() {
|
|
220
|
+
if (isSQLiteRawMessageStorageAvailable(env)) {
|
|
221
|
+
await closeSQLiteRawMessageManager();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
var moduleStore = null;
|
|
227
|
+
var moduleConfig = null;
|
|
228
|
+
function isRawMessageStorageAvailable() {
|
|
229
|
+
if (!moduleStore) {
|
|
230
|
+
moduleStore = createRawMessageStore({ env: moduleConfig?.env });
|
|
231
|
+
}
|
|
232
|
+
return moduleStore.isAvailable();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/search/utilities.ts
|
|
236
|
+
var DEFAULT_LIMIT = 10;
|
|
237
|
+
var DEFAULT_THRESHOLD = 0.7;
|
|
238
|
+
var DEFAULT_SOURCES = ["memory", "insights", "knowledge"];
|
|
239
|
+
var SOURCE_SET = new Set(DEFAULT_SOURCES);
|
|
240
|
+
function normalizeUnifiedMemorySearchSources(sources) {
|
|
241
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
242
|
+
return [...DEFAULT_SOURCES];
|
|
243
|
+
}
|
|
244
|
+
const normalized = sources.filter((source) => typeof source === "string").map((source) => source.trim().toLowerCase()).filter(
|
|
245
|
+
(source) => SOURCE_SET.has(source)
|
|
246
|
+
);
|
|
247
|
+
return normalized.length > 0 ? Array.from(new Set(normalized)) : [...DEFAULT_SOURCES];
|
|
248
|
+
}
|
|
249
|
+
function clampUnifiedMemorySearchLimit(limit) {
|
|
250
|
+
const parsed = typeof limit === "number" ? limit : Number(limit ?? DEFAULT_LIMIT);
|
|
251
|
+
if (!Number.isFinite(parsed)) {
|
|
252
|
+
return DEFAULT_LIMIT;
|
|
253
|
+
}
|
|
254
|
+
return Math.min(50, Math.max(1, Math.floor(parsed)));
|
|
255
|
+
}
|
|
256
|
+
function clampUnifiedMemorySearchThreshold(threshold) {
|
|
257
|
+
const parsed = typeof threshold === "number" ? threshold : Number(threshold ?? DEFAULT_THRESHOLD);
|
|
258
|
+
if (!Number.isFinite(parsed)) {
|
|
259
|
+
return DEFAULT_THRESHOLD;
|
|
260
|
+
}
|
|
261
|
+
return Math.min(1, Math.max(-1, parsed));
|
|
262
|
+
}
|
|
263
|
+
function mergeUnifiedMemorySearchResults(results, limit) {
|
|
264
|
+
return [...results].sort((a, b) => {
|
|
265
|
+
const scoreDelta = b.similarity - a.similarity;
|
|
266
|
+
if (scoreDelta !== 0) {
|
|
267
|
+
return scoreDelta;
|
|
268
|
+
}
|
|
269
|
+
return a.type.localeCompare(b.type) || a.id.localeCompare(b.id);
|
|
270
|
+
}).slice(0, limit);
|
|
271
|
+
}
|
|
272
|
+
function toKnowledgeResult(result) {
|
|
273
|
+
return {
|
|
274
|
+
type: "knowledge",
|
|
275
|
+
id: result.chunkId,
|
|
276
|
+
content: result.content,
|
|
277
|
+
similarity: result.similarity,
|
|
278
|
+
metadata: {
|
|
279
|
+
documentId: result.documentId,
|
|
280
|
+
documentName: result.documentName,
|
|
281
|
+
chunkIndex: result.chunkIndex
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function toMemoryResult(result) {
|
|
286
|
+
return {
|
|
287
|
+
type: "memory",
|
|
288
|
+
id: result.id,
|
|
289
|
+
content: result.content,
|
|
290
|
+
similarity: result.similarity,
|
|
291
|
+
metadata: result.metadata
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function isRawMemorySemanticResult(result) {
|
|
295
|
+
if (!result || typeof result !== "object") {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const item = result;
|
|
299
|
+
return typeof item.id === "string" && typeof item.content === "string" && typeof item.similarity === "number" && Boolean(item.metadata) && typeof item.metadata === "object";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/search/unified-search.ts
|
|
303
|
+
function createUnifiedSearch(deps = {}) {
|
|
304
|
+
const logger = deps.logger ?? console;
|
|
305
|
+
async function searchUnifiedMemory(input) {
|
|
306
|
+
const query = input.query.trim();
|
|
307
|
+
const sources = normalizeUnifiedMemorySearchSources(input.sources);
|
|
308
|
+
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
309
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
310
|
+
const warnings = [];
|
|
311
|
+
const results = [];
|
|
312
|
+
if (!query) {
|
|
313
|
+
return {
|
|
314
|
+
query,
|
|
315
|
+
sources,
|
|
316
|
+
results: [],
|
|
317
|
+
count: 0,
|
|
318
|
+
warnings
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (sources.includes("memory")) {
|
|
322
|
+
if (isRawMessageStorageAvailable()) {
|
|
323
|
+
try {
|
|
324
|
+
const memoryHits = await runMemorySource(input, limit, threshold);
|
|
325
|
+
results.push(...memoryHits);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
logger.warn?.("[memory-store] memory source failed:", error);
|
|
328
|
+
warnings.push({
|
|
329
|
+
source: "memory",
|
|
330
|
+
code: "memory_search_failed",
|
|
331
|
+
message: error.message ?? "memory_search_failed"
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
warnings.push({
|
|
336
|
+
source: "memory",
|
|
337
|
+
code: "raw_message_storage_unavailable",
|
|
338
|
+
message: "Raw memory storage is not available in this environment."
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (sources.includes("insights") && typeof deps.searchInsights === "function") {
|
|
343
|
+
try {
|
|
344
|
+
const insightHits = await deps.searchInsights({
|
|
345
|
+
userId: input.userId,
|
|
346
|
+
query: input.query,
|
|
347
|
+
limit,
|
|
348
|
+
threshold,
|
|
349
|
+
botIds: input.botIds,
|
|
350
|
+
includeArchived: input.includeArchivedInsights,
|
|
351
|
+
authToken: input.authToken
|
|
352
|
+
});
|
|
353
|
+
for (const hit of insightHits) {
|
|
354
|
+
results.push({
|
|
355
|
+
type: "insight",
|
|
356
|
+
id: hit.id,
|
|
357
|
+
content: hit.content,
|
|
358
|
+
similarity: hit.similarity,
|
|
359
|
+
metadata: hit.metadata
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
logger.warn?.("[memory-store] insights source failed:", error);
|
|
364
|
+
warnings.push({
|
|
365
|
+
source: "insights",
|
|
366
|
+
code: "insights_search_failed",
|
|
367
|
+
message: error.message ?? "insights_search_failed"
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
} else if (sources.includes("insights")) {
|
|
371
|
+
warnings.push({
|
|
372
|
+
source: "insights",
|
|
373
|
+
code: "insights_search_not_configured",
|
|
374
|
+
message: "Insights search is not configured for this host."
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (sources.includes("knowledge") && typeof deps.searchKnowledge === "function") {
|
|
378
|
+
try {
|
|
379
|
+
const knowledgeHits = await deps.searchKnowledge({
|
|
380
|
+
userId: input.userId,
|
|
381
|
+
query: input.query,
|
|
382
|
+
options: {
|
|
383
|
+
limit,
|
|
384
|
+
threshold,
|
|
385
|
+
documentIds: input.documentIds
|
|
386
|
+
},
|
|
387
|
+
authToken: input.authToken
|
|
388
|
+
});
|
|
389
|
+
for (const hit of knowledgeHits) {
|
|
390
|
+
results.push(toKnowledgeResult(hit));
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
logger.warn?.("[memory-store] knowledge source failed:", error);
|
|
394
|
+
warnings.push({
|
|
395
|
+
source: "knowledge",
|
|
396
|
+
code: "knowledge_search_failed",
|
|
397
|
+
message: error.message ?? "knowledge_search_failed"
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
} else if (sources.includes("knowledge")) {
|
|
401
|
+
warnings.push({
|
|
402
|
+
source: "knowledge",
|
|
403
|
+
code: "knowledge_search_not_configured",
|
|
404
|
+
message: "Knowledge search is not configured for this host."
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const merged = mergeUnifiedMemorySearchResults(results, limit);
|
|
408
|
+
return {
|
|
409
|
+
query: input.query,
|
|
410
|
+
sources,
|
|
411
|
+
results: merged,
|
|
412
|
+
count: merged.length,
|
|
413
|
+
warnings
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
async function runMemorySource(input, limit, threshold) {
|
|
417
|
+
if (typeof deps.embedQuery !== "function") {
|
|
418
|
+
throw new Error("embedQuery is not configured");
|
|
419
|
+
}
|
|
420
|
+
const queryEmbedding = await deps.embedQuery({
|
|
421
|
+
userId: input.userId,
|
|
422
|
+
query: input.query,
|
|
423
|
+
authToken: input.authToken
|
|
424
|
+
});
|
|
425
|
+
const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
|
|
426
|
+
if (isRawMessageChromaEnabled()) {
|
|
427
|
+
try {
|
|
428
|
+
const chromaHits = (await Promise.all(
|
|
429
|
+
filters.map((filter) => {
|
|
430
|
+
const botId = "botId" in filter ? filter.botId : void 0;
|
|
431
|
+
return searchRawMessagesWithChroma({
|
|
432
|
+
userId: input.userId,
|
|
433
|
+
queryEmbedding,
|
|
434
|
+
limit,
|
|
435
|
+
threshold,
|
|
436
|
+
botId
|
|
437
|
+
});
|
|
438
|
+
})
|
|
439
|
+
)).flat().map(toMemoryResult);
|
|
440
|
+
logger.log?.("[memory-store] Raw message semantic search completed", {
|
|
441
|
+
backend: "chroma",
|
|
442
|
+
dimensions: queryEmbedding.length,
|
|
443
|
+
count: chromaHits.length
|
|
444
|
+
});
|
|
445
|
+
return chromaHits;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
logger.warn?.(
|
|
448
|
+
"[memory-store] Chroma raw message search failed; falling back to database search:",
|
|
449
|
+
error
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (typeof deps.searchRawMessagesAnn === "function") {
|
|
454
|
+
const rows = (await Promise.all(
|
|
455
|
+
filters.map(
|
|
456
|
+
(filter) => deps.searchRawMessagesAnn({
|
|
457
|
+
userId: input.userId,
|
|
458
|
+
queryEmbedding,
|
|
459
|
+
limit,
|
|
460
|
+
threshold,
|
|
461
|
+
botId: "botId" in filter ? filter.botId : void 0
|
|
462
|
+
})
|
|
463
|
+
)
|
|
464
|
+
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
465
|
+
return rows;
|
|
466
|
+
}
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
async function searchRawMemorySemantically(input) {
|
|
470
|
+
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
471
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
472
|
+
return runMemorySource(input, limit, threshold);
|
|
473
|
+
}
|
|
474
|
+
return { searchUnifiedMemory, searchRawMemorySemantically };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/mcp.ts
|
|
478
|
+
var DEFAULT_NAME = "@melandlabs/memory-store";
|
|
479
|
+
var DEFAULT_VERSION = "0.1.0";
|
|
480
|
+
async function startMcpServer(options = {}) {
|
|
481
|
+
const server = new McpServer({
|
|
482
|
+
name: options.name ?? DEFAULT_NAME,
|
|
483
|
+
version: options.version ?? DEFAULT_VERSION
|
|
484
|
+
});
|
|
485
|
+
const rawStore = createRawMessageStore({
|
|
486
|
+
env: options.env
|
|
487
|
+
});
|
|
488
|
+
const search = createUnifiedSearch(options.unified);
|
|
489
|
+
server.tool("memory.health", "Returns the health status of the memory store.", async () => ({
|
|
490
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true }) }]
|
|
491
|
+
}));
|
|
492
|
+
const searchSchema = {
|
|
493
|
+
userId: z.string(),
|
|
494
|
+
query: z.string().min(1),
|
|
495
|
+
sources: z.array(z.enum(["memory", "insights", "knowledge"])).optional().describe("Default: ['memory','insights','knowledge']"),
|
|
496
|
+
limit: z.number().int().min(1).max(50).default(10),
|
|
497
|
+
threshold: z.number().min(-1).max(1).default(0.7),
|
|
498
|
+
botIds: z.array(z.string()).optional(),
|
|
499
|
+
documentIds: z.array(z.string()).optional(),
|
|
500
|
+
includeArchivedInsights: z.boolean().default(false),
|
|
501
|
+
authToken: z.string().optional()
|
|
502
|
+
};
|
|
503
|
+
const writeSchema = {
|
|
504
|
+
userId: z.string(),
|
|
505
|
+
message: z.object({
|
|
506
|
+
id: z.string().optional(),
|
|
507
|
+
messageId: z.string().optional(),
|
|
508
|
+
role: z.string(),
|
|
509
|
+
content: z.union([z.string(), z.array(z.unknown())]),
|
|
510
|
+
platform: z.string().optional(),
|
|
511
|
+
botId: z.string().optional(),
|
|
512
|
+
channel: z.string().optional(),
|
|
513
|
+
person: z.string().optional(),
|
|
514
|
+
timestamp: z.number().optional(),
|
|
515
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
516
|
+
}).passthrough()
|
|
517
|
+
};
|
|
518
|
+
const getSchema = {
|
|
519
|
+
userId: z.string(),
|
|
520
|
+
messageId: z.string()
|
|
521
|
+
};
|
|
522
|
+
server.registerTool(
|
|
523
|
+
"memory.searchUnified",
|
|
524
|
+
{
|
|
525
|
+
title: "Search Unified Memory",
|
|
526
|
+
description: "Semantic search across raw messages (always), and optionally insights and uploaded knowledge.",
|
|
527
|
+
inputSchema: searchSchema
|
|
528
|
+
},
|
|
529
|
+
async (args) => {
|
|
530
|
+
const a = args;
|
|
531
|
+
const result = await search.searchUnifiedMemory(a);
|
|
532
|
+
return {
|
|
533
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
server.registerTool(
|
|
538
|
+
"memory.writeRawMessage",
|
|
539
|
+
{
|
|
540
|
+
title: "Write Raw Message",
|
|
541
|
+
description: "Persist a single raw message to the user's memory store.",
|
|
542
|
+
inputSchema: writeSchema
|
|
543
|
+
},
|
|
544
|
+
async (args) => {
|
|
545
|
+
const a = args;
|
|
546
|
+
const manager2 = await rawStore.getManager();
|
|
547
|
+
const result = await manager2.upsertRawMessages?.({
|
|
548
|
+
userId: a.userId,
|
|
549
|
+
messages: [a.message]
|
|
550
|
+
});
|
|
551
|
+
try {
|
|
552
|
+
await upsertRawMessagesToChroma([a.message]);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
console.warn("[memory-store/mcp] chroma upsert failed:", error);
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, result }) }]
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
server.registerTool(
|
|
562
|
+
"memory.getRawMessage",
|
|
563
|
+
{
|
|
564
|
+
title: "Get Raw Message",
|
|
565
|
+
description: "Fetch a single raw message by its message id.",
|
|
566
|
+
inputSchema: getSchema
|
|
567
|
+
},
|
|
568
|
+
async (args) => {
|
|
569
|
+
const a = args;
|
|
570
|
+
const manager2 = await rawStore.getManager();
|
|
571
|
+
const row = await manager2.getMessageById?.(a.messageId);
|
|
572
|
+
return {
|
|
573
|
+
content: [
|
|
574
|
+
{
|
|
575
|
+
type: "text",
|
|
576
|
+
text: JSON.stringify({ message: row ?? null })
|
|
577
|
+
}
|
|
578
|
+
]
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
const transport = new StdioServerTransport();
|
|
583
|
+
await server.connect(transport);
|
|
584
|
+
return server;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export { startMcpServer };
|
|
588
|
+
//# sourceMappingURL=mcp.js.map
|
|
589
|
+
//# sourceMappingURL=mcp.js.map
|