mem0-mcp 0.2.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/CHANGELOG.md +24 -0
- package/LICENSE +27 -0
- package/README.md +71 -0
- package/dist/adapters/ollama/ollama-embedder.d.ts +16 -0
- package/dist/adapters/ollama/ollama-embedder.js +132 -0
- package/dist/adapters/sqlite/sqlite-memory-store.d.ts +27 -0
- package/dist/adapters/sqlite/sqlite-memory-store.js +217 -0
- package/dist/bin/mem0-mcp.d.ts +5 -0
- package/dist/bin/mem0-mcp.js +76 -0
- package/dist/domain/errors.d.ts +7 -0
- package/dist/domain/errors.js +12 -0
- package/dist/domain/memory.types.d.ts +158 -0
- package/dist/domain/memory.types.js +115 -0
- package/dist/domain/memory.utils.d.ts +9 -0
- package/dist/domain/memory.utils.js +70 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +14 -0
- package/dist/ports/embedder.port.d.ts +12 -0
- package/dist/ports/embedder.port.js +4 -0
- package/dist/ports/memory-store.port.d.ts +19 -0
- package/dist/ports/memory-store.port.js +4 -0
- package/dist/test/mem0-mcp-server.test.d.ts +1 -0
- package/dist/test/mem0-mcp-server.test.js +122 -0
- package/dist/test/ollama-embedder.test.d.ts +1 -0
- package/dist/test/ollama-embedder.test.js +75 -0
- package/dist/test/setup-wizard.tool.test.d.ts +1 -0
- package/dist/test/setup-wizard.tool.test.js +31 -0
- package/dist/test/sqlite-memory-store.test.d.ts +1 -0
- package/dist/test/sqlite-memory-store.test.js +110 -0
- package/dist/transport/jsonrpc-stdio.d.ts +73 -0
- package/dist/transport/jsonrpc-stdio.js +230 -0
- package/dist/transport/mcp-server.d.ts +38 -0
- package/dist/transport/mcp-server.js +156 -0
- package/dist/transport/tools/health.tool.d.ts +3 -0
- package/dist/transport/tools/health.tool.js +13 -0
- package/dist/transport/tools/memory-forget.tool.d.ts +3 -0
- package/dist/transport/tools/memory-forget.tool.js +22 -0
- package/dist/transport/tools/memory-recall.tool.d.ts +3 -0
- package/dist/transport/tools/memory-recall.tool.js +22 -0
- package/dist/transport/tools/memory-search.tool.d.ts +3 -0
- package/dist/transport/tools/memory-search.tool.js +27 -0
- package/dist/transport/tools/memory-store.tool.d.ts +3 -0
- package/dist/transport/tools/memory-store.tool.js +32 -0
- package/dist/transport/tools/memory-update.tool.d.ts +3 -0
- package/dist/transport/tools/memory-update.tool.js +24 -0
- package/dist/transport/tools/setup-wizard.tool.d.ts +7 -0
- package/dist/transport/tools/setup-wizard.tool.js +35 -0
- package/dist/transport/tools/shared-schemas.d.ts +46 -0
- package/dist/transport/tools/shared-schemas.js +30 -0
- package/package.json +59 -0
- package/scripts/prepare.cjs +59 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All Zod schemas, types, and configuration for the mem0 domain.
|
|
3
|
+
* This is the single source of truth for data shapes.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export declare const memoryKindSchema: z.ZodEnum<{
|
|
7
|
+
decision: "decision";
|
|
8
|
+
preference: "preference";
|
|
9
|
+
summary: "summary";
|
|
10
|
+
artifact_context: "artifact_context";
|
|
11
|
+
note: "note";
|
|
12
|
+
}>;
|
|
13
|
+
export type MemoryKind = z.infer<typeof memoryKindSchema>;
|
|
14
|
+
export declare const memoryScopeSchema: z.ZodObject<{
|
|
15
|
+
workspace: z.ZodString;
|
|
16
|
+
project: z.ZodString;
|
|
17
|
+
campaign: z.ZodOptional<z.ZodString>;
|
|
18
|
+
task: z.ZodOptional<z.ZodString>;
|
|
19
|
+
run: z.ZodOptional<z.ZodString>;
|
|
20
|
+
}, z.core.$loose>;
|
|
21
|
+
export type MemoryScope = z.infer<typeof memoryScopeSchema>;
|
|
22
|
+
export declare const memoryProvenanceSchema: z.ZodObject<{
|
|
23
|
+
checkpointId: z.ZodOptional<z.ZodString>;
|
|
24
|
+
artifactIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
25
|
+
note: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, z.core.$loose>;
|
|
27
|
+
export type MemoryProvenance = z.infer<typeof memoryProvenanceSchema>;
|
|
28
|
+
export declare const memoryMetadataSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
29
|
+
export type MemoryMetadata = z.infer<typeof memoryMetadataSchema>;
|
|
30
|
+
export declare const memoryStoreInputSchema: z.ZodObject<{
|
|
31
|
+
kind: z.ZodEnum<{
|
|
32
|
+
decision: "decision";
|
|
33
|
+
preference: "preference";
|
|
34
|
+
summary: "summary";
|
|
35
|
+
artifact_context: "artifact_context";
|
|
36
|
+
note: "note";
|
|
37
|
+
}>;
|
|
38
|
+
content: z.ZodString;
|
|
39
|
+
scope: z.ZodObject<{
|
|
40
|
+
workspace: z.ZodString;
|
|
41
|
+
project: z.ZodString;
|
|
42
|
+
campaign: z.ZodOptional<z.ZodString>;
|
|
43
|
+
task: z.ZodOptional<z.ZodString>;
|
|
44
|
+
run: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, z.core.$loose>;
|
|
46
|
+
provenance: z.ZodObject<{
|
|
47
|
+
checkpointId: z.ZodOptional<z.ZodString>;
|
|
48
|
+
artifactIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
49
|
+
note: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$loose>;
|
|
51
|
+
metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
52
|
+
}, z.core.$loose>;
|
|
53
|
+
export type MemoryStoreInput = z.infer<typeof memoryStoreInputSchema>;
|
|
54
|
+
export declare const memoryRecallInputSchema: z.ZodObject<{
|
|
55
|
+
memoryId: z.ZodString;
|
|
56
|
+
scope: z.ZodObject<{
|
|
57
|
+
workspace: z.ZodString;
|
|
58
|
+
project: z.ZodString;
|
|
59
|
+
campaign: z.ZodOptional<z.ZodString>;
|
|
60
|
+
task: z.ZodOptional<z.ZodString>;
|
|
61
|
+
run: z.ZodOptional<z.ZodString>;
|
|
62
|
+
}, z.core.$loose>;
|
|
63
|
+
}, z.core.$loose>;
|
|
64
|
+
export type MemoryRecallInput = z.infer<typeof memoryRecallInputSchema>;
|
|
65
|
+
export declare const memorySearchInputSchema: z.ZodObject<{
|
|
66
|
+
query: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
67
|
+
scope: z.ZodObject<{
|
|
68
|
+
workspace: z.ZodString;
|
|
69
|
+
project: z.ZodString;
|
|
70
|
+
campaign: z.ZodOptional<z.ZodString>;
|
|
71
|
+
task: z.ZodOptional<z.ZodString>;
|
|
72
|
+
run: z.ZodOptional<z.ZodString>;
|
|
73
|
+
}, z.core.$loose>;
|
|
74
|
+
kind: z.ZodOptional<z.ZodEnum<{
|
|
75
|
+
decision: "decision";
|
|
76
|
+
preference: "preference";
|
|
77
|
+
summary: "summary";
|
|
78
|
+
artifact_context: "artifact_context";
|
|
79
|
+
note: "note";
|
|
80
|
+
}>>;
|
|
81
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
82
|
+
}, z.core.$loose>;
|
|
83
|
+
export type MemorySearchInput = z.infer<typeof memorySearchInputSchema>;
|
|
84
|
+
export declare const memoryUpdateInputSchema: z.ZodObject<{
|
|
85
|
+
memoryId: z.ZodString;
|
|
86
|
+
scope: z.ZodObject<{
|
|
87
|
+
workspace: z.ZodString;
|
|
88
|
+
project: z.ZodString;
|
|
89
|
+
campaign: z.ZodOptional<z.ZodString>;
|
|
90
|
+
task: z.ZodOptional<z.ZodString>;
|
|
91
|
+
run: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$loose>;
|
|
93
|
+
content: z.ZodOptional<z.ZodString>;
|
|
94
|
+
metadata: z.ZodOptional<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
95
|
+
}, z.core.$loose>;
|
|
96
|
+
export type MemoryUpdateInput = z.infer<typeof memoryUpdateInputSchema>;
|
|
97
|
+
export declare const memoryForgetInputSchema: z.ZodObject<{
|
|
98
|
+
memoryId: z.ZodString;
|
|
99
|
+
scope: z.ZodObject<{
|
|
100
|
+
workspace: z.ZodString;
|
|
101
|
+
project: z.ZodString;
|
|
102
|
+
campaign: z.ZodOptional<z.ZodString>;
|
|
103
|
+
task: z.ZodOptional<z.ZodString>;
|
|
104
|
+
run: z.ZodOptional<z.ZodString>;
|
|
105
|
+
}, z.core.$loose>;
|
|
106
|
+
}, z.core.$loose>;
|
|
107
|
+
export type MemoryForgetInput = z.infer<typeof memoryForgetInputSchema>;
|
|
108
|
+
export declare const storedMemoryRecordSchema: z.ZodObject<{
|
|
109
|
+
kind: z.ZodEnum<{
|
|
110
|
+
decision: "decision";
|
|
111
|
+
preference: "preference";
|
|
112
|
+
summary: "summary";
|
|
113
|
+
artifact_context: "artifact_context";
|
|
114
|
+
note: "note";
|
|
115
|
+
}>;
|
|
116
|
+
content: z.ZodString;
|
|
117
|
+
scope: z.ZodObject<{
|
|
118
|
+
workspace: z.ZodString;
|
|
119
|
+
project: z.ZodString;
|
|
120
|
+
campaign: z.ZodOptional<z.ZodString>;
|
|
121
|
+
task: z.ZodOptional<z.ZodString>;
|
|
122
|
+
run: z.ZodOptional<z.ZodString>;
|
|
123
|
+
}, z.core.$loose>;
|
|
124
|
+
provenance: z.ZodObject<{
|
|
125
|
+
checkpointId: z.ZodOptional<z.ZodString>;
|
|
126
|
+
artifactIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
127
|
+
note: z.ZodOptional<z.ZodString>;
|
|
128
|
+
}, z.core.$loose>;
|
|
129
|
+
metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
130
|
+
id: z.ZodString;
|
|
131
|
+
embedding: z.ZodArray<z.ZodNumber>;
|
|
132
|
+
createdAt: z.ZodString;
|
|
133
|
+
updatedAt: z.ZodString;
|
|
134
|
+
}, z.core.$loose>;
|
|
135
|
+
export type StoredMemoryRecord = z.infer<typeof storedMemoryRecordSchema>;
|
|
136
|
+
export type PublicMemoryRecord = Omit<StoredMemoryRecord, 'embedding'>;
|
|
137
|
+
export interface MemorySearchResult {
|
|
138
|
+
memory: PublicMemoryRecord;
|
|
139
|
+
score: number;
|
|
140
|
+
}
|
|
141
|
+
export interface HealthCheckResult {
|
|
142
|
+
ok: boolean;
|
|
143
|
+
storePath: string;
|
|
144
|
+
ollamaBaseUrl: string;
|
|
145
|
+
embedModel: string;
|
|
146
|
+
modelAvailable: boolean;
|
|
147
|
+
recordCount: number;
|
|
148
|
+
details?: string;
|
|
149
|
+
}
|
|
150
|
+
export declare const mem0ConfigSchema: z.ZodObject<{
|
|
151
|
+
storePath: z.ZodString;
|
|
152
|
+
ollamaBaseUrl: z.ZodString;
|
|
153
|
+
embedModel: z.ZodString;
|
|
154
|
+
ollamaTimeoutMs: z.ZodNumber;
|
|
155
|
+
}, z.core.$loose>;
|
|
156
|
+
export type Mem0Config = z.infer<typeof mem0ConfigSchema>;
|
|
157
|
+
export declare function loadMem0ConfigFromEnv(env?: NodeJS.ProcessEnv): Mem0Config;
|
|
158
|
+
export declare function toPublicMemoryRecord(record: StoredMemoryRecord): PublicMemoryRecord;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All Zod schemas, types, and configuration for the mem0 domain.
|
|
3
|
+
* This is the single source of truth for data shapes.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
// ─── Memory Kind ────────────────────────────────────────────────────
|
|
9
|
+
export const memoryKindSchema = z.enum([
|
|
10
|
+
'decision',
|
|
11
|
+
'preference',
|
|
12
|
+
'summary',
|
|
13
|
+
'artifact_context',
|
|
14
|
+
'note',
|
|
15
|
+
]);
|
|
16
|
+
// ─── Scope ──────────────────────────────────────────────────────────
|
|
17
|
+
export const memoryScopeSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
workspace: z.string().min(1),
|
|
20
|
+
project: z.string().min(1),
|
|
21
|
+
campaign: z.string().min(1).optional(),
|
|
22
|
+
task: z.string().min(1).optional(),
|
|
23
|
+
run: z.string().min(1).optional(),
|
|
24
|
+
})
|
|
25
|
+
.passthrough();
|
|
26
|
+
// ─── Provenance ─────────────────────────────────────────────────────
|
|
27
|
+
export const memoryProvenanceSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
checkpointId: z.string().min(1).optional(),
|
|
30
|
+
artifactIds: z.array(z.string().min(1)).default([]),
|
|
31
|
+
note: z.string().min(1).optional(),
|
|
32
|
+
})
|
|
33
|
+
.passthrough();
|
|
34
|
+
// ─── Metadata ───────────────────────────────────────────────────────
|
|
35
|
+
export const memoryMetadataSchema = z.record(z.string(), z.string()).default({});
|
|
36
|
+
// ─── Input Schemas ──────────────────────────────────────────────────
|
|
37
|
+
export const memoryStoreInputSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
kind: memoryKindSchema,
|
|
40
|
+
content: z.string().min(1),
|
|
41
|
+
scope: memoryScopeSchema,
|
|
42
|
+
provenance: memoryProvenanceSchema,
|
|
43
|
+
metadata: memoryMetadataSchema,
|
|
44
|
+
})
|
|
45
|
+
.passthrough();
|
|
46
|
+
export const memoryRecallInputSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
memoryId: z.string().uuid(),
|
|
49
|
+
scope: memoryScopeSchema,
|
|
50
|
+
})
|
|
51
|
+
.passthrough();
|
|
52
|
+
export const memorySearchInputSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
query: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),
|
|
55
|
+
scope: memoryScopeSchema,
|
|
56
|
+
kind: memoryKindSchema.optional(),
|
|
57
|
+
limit: z.number().int().min(1).max(25).default(5),
|
|
58
|
+
})
|
|
59
|
+
.passthrough();
|
|
60
|
+
export const memoryUpdateInputSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
memoryId: z.string().uuid(),
|
|
63
|
+
scope: memoryScopeSchema,
|
|
64
|
+
content: z.string().min(1).optional(),
|
|
65
|
+
metadata: memoryMetadataSchema.optional(),
|
|
66
|
+
})
|
|
67
|
+
.passthrough();
|
|
68
|
+
export const memoryForgetInputSchema = z
|
|
69
|
+
.object({
|
|
70
|
+
memoryId: z.string().uuid(),
|
|
71
|
+
scope: memoryScopeSchema,
|
|
72
|
+
})
|
|
73
|
+
.passthrough();
|
|
74
|
+
// ─── Records ────────────────────────────────────────────────────────
|
|
75
|
+
export const storedMemoryRecordSchema = memoryStoreInputSchema.extend({
|
|
76
|
+
id: z.string().uuid(),
|
|
77
|
+
embedding: z.array(z.number()),
|
|
78
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
79
|
+
updatedAt: z.string().datetime({ offset: true }),
|
|
80
|
+
});
|
|
81
|
+
// ─── Config ─────────────────────────────────────────────────────────
|
|
82
|
+
export const mem0ConfigSchema = z
|
|
83
|
+
.object({
|
|
84
|
+
storePath: z.string().min(1),
|
|
85
|
+
ollamaBaseUrl: z.string().url(),
|
|
86
|
+
embedModel: z.string().min(1),
|
|
87
|
+
ollamaTimeoutMs: z.number().int().positive(),
|
|
88
|
+
})
|
|
89
|
+
.passthrough();
|
|
90
|
+
export function loadMem0ConfigFromEnv(env = process.env) {
|
|
91
|
+
return mem0ConfigSchema.parse({
|
|
92
|
+
storePath: expandHomePath(env.MEM0_STORE_PATH ?? '~/.copilot/mem0'),
|
|
93
|
+
ollamaBaseUrl: env.OLLAMA_BASE_URL ?? 'http://127.0.0.1:11434',
|
|
94
|
+
embedModel: env.MEM0_EMBED_MODEL ?? 'qwen3-embedding:latest',
|
|
95
|
+
ollamaTimeoutMs: parseIntegerEnv(env.MEM0_OLLAMA_TIMEOUT_MS, 10_000),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
99
|
+
export function toPublicMemoryRecord(record) {
|
|
100
|
+
const { embedding: _embedding, ...publicRecord } = record;
|
|
101
|
+
return publicRecord;
|
|
102
|
+
}
|
|
103
|
+
function expandHomePath(input) {
|
|
104
|
+
if (input === '~')
|
|
105
|
+
return homedir();
|
|
106
|
+
if (input.startsWith('~/'))
|
|
107
|
+
return resolve(homedir(), input.slice(2));
|
|
108
|
+
return input;
|
|
109
|
+
}
|
|
110
|
+
function parseIntegerEnv(input, fallback) {
|
|
111
|
+
if (input === undefined || input.trim().length === 0) {
|
|
112
|
+
return fallback;
|
|
113
|
+
}
|
|
114
|
+
return Number.parseInt(input, 10);
|
|
115
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure domain functions for memory operations.
|
|
3
|
+
* Zero infrastructure dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import type { MemoryScope } from './memory.types.js';
|
|
6
|
+
export declare function matchesScope(recordScope: MemoryScope, requestedScope: MemoryScope): boolean;
|
|
7
|
+
export declare function cosineSimilarity(left: number[], right: number[]): number;
|
|
8
|
+
export declare function buildEmbeddingSource(kind: string, content: string, scope: Record<string, unknown>, provenance: Record<string, unknown>, metadata: Record<string, unknown>): string;
|
|
9
|
+
export declare function buildMemoryDedupeKey(kind: string, content: string, scope: Record<string, unknown>, metadata: Record<string, unknown>): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure domain functions for memory operations.
|
|
3
|
+
* Zero infrastructure dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
// ─── Scope Matching ─────────────────────────────────────────────────
|
|
7
|
+
export function matchesScope(recordScope, requestedScope) {
|
|
8
|
+
return (recordScope.workspace === requestedScope.workspace &&
|
|
9
|
+
recordScope.project === requestedScope.project &&
|
|
10
|
+
matchesOptionalField(recordScope.campaign, requestedScope.campaign) &&
|
|
11
|
+
matchesOptionalField(recordScope.task, requestedScope.task) &&
|
|
12
|
+
matchesOptionalField(recordScope.run, requestedScope.run));
|
|
13
|
+
}
|
|
14
|
+
function matchesOptionalField(recordValue, requestedValue) {
|
|
15
|
+
if (requestedValue === undefined)
|
|
16
|
+
return true;
|
|
17
|
+
return recordValue === requestedValue;
|
|
18
|
+
}
|
|
19
|
+
// ─── Cosine Similarity ──────────────────────────────────────────────
|
|
20
|
+
export function cosineSimilarity(left, right) {
|
|
21
|
+
if (left.length !== right.length) {
|
|
22
|
+
throw new Error(`Embedding dimension mismatch: query=${left.length}, record=${right.length}`);
|
|
23
|
+
}
|
|
24
|
+
let dot = 0;
|
|
25
|
+
let leftMag = 0;
|
|
26
|
+
let rightMag = 0;
|
|
27
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
28
|
+
dot += left[i] * right[i];
|
|
29
|
+
leftMag += left[i] * left[i];
|
|
30
|
+
rightMag += right[i] * right[i];
|
|
31
|
+
}
|
|
32
|
+
if (leftMag === 0 || rightMag === 0)
|
|
33
|
+
return 0;
|
|
34
|
+
return dot / (Math.sqrt(leftMag) * Math.sqrt(rightMag));
|
|
35
|
+
}
|
|
36
|
+
// ─── Embedding Source Builder ───────────────────────────────────────
|
|
37
|
+
export function buildEmbeddingSource(kind, content, scope, provenance, metadata) {
|
|
38
|
+
const scopeBlock = buildStringRecordBlock(scope);
|
|
39
|
+
const metadataBlock = buildStringRecordBlock(metadata);
|
|
40
|
+
const artifactIds = provenance.artifactIds;
|
|
41
|
+
const artifactBlock = artifactIds && artifactIds.length > 0 ? artifactIds.join(', ') : 'none';
|
|
42
|
+
return [
|
|
43
|
+
`kind: ${kind}`,
|
|
44
|
+
content,
|
|
45
|
+
scopeBlock.length > 0 ? `scope:\n${scopeBlock}` : '',
|
|
46
|
+
provenance.checkpointId ? `checkpointId: ${provenance.checkpointId}` : '',
|
|
47
|
+
`artifactIds: ${artifactBlock}`,
|
|
48
|
+
provenance.note ? `note: ${provenance.note}` : '',
|
|
49
|
+
metadataBlock.length > 0 ? `metadata:\n${metadataBlock}` : '',
|
|
50
|
+
]
|
|
51
|
+
.filter((block) => block.length > 0)
|
|
52
|
+
.join('\n\n');
|
|
53
|
+
}
|
|
54
|
+
export function buildMemoryDedupeKey(kind, content, scope, metadata) {
|
|
55
|
+
return createHash('sha256')
|
|
56
|
+
.update([
|
|
57
|
+
`kind: ${kind}`,
|
|
58
|
+
`content:\n${content}`,
|
|
59
|
+
`scope:\n${buildStringRecordBlock(scope)}`,
|
|
60
|
+
`metadata:\n${buildStringRecordBlock(metadata)}`,
|
|
61
|
+
].join('\n\n'), 'utf8')
|
|
62
|
+
.digest('hex');
|
|
63
|
+
}
|
|
64
|
+
function buildStringRecordBlock(record) {
|
|
65
|
+
return Object.entries(record)
|
|
66
|
+
.filter(([, value]) => typeof value === 'string' && value.length > 0)
|
|
67
|
+
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
|
68
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
69
|
+
.join('\n');
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './domain/memory.types.js';
|
|
2
|
+
export * from './domain/memory.utils.js';
|
|
3
|
+
export * from './domain/errors.js';
|
|
4
|
+
export * from './ports/memory-store.port.js';
|
|
5
|
+
export * from './ports/embedder.port.js';
|
|
6
|
+
export { SqliteMem0Adapter } from './adapters/sqlite/sqlite-memory-store.js';
|
|
7
|
+
export { OllamaEmbedder } from './adapters/ollama/ollama-embedder.js';
|
|
8
|
+
export { McpServer } from './transport/mcp-server.js';
|
|
9
|
+
export type { ToolDefinition } from './transport/mcp-server.js';
|
|
10
|
+
export * from './transport/jsonrpc-stdio.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Public API barrel — hexagonal architecture exports
|
|
2
|
+
// Domain
|
|
3
|
+
export * from './domain/memory.types.js';
|
|
4
|
+
export * from './domain/memory.utils.js';
|
|
5
|
+
export * from './domain/errors.js';
|
|
6
|
+
// Ports
|
|
7
|
+
export * from './ports/memory-store.port.js';
|
|
8
|
+
export * from './ports/embedder.port.js';
|
|
9
|
+
// Adapters
|
|
10
|
+
export { SqliteMem0Adapter } from './adapters/sqlite/sqlite-memory-store.js';
|
|
11
|
+
export { OllamaEmbedder } from './adapters/ollama/ollama-embedder.js';
|
|
12
|
+
// Transport
|
|
13
|
+
export { McpServer } from './transport/mcp-server.js';
|
|
14
|
+
export * from './transport/jsonrpc-stdio.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port: Embedder — contract for text embedding providers.
|
|
3
|
+
*/
|
|
4
|
+
export interface EmbedderPort {
|
|
5
|
+
embedText(text: string): Promise<number[]>;
|
|
6
|
+
healthCheck(): Promise<EmbedderHealthStatus>;
|
|
7
|
+
}
|
|
8
|
+
export interface EmbedderHealthStatus {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
modelAvailable: boolean;
|
|
11
|
+
details?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port: MemoryStore — contract for memory persistence backends.
|
|
3
|
+
*/
|
|
4
|
+
import type { HealthCheckResult, MemoryForgetInput, MemoryRecallInput, MemorySearchInput, MemorySearchResult, MemoryStoreInput, MemoryUpdateInput, PublicMemoryRecord } from '../domain/memory.types.js';
|
|
5
|
+
export interface MemoryStorePort {
|
|
6
|
+
healthCheck(): Promise<HealthCheckResult>;
|
|
7
|
+
storeMemory(input: MemoryStoreInput): Promise<PublicMemoryRecord>;
|
|
8
|
+
recallMemory(input: MemoryRecallInput): Promise<PublicMemoryRecord | null>;
|
|
9
|
+
searchMemory(input: MemorySearchInput): Promise<MemorySearchResult[]>;
|
|
10
|
+
updateMemory(input: MemoryUpdateInput): Promise<PublicMemoryRecord>;
|
|
11
|
+
deleteMemory(input: MemoryForgetInput): Promise<void>;
|
|
12
|
+
listWorkspaces(): Promise<string[]>;
|
|
13
|
+
listProjects(workspace: string): Promise<string[]>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Backward-compatible alias so downstream consumers (agent-harness-core)
|
|
17
|
+
* can keep importing `Mem0Adapter` without changes.
|
|
18
|
+
*/
|
|
19
|
+
export type Mem0Adapter = MemoryStorePort;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn, } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { JsonRpcError, JsonRpcStreamTransport, isJsonRpcErrorResponse, isJsonRpcSuccessResponse, } from '../transport/jsonrpc-stdio.js';
|
|
9
|
+
class TestJsonRpcClient {
|
|
10
|
+
child;
|
|
11
|
+
transport;
|
|
12
|
+
pendingRequests = new Map();
|
|
13
|
+
nextId = 1;
|
|
14
|
+
constructor(child) {
|
|
15
|
+
this.child = child;
|
|
16
|
+
this.transport = new JsonRpcStreamTransport((message) => this.handleMessage(message), {
|
|
17
|
+
input: child.stdout,
|
|
18
|
+
output: child.stdin,
|
|
19
|
+
});
|
|
20
|
+
this.transport.start();
|
|
21
|
+
}
|
|
22
|
+
async request(method, params) {
|
|
23
|
+
const id = this.nextId++;
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
const timeout = setTimeout(() => {
|
|
26
|
+
this.pendingRequests.delete(id);
|
|
27
|
+
reject(new Error(`Timed out waiting for response to ${method}`));
|
|
28
|
+
}, 5_000);
|
|
29
|
+
this.pendingRequests.set(id, {
|
|
30
|
+
reject,
|
|
31
|
+
resolve,
|
|
32
|
+
timeout,
|
|
33
|
+
});
|
|
34
|
+
this.transport.sendRequest(id, method, params);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
notify(method, params) {
|
|
38
|
+
this.transport.sendNotification(method, params);
|
|
39
|
+
}
|
|
40
|
+
async close() {
|
|
41
|
+
this.transport.stop();
|
|
42
|
+
for (const pendingRequest of this.pendingRequests.values()) {
|
|
43
|
+
clearTimeout(pendingRequest.timeout);
|
|
44
|
+
pendingRequest.reject(new Error('Client closed before receiving a response'));
|
|
45
|
+
}
|
|
46
|
+
this.pendingRequests.clear();
|
|
47
|
+
}
|
|
48
|
+
async handleMessage(message) {
|
|
49
|
+
if (isJsonRpcSuccessResponse(message) || isJsonRpcErrorResponse(message)) {
|
|
50
|
+
const pendingRequest = this.pendingRequests.get(message.id);
|
|
51
|
+
if (pendingRequest === undefined) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.pendingRequests.delete(message.id);
|
|
55
|
+
clearTimeout(pendingRequest.timeout);
|
|
56
|
+
if (isJsonRpcSuccessResponse(message)) {
|
|
57
|
+
pendingRequest.resolve(message.result);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
pendingRequest.reject(new JsonRpcError(message.error.code, message.error.message, message.error.data));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
test('mem0-mcp initializes and exposes memory tools', async () => {
|
|
66
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'mem0-mcp-server-'));
|
|
67
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
const distRoot = join(currentDir, '..');
|
|
69
|
+
const serverPath = join(distRoot, 'bin', 'mem0-mcp.js');
|
|
70
|
+
const packageVersion = JSON.parse(readFileSync(join(distRoot, '..', 'package.json'), 'utf8')).version;
|
|
71
|
+
const server = spawn(process.execPath, [serverPath], {
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
|
+
env: {
|
|
74
|
+
...process.env,
|
|
75
|
+
MEM0_STORE_PATH: join(tempDir, 'store'),
|
|
76
|
+
OLLAMA_BASE_URL: 'http://127.0.0.1:11434',
|
|
77
|
+
MEM0_EMBED_MODEL: 'stub',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const client = new TestJsonRpcClient(server);
|
|
81
|
+
try {
|
|
82
|
+
const initializeResult = (await client.request('initialize', {
|
|
83
|
+
protocolVersion: '2025-11-25',
|
|
84
|
+
capabilities: {
|
|
85
|
+
experimental: null,
|
|
86
|
+
roots: null,
|
|
87
|
+
sampling: null,
|
|
88
|
+
},
|
|
89
|
+
clientInfo: {
|
|
90
|
+
name: 'copilot-cli-test',
|
|
91
|
+
version: '1.0.0',
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
assert.equal(initializeResult.serverInfo.name, 'mem0-mcp');
|
|
95
|
+
assert.equal(initializeResult.serverInfo.version, packageVersion);
|
|
96
|
+
client.notify('notifications/initialized', {});
|
|
97
|
+
const toolList = (await client.request('tools/list', {}));
|
|
98
|
+
assert.deepEqual(toolList.tools.map((tool) => tool.name), [
|
|
99
|
+
'health',
|
|
100
|
+
'memory_store',
|
|
101
|
+
'memory_recall',
|
|
102
|
+
'memory_search',
|
|
103
|
+
'memory_update',
|
|
104
|
+
'memory_forget',
|
|
105
|
+
'setup_wizard',
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
try {
|
|
110
|
+
await client.request('shutdown', {});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Ignore shutdown failures during cleanup.
|
|
114
|
+
}
|
|
115
|
+
client.notify('exit');
|
|
116
|
+
await client.close();
|
|
117
|
+
if (!server.killed) {
|
|
118
|
+
server.kill('SIGTERM');
|
|
119
|
+
}
|
|
120
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { OllamaEmbedder } from '../index.js';
|
|
6
|
+
test('healthCheck times out when Ollama does not respond', async () => {
|
|
7
|
+
const harness = await createStallingServer();
|
|
8
|
+
const embedder = new OllamaEmbedder({
|
|
9
|
+
ollamaBaseUrl: harness.baseUrl,
|
|
10
|
+
embedModel: 'stub',
|
|
11
|
+
ollamaTimeoutMs: 50,
|
|
12
|
+
});
|
|
13
|
+
try {
|
|
14
|
+
const startedAt = Date.now();
|
|
15
|
+
const health = await embedder.healthCheck();
|
|
16
|
+
assert.equal(health.ok, false);
|
|
17
|
+
assert.match(health.details ?? '', /Timed out after 50ms contacting Ollama/);
|
|
18
|
+
assert.ok(Date.now() - startedAt < 1_000);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await harness.close();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
test('embedText aborts stalled Ollama requests', async () => {
|
|
25
|
+
const harness = await createStallingServer();
|
|
26
|
+
const embedder = new OllamaEmbedder({
|
|
27
|
+
ollamaBaseUrl: harness.baseUrl,
|
|
28
|
+
embedModel: 'stub',
|
|
29
|
+
ollamaTimeoutMs: 50,
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
await assert.rejects(() => embedder.embedText('hello'), /Timed out after 50ms contacting Ollama/);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await harness.close();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
async function createStallingServer() {
|
|
39
|
+
const sockets = new Set();
|
|
40
|
+
const server = createServer((_request, _response) => {
|
|
41
|
+
// Intentionally never respond so client-side timeouts are exercised.
|
|
42
|
+
});
|
|
43
|
+
server.on('connection', (socket) => {
|
|
44
|
+
sockets.add(socket);
|
|
45
|
+
socket.on('close', () => {
|
|
46
|
+
sockets.delete(socket);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
server.listen(0, '127.0.0.1');
|
|
50
|
+
await once(server, 'listening');
|
|
51
|
+
const address = server.address();
|
|
52
|
+
if (!address || typeof address === 'string') {
|
|
53
|
+
throw new Error('Unable to resolve test server address.');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
57
|
+
async close() {
|
|
58
|
+
await closeServer(server, sockets);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function closeServer(server, sockets) {
|
|
63
|
+
for (const socket of sockets) {
|
|
64
|
+
socket.destroy();
|
|
65
|
+
}
|
|
66
|
+
await new Promise((resolve, reject) => {
|
|
67
|
+
server.close((error) => {
|
|
68
|
+
if (error) {
|
|
69
|
+
reject(error);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createSetupWizardTool } from '../transport/tools/setup-wizard.tool.js';
|
|
4
|
+
test('setup_wizard pulls the configured embedding model', async () => {
|
|
5
|
+
const calls = [];
|
|
6
|
+
const tool = createSetupWizardTool({
|
|
7
|
+
embedModel: 'nomic-embed-text',
|
|
8
|
+
runCommand: async (command, args) => {
|
|
9
|
+
calls.push({ command, args });
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
const result = (await tool.handler({}));
|
|
13
|
+
assert.deepEqual(calls, [
|
|
14
|
+
{
|
|
15
|
+
command: 'ollama',
|
|
16
|
+
args: ['pull', 'nomic-embed-text'],
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
assert.deepEqual(result, {
|
|
20
|
+
success: true,
|
|
21
|
+
model: 'nomic-embed-text',
|
|
22
|
+
message: 'Ollama model nomic-embed-text pulled successfully.',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
test('setup_wizard rejects unexpected arguments', async () => {
|
|
26
|
+
const tool = createSetupWizardTool({
|
|
27
|
+
embedModel: 'qwen3-embedding:latest',
|
|
28
|
+
runCommand: async () => { },
|
|
29
|
+
});
|
|
30
|
+
await assert.rejects(() => tool.handler({ force: true }));
|
|
31
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|