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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.2.0 - 2026-04-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added deterministic deduplication with a stable `dedupe_key` so exact memory updates do not silently preserve stale content.
|
|
9
|
+
- Added abortable Ollama requests with configurable `MEM0_OLLAMA_TIMEOUT_MS`.
|
|
10
|
+
- Added repository metadata and installable package metadata for npm-based distribution.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `setup_wizard` now respects the configured embedding model instead of always pulling `qwen3-embedding:latest`.
|
|
14
|
+
- MCP initialization now reports the real package version from `package.json`.
|
|
15
|
+
- README now documents installation, host launch guidance, and current release highlights.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fixed soft deduplication behavior that could return the old content while only updating provenance and timestamps.
|
|
19
|
+
- Fixed package metadata drift between `package.json`, lockfile, and license declarations.
|
|
20
|
+
|
|
21
|
+
## 0.1.0 - 2026-03-31
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Initial dedicated `mem0` MCP server with SQLite-backed scope storage, provenance-aware memory tools, and Ollama embeddings.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
5
|
+
|
|
6
|
+
Parameters
|
|
7
|
+
Licensor: Giulio Leone
|
|
8
|
+
Licensed Work: mem0-mcp
|
|
9
|
+
Additional Use Grant: You may use the Licensed Work for non-commercial and non-production purposes (e.g., development, testing, research, and personal projects) free of charge. Any commercial or production use requires prior written authorization from the Licensor.
|
|
10
|
+
Change Date: 2030-03-22
|
|
11
|
+
Change License: Apache License, Version 2.0
|
|
12
|
+
|
|
13
|
+
Terms
|
|
14
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
15
|
+
|
|
16
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you the rights provided under the Change License, and this Business Source License will terminate as to that version.
|
|
17
|
+
|
|
18
|
+
If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work.
|
|
19
|
+
|
|
20
|
+
All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work.
|
|
21
|
+
|
|
22
|
+
Any copy of the Licensed Work must prominently display this License with the Notice. Any distribution of the Licensed Work must include this License with the Notice.
|
|
23
|
+
|
|
24
|
+
You must not remove or alter any copyright, trademark, or patent notice, any license or the Notice.
|
|
25
|
+
|
|
26
|
+
Notice
|
|
27
|
+
Any use of the Licensed Work is subject to the Business Source License 1.1.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# mem0-mcp
|
|
2
|
+
|
|
3
|
+
A dedicated repository for the `mem0` MCP server we developed: file-backed scoped memory records, mandatory checkpoint provenance, and Ollama-based embeddings.
|
|
4
|
+
|
|
5
|
+
Release notes and migration details live in [CHANGELOG.md](CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g mem0-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Release Highlights (0.2.0)
|
|
14
|
+
|
|
15
|
+
- Deterministic memory deduplication through a stable `dedupe_key` instead of similarity-based soft merging.
|
|
16
|
+
- Config-aware `setup_wizard` that validates and pulls the configured embedding model instead of assuming one hard-coded model.
|
|
17
|
+
- Abortable Ollama requests with `MEM0_OLLAMA_TIMEOUT_MS` across health checks and embedding calls.
|
|
18
|
+
- MCP `initialize` now reports the real package version from `package.json`.
|
|
19
|
+
- Package metadata is ready for installable distribution, including repository, homepage, and issue tracker links.
|
|
20
|
+
|
|
21
|
+
## Architecture boundary
|
|
22
|
+
|
|
23
|
+
This repository contains only the `mem0` server/runtime surface:
|
|
24
|
+
|
|
25
|
+
- `src/transport/mcp-server.ts`
|
|
26
|
+
- `src/adapters/sqlite/sqlite-memory-store.ts`
|
|
27
|
+
- `src/adapters/ollama/ollama-embedder.ts`
|
|
28
|
+
- `src/bin/mem0-mcp.ts`
|
|
29
|
+
|
|
30
|
+
Generic MCP transport mechanics are implemented inside this repository under `src/transport/`, so `mem0-mcp` can build and install independently without requiring sibling repositories.
|
|
31
|
+
|
|
32
|
+
## Environment
|
|
33
|
+
|
|
34
|
+
- `MEM0_STORE_PATH` (default: `~/.copilot/mem0`)
|
|
35
|
+
- `OLLAMA_BASE_URL` (default: `http://127.0.0.1:11434`)
|
|
36
|
+
- `MEM0_EMBED_MODEL` (default: `qwen3-embedding:latest`)
|
|
37
|
+
- `MEM0_OLLAMA_TIMEOUT_MS` (default: `10000`)
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install
|
|
43
|
+
npm run typecheck
|
|
44
|
+
npm run build
|
|
45
|
+
npm test
|
|
46
|
+
npm run mem0:mcp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Host Launch Guidance
|
|
50
|
+
|
|
51
|
+
If an MCP host isolates native modules poorly, prefer launching the server as `node <absolute-path>/dist/bin/mem0-mcp.js` instead of a symlinked shim. This avoids native module resolution failures around `better-sqlite3` in some host runtimes.
|
|
52
|
+
|
|
53
|
+
## Tool surface
|
|
54
|
+
|
|
55
|
+
- `health`
|
|
56
|
+
- `memory_store`
|
|
57
|
+
- `memory_recall`
|
|
58
|
+
- `memory_search`
|
|
59
|
+
- `memory_update`
|
|
60
|
+
- `memory_forget`
|
|
61
|
+
- `setup_wizard`
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
This project is licensed under the **Business Source License 1.1 (BSL)**.
|
|
66
|
+
|
|
67
|
+
**You may use this software for non-commercial and non-production purposes (e.g., development, testing, research, and personal projects) free of charge.**
|
|
68
|
+
|
|
69
|
+
**Commercial and production use is strictly prohibited without prior written authorization.**
|
|
70
|
+
|
|
71
|
+
On March 22, 2030 (the Change Date), this license automatically converts to the **Apache License, Version 2.0**.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: Ollama Embedder — HTTP client for the Ollama embedding API.
|
|
3
|
+
* Implements EmbedderPort.
|
|
4
|
+
*/
|
|
5
|
+
import type { Mem0Config } from '../../domain/memory.types.js';
|
|
6
|
+
import type { EmbedderPort, EmbedderHealthStatus } from '../../ports/embedder.port.js';
|
|
7
|
+
export declare class OllamaEmbedder implements EmbedderPort {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly baseUrl;
|
|
10
|
+
constructor(config: Pick<Mem0Config, 'ollamaBaseUrl' | 'embedModel' | 'ollamaTimeoutMs'>);
|
|
11
|
+
healthCheck(): Promise<EmbedderHealthStatus>;
|
|
12
|
+
embedText(text: string): Promise<number[]>;
|
|
13
|
+
private tryModernEndpoint;
|
|
14
|
+
private tryLegacyEndpoint;
|
|
15
|
+
private fetchWithTimeout;
|
|
16
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: Ollama Embedder — HTTP client for the Ollama embedding API.
|
|
3
|
+
* Implements EmbedderPort.
|
|
4
|
+
*/
|
|
5
|
+
import { getErrorMessage } from '../../domain/errors.js';
|
|
6
|
+
export class OllamaEmbedder {
|
|
7
|
+
config;
|
|
8
|
+
baseUrl;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.baseUrl = config.ollamaBaseUrl.replace(/\/+$/, '');
|
|
12
|
+
}
|
|
13
|
+
async healthCheck() {
|
|
14
|
+
try {
|
|
15
|
+
const response = await this.fetchWithTimeout('/api/tags');
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
modelAvailable: false,
|
|
20
|
+
details: `Ollama /api/tags returned ${response.status}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const payload = (await response.json());
|
|
24
|
+
const modelAvailable = (payload.models ?? []).some((model) => model.name === this.config.embedModel ||
|
|
25
|
+
model.model === this.config.embedModel);
|
|
26
|
+
return {
|
|
27
|
+
ok: modelAvailable,
|
|
28
|
+
modelAvailable,
|
|
29
|
+
details: modelAvailable
|
|
30
|
+
? undefined
|
|
31
|
+
: `Embedding model ${this.config.embedModel} is not available in Ollama`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
modelAvailable: false,
|
|
38
|
+
details: getErrorMessage(error),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async embedText(text) {
|
|
43
|
+
const modern = await this.tryModernEndpoint(text);
|
|
44
|
+
if (modern !== null)
|
|
45
|
+
return modern;
|
|
46
|
+
return this.tryLegacyEndpoint(text);
|
|
47
|
+
}
|
|
48
|
+
async tryModernEndpoint(text) {
|
|
49
|
+
const response = await this.fetchWithTimeout('/api/embed', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'content-type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ model: this.config.embedModel, input: text }),
|
|
53
|
+
});
|
|
54
|
+
if (response.status === 404 || response.status === 405) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const body = await safeReadText(response);
|
|
59
|
+
throw new Error(`Ollama modern embedding request failed (${response.status}): ${body || response.statusText}`);
|
|
60
|
+
}
|
|
61
|
+
const payload = (await response.json());
|
|
62
|
+
if (!Array.isArray(payload.embeddings) ||
|
|
63
|
+
payload.embeddings.length === 0 ||
|
|
64
|
+
!Array.isArray(payload.embeddings[0])) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return ensureNumericVector(payload.embeddings[0]);
|
|
68
|
+
}
|
|
69
|
+
async tryLegacyEndpoint(text) {
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await this.fetchWithTimeout('/api/embeddings', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'content-type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({ model: this.config.embedModel, prompt: text }),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new Error(`Unable to reach Ollama at ${this.baseUrl}: ${getErrorMessage(error)}`);
|
|
80
|
+
}
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const body = await safeReadText(response);
|
|
83
|
+
throw new Error(`Ollama embedding request failed (${response.status}): ${body || response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
const payload = (await response.json());
|
|
86
|
+
if (!Array.isArray(payload.embedding)) {
|
|
87
|
+
throw new Error('Ollama /api/embeddings returned no embedding vector');
|
|
88
|
+
}
|
|
89
|
+
return ensureNumericVector(payload.embedding);
|
|
90
|
+
}
|
|
91
|
+
async fetchWithTimeout(path, init) {
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timeout = setTimeout(() => {
|
|
94
|
+
controller.abort();
|
|
95
|
+
}, this.config.ollamaTimeoutMs);
|
|
96
|
+
try {
|
|
97
|
+
return await fetch(`${this.baseUrl}${path}`, {
|
|
98
|
+
...init,
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
104
|
+
throw new Error(`Timed out after ${this.config.ollamaTimeoutMs}ms contacting Ollama at ${this.baseUrl}`);
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function ensureNumericVector(values) {
|
|
114
|
+
const vector = values.map((value) => {
|
|
115
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
116
|
+
throw new Error('Embedding vector contains a non-numeric value');
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
});
|
|
120
|
+
if (vector.length === 0) {
|
|
121
|
+
throw new Error('Embedding vector is empty');
|
|
122
|
+
}
|
|
123
|
+
return vector;
|
|
124
|
+
}
|
|
125
|
+
async function safeReadText(response) {
|
|
126
|
+
try {
|
|
127
|
+
return await response.text();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: SQLite Memory Store — better-sqlite3 implementation of MemoryStorePort.
|
|
3
|
+
*/
|
|
4
|
+
import { type HealthCheckResult, type Mem0Config, type MemoryForgetInput, type MemoryRecallInput, type MemorySearchInput, type MemorySearchResult, type MemoryStoreInput, type MemoryUpdateInput, type PublicMemoryRecord } from '../../domain/memory.types.js';
|
|
5
|
+
import type { EmbedderPort } from '../../ports/embedder.port.js';
|
|
6
|
+
import type { MemoryStorePort } from '../../ports/memory-store.port.js';
|
|
7
|
+
export declare class SqliteMem0Adapter implements MemoryStorePort {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly db;
|
|
10
|
+
private readonly embedder;
|
|
11
|
+
constructor(config: Mem0Config, embedder?: EmbedderPort);
|
|
12
|
+
static fromEnv(env?: NodeJS.ProcessEnv): SqliteMem0Adapter;
|
|
13
|
+
healthCheck(): Promise<HealthCheckResult>;
|
|
14
|
+
storeMemory(input: MemoryStoreInput): Promise<PublicMemoryRecord>;
|
|
15
|
+
recallMemory(input: MemoryRecallInput): Promise<PublicMemoryRecord | null>;
|
|
16
|
+
searchMemory(input: MemorySearchInput): Promise<MemorySearchResult[]>;
|
|
17
|
+
updateMemory(input: MemoryUpdateInput): Promise<PublicMemoryRecord>;
|
|
18
|
+
deleteMemory(input: MemoryForgetInput): Promise<void>;
|
|
19
|
+
listWorkspaces(): Promise<string[]>;
|
|
20
|
+
listProjects(workspace: string): Promise<string[]>;
|
|
21
|
+
private initDb;
|
|
22
|
+
private getAllRecords;
|
|
23
|
+
private getRecordsByScope;
|
|
24
|
+
private ensureLegacyColumns;
|
|
25
|
+
private backfillMissingDedupeKeys;
|
|
26
|
+
private mapRowToRecord;
|
|
27
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: SQLite Memory Store — better-sqlite3 implementation of MemoryStorePort.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { mkdirSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { loadMem0ConfigFromEnv, toPublicMemoryRecord, } from '../../domain/memory.types.js';
|
|
9
|
+
import { getErrorMessage, MemoryNotFoundError } from '../../domain/errors.js';
|
|
10
|
+
import { matchesScope, cosineSimilarity, buildEmbeddingSource, buildMemoryDedupeKey, } from '../../domain/memory.utils.js';
|
|
11
|
+
import { OllamaEmbedder } from '../ollama/ollama-embedder.js';
|
|
12
|
+
export class SqliteMem0Adapter {
|
|
13
|
+
config;
|
|
14
|
+
db;
|
|
15
|
+
embedder;
|
|
16
|
+
constructor(config, embedder) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
const dbPath = join(config.storePath, 'memories.sqlite');
|
|
19
|
+
mkdirSync(config.storePath, { recursive: true });
|
|
20
|
+
this.db = new Database(dbPath);
|
|
21
|
+
this.embedder = embedder ?? new OllamaEmbedder(config);
|
|
22
|
+
this.initDb();
|
|
23
|
+
}
|
|
24
|
+
static fromEnv(env = process.env) {
|
|
25
|
+
return new SqliteMem0Adapter(loadMem0ConfigFromEnv(env));
|
|
26
|
+
}
|
|
27
|
+
// ─── MemoryStorePort ──────────────────────────────────────────────
|
|
28
|
+
async healthCheck() {
|
|
29
|
+
try {
|
|
30
|
+
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM memories');
|
|
31
|
+
const { count } = stmt.get();
|
|
32
|
+
const ollamaHealth = await this.embedder.healthCheck();
|
|
33
|
+
return {
|
|
34
|
+
ok: ollamaHealth.ok,
|
|
35
|
+
storePath: this.config.storePath,
|
|
36
|
+
ollamaBaseUrl: this.config.ollamaBaseUrl,
|
|
37
|
+
embedModel: this.config.embedModel,
|
|
38
|
+
modelAvailable: ollamaHealth.modelAvailable,
|
|
39
|
+
recordCount: count,
|
|
40
|
+
details: ollamaHealth.details,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
storePath: this.config.storePath,
|
|
47
|
+
ollamaBaseUrl: this.config.ollamaBaseUrl,
|
|
48
|
+
embedModel: this.config.embedModel,
|
|
49
|
+
modelAvailable: false,
|
|
50
|
+
recordCount: 0,
|
|
51
|
+
details: getErrorMessage(error),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async storeMemory(input) {
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
const dedupeKey = buildMemoryDedupeKey(input.kind, input.content, input.scope, input.metadata);
|
|
58
|
+
const duplicateRow = this.db
|
|
59
|
+
.prepare('SELECT * FROM memories WHERE dedupe_key = ? LIMIT 1')
|
|
60
|
+
.get(dedupeKey);
|
|
61
|
+
if (duplicateRow) {
|
|
62
|
+
const duplicate = this.mapRowToRecord(duplicateRow);
|
|
63
|
+
this.db
|
|
64
|
+
.prepare('UPDATE memories SET provenance = ?, updated_at = ? WHERE id = ?')
|
|
65
|
+
.run(JSON.stringify(input.provenance), now, duplicate.id);
|
|
66
|
+
duplicate.provenance = input.provenance;
|
|
67
|
+
duplicate.updatedAt = now;
|
|
68
|
+
return toPublicMemoryRecord(duplicate);
|
|
69
|
+
}
|
|
70
|
+
const sourceText = buildEmbeddingSource(input.kind, input.content, input.scope, input.provenance, input.metadata);
|
|
71
|
+
const embedding = await this.embedder.embedText(sourceText);
|
|
72
|
+
const id = randomUUID();
|
|
73
|
+
this.db.prepare(`
|
|
74
|
+
INSERT INTO memories (id, kind, content, scope, provenance, metadata, embedding, dedupe_key, created_at, updated_at)
|
|
75
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
76
|
+
`).run(id, input.kind, input.content, JSON.stringify(input.scope), JSON.stringify(input.provenance), JSON.stringify(input.metadata), JSON.stringify(embedding), dedupeKey, now, now);
|
|
77
|
+
return {
|
|
78
|
+
id, kind: input.kind, content: input.content,
|
|
79
|
+
scope: input.scope, provenance: input.provenance,
|
|
80
|
+
metadata: input.metadata, createdAt: now, updatedAt: now,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async recallMemory(input) {
|
|
84
|
+
const row = this.db.prepare('SELECT * FROM memories WHERE id = ?').get(input.memoryId);
|
|
85
|
+
if (!row)
|
|
86
|
+
return null;
|
|
87
|
+
const record = this.mapRowToRecord(row);
|
|
88
|
+
if (!matchesScope(record.scope, input.scope))
|
|
89
|
+
return null;
|
|
90
|
+
return toPublicMemoryRecord(record);
|
|
91
|
+
}
|
|
92
|
+
async searchMemory(input) {
|
|
93
|
+
let records = this.getRecordsByScope(input.scope);
|
|
94
|
+
if (input.kind !== undefined) {
|
|
95
|
+
records = records.filter((r) => r.kind === input.kind);
|
|
96
|
+
}
|
|
97
|
+
if (records.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const queries = Array.isArray(input.query) ? input.query : [input.query];
|
|
100
|
+
const queryEmbeddings = await Promise.all(queries.map((q) => this.embedder.embedText(q)));
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const record of records) {
|
|
103
|
+
let maxScore = -1;
|
|
104
|
+
for (const qEmb of queryEmbeddings) {
|
|
105
|
+
const score = cosineSimilarity(qEmb, record.embedding);
|
|
106
|
+
if (score > maxScore)
|
|
107
|
+
maxScore = score;
|
|
108
|
+
}
|
|
109
|
+
results.push({ memory: toPublicMemoryRecord(record), score: maxScore });
|
|
110
|
+
}
|
|
111
|
+
return results.sort((a, b) => b.score - a.score).slice(0, input.limit);
|
|
112
|
+
}
|
|
113
|
+
async updateMemory(input) {
|
|
114
|
+
const existing = await this.recallMemory({ memoryId: input.memoryId, scope: input.scope });
|
|
115
|
+
if (!existing)
|
|
116
|
+
throw new MemoryNotFoundError(input.memoryId);
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
const newContent = input.content ?? existing.content;
|
|
119
|
+
const newMetadata = input.metadata ?? existing.metadata;
|
|
120
|
+
let newEmbeddingStr;
|
|
121
|
+
if (input.content !== undefined || input.metadata !== undefined) {
|
|
122
|
+
const sourceText = buildEmbeddingSource(existing.kind, newContent, existing.scope, existing.provenance, newMetadata);
|
|
123
|
+
const newEmbedding = await this.embedder.embedText(sourceText);
|
|
124
|
+
newEmbeddingStr = JSON.stringify(newEmbedding);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const row = this.db.prepare('SELECT embedding FROM memories WHERE id = ?').get(input.memoryId);
|
|
128
|
+
newEmbeddingStr = row.embedding;
|
|
129
|
+
}
|
|
130
|
+
this.db.prepare('UPDATE memories SET content = ?, metadata = ?, embedding = ?, dedupe_key = ?, updated_at = ? WHERE id = ?').run(newContent, JSON.stringify(newMetadata), newEmbeddingStr, buildMemoryDedupeKey(existing.kind, newContent, existing.scope, newMetadata), now, input.memoryId);
|
|
131
|
+
return { ...existing, content: newContent, metadata: newMetadata, updatedAt: now };
|
|
132
|
+
}
|
|
133
|
+
async deleteMemory(input) {
|
|
134
|
+
const existing = await this.recallMemory({ memoryId: input.memoryId, scope: input.scope });
|
|
135
|
+
if (!existing)
|
|
136
|
+
throw new MemoryNotFoundError(input.memoryId);
|
|
137
|
+
this.db.prepare('DELETE FROM memories WHERE id = ?').run(input.memoryId);
|
|
138
|
+
}
|
|
139
|
+
async listWorkspaces() {
|
|
140
|
+
const records = this.getAllRecords();
|
|
141
|
+
const workspaces = new Set();
|
|
142
|
+
for (const r of records)
|
|
143
|
+
workspaces.add(r.scope.workspace);
|
|
144
|
+
return Array.from(workspaces);
|
|
145
|
+
}
|
|
146
|
+
async listProjects(workspace) {
|
|
147
|
+
const records = this.getAllRecords();
|
|
148
|
+
const projects = new Set();
|
|
149
|
+
for (const r of records) {
|
|
150
|
+
if (r.scope.workspace === workspace)
|
|
151
|
+
projects.add(r.scope.project);
|
|
152
|
+
}
|
|
153
|
+
return Array.from(projects);
|
|
154
|
+
}
|
|
155
|
+
// ─── Private ──────────────────────────────────────────────────────
|
|
156
|
+
initDb() {
|
|
157
|
+
this.db.exec(`
|
|
158
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
159
|
+
id TEXT PRIMARY KEY,
|
|
160
|
+
kind TEXT NOT NULL,
|
|
161
|
+
content TEXT NOT NULL,
|
|
162
|
+
scope TEXT NOT NULL,
|
|
163
|
+
provenance TEXT NOT NULL,
|
|
164
|
+
metadata TEXT NOT NULL,
|
|
165
|
+
embedding TEXT NOT NULL,
|
|
166
|
+
dedupe_key TEXT NOT NULL DEFAULT '',
|
|
167
|
+
created_at TEXT NOT NULL,
|
|
168
|
+
updated_at TEXT NOT NULL
|
|
169
|
+
)
|
|
170
|
+
`);
|
|
171
|
+
this.ensureLegacyColumns();
|
|
172
|
+
this.db.exec(`
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_memories_dedupe_key
|
|
174
|
+
ON memories (dedupe_key)
|
|
175
|
+
`);
|
|
176
|
+
this.backfillMissingDedupeKeys();
|
|
177
|
+
}
|
|
178
|
+
getAllRecords() {
|
|
179
|
+
const rows = this.db.prepare('SELECT * FROM memories').all();
|
|
180
|
+
return rows.map(this.mapRowToRecord);
|
|
181
|
+
}
|
|
182
|
+
getRecordsByScope(scope) {
|
|
183
|
+
return this.getAllRecords().filter((r) => matchesScope(r.scope, scope));
|
|
184
|
+
}
|
|
185
|
+
ensureLegacyColumns() {
|
|
186
|
+
const columns = this.db
|
|
187
|
+
.prepare('PRAGMA table_info(memories)')
|
|
188
|
+
.all();
|
|
189
|
+
if (!columns.some((column) => column.name === 'dedupe_key')) {
|
|
190
|
+
this.db.exec(`ALTER TABLE memories ADD COLUMN dedupe_key TEXT NOT NULL DEFAULT ''`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
backfillMissingDedupeKeys() {
|
|
194
|
+
const rows = this.db
|
|
195
|
+
.prepare(`SELECT id, kind, content, scope, metadata
|
|
196
|
+
FROM memories
|
|
197
|
+
WHERE dedupe_key = '' OR dedupe_key IS NULL`)
|
|
198
|
+
.all();
|
|
199
|
+
const updateStatement = this.db.prepare('UPDATE memories SET dedupe_key = ? WHERE id = ?');
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
updateStatement.run(buildMemoryDedupeKey(row.kind, row.content, JSON.parse(row.scope), JSON.parse(row.metadata)), row.id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
mapRowToRecord(row) {
|
|
205
|
+
return {
|
|
206
|
+
id: row.id,
|
|
207
|
+
kind: row.kind,
|
|
208
|
+
content: row.content,
|
|
209
|
+
scope: JSON.parse(row.scope),
|
|
210
|
+
provenance: JSON.parse(row.provenance),
|
|
211
|
+
metadata: JSON.parse(row.metadata),
|
|
212
|
+
embedding: JSON.parse(row.embedding),
|
|
213
|
+
createdAt: row.created_at,
|
|
214
|
+
updatedAt: row.updated_at,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Composition Root — wires domain, ports, adapters, and transport.
|
|
4
|
+
*/
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { loadMem0ConfigFromEnv } from '../domain/memory.types.js';
|
|
7
|
+
import { OllamaEmbedder } from '../adapters/ollama/ollama-embedder.js';
|
|
8
|
+
import { SqliteMem0Adapter } from '../adapters/sqlite/sqlite-memory-store.js';
|
|
9
|
+
import { McpServer } from '../transport/mcp-server.js';
|
|
10
|
+
// Tools
|
|
11
|
+
import { createHealthTool } from '../transport/tools/health.tool.js';
|
|
12
|
+
import { createMemoryStoreTool } from '../transport/tools/memory-store.tool.js';
|
|
13
|
+
import { createMemoryRecallTool } from '../transport/tools/memory-recall.tool.js';
|
|
14
|
+
import { createMemorySearchTool } from '../transport/tools/memory-search.tool.js';
|
|
15
|
+
import { createMemoryUpdateTool } from '../transport/tools/memory-update.tool.js';
|
|
16
|
+
import { createMemoryForgetTool } from '../transport/tools/memory-forget.tool.js';
|
|
17
|
+
import { createSetupWizardTool } from '../transport/tools/setup-wizard.tool.js';
|
|
18
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
19
|
+
async function main() {
|
|
20
|
+
// 1. Config
|
|
21
|
+
const config = loadMem0ConfigFromEnv();
|
|
22
|
+
// 2. Adapters (infrastructure)
|
|
23
|
+
const embedder = new OllamaEmbedder(config);
|
|
24
|
+
const store = new SqliteMem0Adapter(config, embedder);
|
|
25
|
+
// 3. Transport (MCP server)
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
serverInfo: {
|
|
28
|
+
name: 'mem0-mcp',
|
|
29
|
+
version: resolvePackageVersion(),
|
|
30
|
+
},
|
|
31
|
+
instructions: 'SQLite remains canonical. memory_store requires canonical scope metadata and checkpoint provenance before a memory can be persisted.',
|
|
32
|
+
});
|
|
33
|
+
// 4. Register tools
|
|
34
|
+
server.registerTool(createHealthTool(store));
|
|
35
|
+
server.registerTool(createMemoryStoreTool(store));
|
|
36
|
+
server.registerTool(createMemoryRecallTool(store));
|
|
37
|
+
server.registerTool(createMemorySearchTool(store));
|
|
38
|
+
server.registerTool(createMemoryUpdateTool(store));
|
|
39
|
+
server.registerTool(createMemoryForgetTool(store));
|
|
40
|
+
server.registerTool(createSetupWizardTool({
|
|
41
|
+
embedModel: config.embedModel,
|
|
42
|
+
}));
|
|
43
|
+
// 5. Register resource handler
|
|
44
|
+
server.registerResourceListHandler(async () => {
|
|
45
|
+
const workspaces = await store.listWorkspaces();
|
|
46
|
+
const resources = [];
|
|
47
|
+
for (const workspace of workspaces) {
|
|
48
|
+
const projects = await store.listProjects(workspace);
|
|
49
|
+
for (const project of projects) {
|
|
50
|
+
resources.push({
|
|
51
|
+
uri: `mem0://${workspace}/${project}`,
|
|
52
|
+
name: `Memory Scope: ${workspace}/${project}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { resources };
|
|
57
|
+
});
|
|
58
|
+
// 6. Start
|
|
59
|
+
server.start();
|
|
60
|
+
}
|
|
61
|
+
main().catch((error) => {
|
|
62
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
function resolvePackageVersion() {
|
|
66
|
+
try {
|
|
67
|
+
const packageJson = requireFromHere('../../package.json');
|
|
68
|
+
if (typeof packageJson.version === 'string' && packageJson.version.length > 0) {
|
|
69
|
+
return packageJson.version;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Fall back below when package.json is unavailable in constrained runtimes.
|
|
74
|
+
}
|
|
75
|
+
return '0.2.0';
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical error utilities for the mem0-mcp domain.
|
|
3
|
+
*/
|
|
4
|
+
export function getErrorMessage(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
export class MemoryNotFoundError extends Error {
|
|
8
|
+
constructor(memoryId) {
|
|
9
|
+
super(`Memory ${memoryId} not found in given scope.`);
|
|
10
|
+
this.name = 'MemoryNotFoundError';
|
|
11
|
+
}
|
|
12
|
+
}
|