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,110 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { SqliteMem0Adapter } from '../index.js';
|
|
7
|
+
const stubEmbedder = {
|
|
8
|
+
async embedText() {
|
|
9
|
+
return [1, 0, 0];
|
|
10
|
+
},
|
|
11
|
+
async healthCheck() {
|
|
12
|
+
return {
|
|
13
|
+
ok: true,
|
|
14
|
+
modelAvailable: true,
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
test('storeMemory dedupes exact duplicates while keeping latest provenance', async () => {
|
|
19
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'mem0-sqlite-dedupe-'));
|
|
20
|
+
const adapter = new SqliteMem0Adapter({
|
|
21
|
+
storePath: tempDir,
|
|
22
|
+
ollamaBaseUrl: 'http://127.0.0.1:11434',
|
|
23
|
+
embedModel: 'stub',
|
|
24
|
+
ollamaTimeoutMs: 1_000,
|
|
25
|
+
}, stubEmbedder);
|
|
26
|
+
try {
|
|
27
|
+
const scope = { workspace: 'workspace-1', project: 'project-1', task: 'task-1' };
|
|
28
|
+
const metadata = { source: 'agentic-loop' };
|
|
29
|
+
const first = (await adapter.storeMemory({
|
|
30
|
+
kind: 'note',
|
|
31
|
+
content: 'Remember this exact note.',
|
|
32
|
+
scope,
|
|
33
|
+
provenance: {
|
|
34
|
+
checkpointId: 'checkpoint-1',
|
|
35
|
+
artifactIds: [],
|
|
36
|
+
},
|
|
37
|
+
metadata,
|
|
38
|
+
}));
|
|
39
|
+
const second = (await adapter.storeMemory({
|
|
40
|
+
kind: 'note',
|
|
41
|
+
content: 'Remember this exact note.',
|
|
42
|
+
scope,
|
|
43
|
+
provenance: {
|
|
44
|
+
checkpointId: 'checkpoint-2',
|
|
45
|
+
artifactIds: ['artifact-2'],
|
|
46
|
+
},
|
|
47
|
+
metadata,
|
|
48
|
+
}));
|
|
49
|
+
assert.equal(second.id, first.id);
|
|
50
|
+
assert.equal(second.provenance.checkpointId, 'checkpoint-2');
|
|
51
|
+
assert.deepEqual(second.provenance.artifactIds, ['artifact-2']);
|
|
52
|
+
const recalled = (await adapter.recallMemory({
|
|
53
|
+
memoryId: first.id,
|
|
54
|
+
scope,
|
|
55
|
+
}));
|
|
56
|
+
assert.equal(recalled?.content, 'Remember this exact note.');
|
|
57
|
+
assert.equal(recalled?.provenance.checkpointId, 'checkpoint-2');
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
test('storeMemory does not collapse different memories that share the same embedding vector', async () => {
|
|
64
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'mem0-sqlite-distinct-'));
|
|
65
|
+
const adapter = new SqliteMem0Adapter({
|
|
66
|
+
storePath: tempDir,
|
|
67
|
+
ollamaBaseUrl: 'http://127.0.0.1:11434',
|
|
68
|
+
embedModel: 'stub',
|
|
69
|
+
ollamaTimeoutMs: 1_000,
|
|
70
|
+
}, stubEmbedder);
|
|
71
|
+
try {
|
|
72
|
+
const scope = { workspace: 'workspace-1', project: 'project-1', task: 'task-1' };
|
|
73
|
+
const first = (await adapter.storeMemory({
|
|
74
|
+
kind: 'note',
|
|
75
|
+
content: 'First memory content',
|
|
76
|
+
scope,
|
|
77
|
+
provenance: {
|
|
78
|
+
checkpointId: 'checkpoint-1',
|
|
79
|
+
artifactIds: [],
|
|
80
|
+
},
|
|
81
|
+
metadata: { phase: 'one' },
|
|
82
|
+
}));
|
|
83
|
+
const second = (await adapter.storeMemory({
|
|
84
|
+
kind: 'note',
|
|
85
|
+
content: 'Second memory content',
|
|
86
|
+
scope,
|
|
87
|
+
provenance: {
|
|
88
|
+
checkpointId: 'checkpoint-2',
|
|
89
|
+
artifactIds: [],
|
|
90
|
+
},
|
|
91
|
+
metadata: { phase: 'two' },
|
|
92
|
+
}));
|
|
93
|
+
assert.notEqual(second.id, first.id);
|
|
94
|
+
assert.equal(second.content, 'Second memory content');
|
|
95
|
+
assert.equal(second.metadata.phase, 'two');
|
|
96
|
+
const recalledFirst = (await adapter.recallMemory({
|
|
97
|
+
memoryId: first.id,
|
|
98
|
+
scope,
|
|
99
|
+
}));
|
|
100
|
+
const recalledSecond = (await adapter.recallMemory({
|
|
101
|
+
memoryId: second.id,
|
|
102
|
+
scope,
|
|
103
|
+
}));
|
|
104
|
+
assert.equal(recalledFirst?.content, 'First memory content');
|
|
105
|
+
assert.equal(recalledSecond?.content, 'Second memory content');
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON-RPC 2.0 over stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained — no external dependencies.
|
|
5
|
+
* Supports both Content-Length framed and bare JSON-line (jsonl) formats,
|
|
6
|
+
* auto-detecting the peer's output style and mirroring it.
|
|
7
|
+
*/
|
|
8
|
+
export type JsonRpcId = number | string | null;
|
|
9
|
+
export interface JsonRpcMessage {
|
|
10
|
+
jsonrpc: '2.0';
|
|
11
|
+
id?: JsonRpcId;
|
|
12
|
+
method: string;
|
|
13
|
+
params?: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface JsonRpcErrorPayload {
|
|
16
|
+
code: number;
|
|
17
|
+
message: string;
|
|
18
|
+
data?: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface JsonRpcSuccessResponse {
|
|
21
|
+
jsonrpc: '2.0';
|
|
22
|
+
id: JsonRpcId;
|
|
23
|
+
result: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface JsonRpcErrorResponse {
|
|
26
|
+
jsonrpc: '2.0';
|
|
27
|
+
id: JsonRpcId;
|
|
28
|
+
error: JsonRpcErrorPayload;
|
|
29
|
+
}
|
|
30
|
+
export type JsonRpcEnvelope = JsonRpcMessage | JsonRpcSuccessResponse | JsonRpcErrorResponse;
|
|
31
|
+
export declare function isJsonRpcSuccessResponse(message: JsonRpcEnvelope): message is JsonRpcSuccessResponse;
|
|
32
|
+
export declare function isJsonRpcErrorResponse(message: JsonRpcEnvelope): message is JsonRpcErrorResponse;
|
|
33
|
+
export type JsonRpcOutputMode = 'framed' | 'jsonl';
|
|
34
|
+
export declare class JsonRpcError extends Error {
|
|
35
|
+
readonly code: number;
|
|
36
|
+
readonly data?: unknown | undefined;
|
|
37
|
+
constructor(code: number, message: string, data?: unknown | undefined);
|
|
38
|
+
}
|
|
39
|
+
interface JsonRpcStreamEndpoints {
|
|
40
|
+
input: NodeJS.ReadableStream;
|
|
41
|
+
output: NodeJS.WritableStream;
|
|
42
|
+
}
|
|
43
|
+
export declare class JsonRpcStreamTransport {
|
|
44
|
+
private readonly onMessage;
|
|
45
|
+
private readonly endpoints;
|
|
46
|
+
private buffer;
|
|
47
|
+
private chain;
|
|
48
|
+
private listening;
|
|
49
|
+
private outputMode;
|
|
50
|
+
constructor(onMessage: (message: JsonRpcEnvelope) => Promise<void>, endpoints: JsonRpcStreamEndpoints, options?: {
|
|
51
|
+
initialOutputMode?: JsonRpcOutputMode;
|
|
52
|
+
});
|
|
53
|
+
start(): void;
|
|
54
|
+
stop(): void;
|
|
55
|
+
sendRequest(id: JsonRpcId, method: string, params?: unknown): void;
|
|
56
|
+
sendNotification(method: string, params?: unknown): void;
|
|
57
|
+
sendResult(id: JsonRpcId, result: unknown): void;
|
|
58
|
+
sendError(id: JsonRpcId, error: JsonRpcErrorPayload): void;
|
|
59
|
+
private readonly handleChunk;
|
|
60
|
+
private drainBuffer;
|
|
61
|
+
private writeEnvelope;
|
|
62
|
+
}
|
|
63
|
+
export declare class StdioJsonRpcTransport {
|
|
64
|
+
private readonly onMessageHandler?;
|
|
65
|
+
private readonly transport;
|
|
66
|
+
constructor(onMessageHandler?: ((message: JsonRpcMessage) => Promise<void>) | undefined);
|
|
67
|
+
start(): void;
|
|
68
|
+
stop(): void;
|
|
69
|
+
sendNotification(method: string, params?: unknown): void;
|
|
70
|
+
sendResult(id: JsonRpcId, result: unknown): void;
|
|
71
|
+
sendError(id: JsonRpcId, error: JsonRpcErrorPayload): void;
|
|
72
|
+
}
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON-RPC 2.0 over stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained — no external dependencies.
|
|
5
|
+
* Supports both Content-Length framed and bare JSON-line (jsonl) formats,
|
|
6
|
+
* auto-detecting the peer's output style and mirroring it.
|
|
7
|
+
*/
|
|
8
|
+
export function isJsonRpcSuccessResponse(message) {
|
|
9
|
+
return 'result' in message;
|
|
10
|
+
}
|
|
11
|
+
export function isJsonRpcErrorResponse(message) {
|
|
12
|
+
return 'error' in message;
|
|
13
|
+
}
|
|
14
|
+
// ─── Error ──────────────────────────────────────────────────────────
|
|
15
|
+
export class JsonRpcError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
data;
|
|
18
|
+
constructor(code, message, data) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.data = data;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class JsonRpcStreamTransport {
|
|
25
|
+
onMessage;
|
|
26
|
+
endpoints;
|
|
27
|
+
buffer = Buffer.alloc(0);
|
|
28
|
+
chain = Promise.resolve();
|
|
29
|
+
listening = false;
|
|
30
|
+
outputMode;
|
|
31
|
+
constructor(onMessage, endpoints, options = {}) {
|
|
32
|
+
this.onMessage = onMessage;
|
|
33
|
+
this.endpoints = endpoints;
|
|
34
|
+
this.outputMode = options.initialOutputMode ?? 'jsonl';
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
if (this.listening)
|
|
38
|
+
return;
|
|
39
|
+
this.endpoints.input.on('data', this.handleChunk);
|
|
40
|
+
this.listening = true;
|
|
41
|
+
const readable = this.endpoints.input;
|
|
42
|
+
readable.resume?.();
|
|
43
|
+
}
|
|
44
|
+
stop() {
|
|
45
|
+
if (!this.listening)
|
|
46
|
+
return;
|
|
47
|
+
const readable = this.endpoints.input;
|
|
48
|
+
if (typeof readable.off === 'function') {
|
|
49
|
+
readable.off('data', this.handleChunk);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
readable.removeListener?.('data', this.handleChunk);
|
|
53
|
+
}
|
|
54
|
+
this.listening = false;
|
|
55
|
+
}
|
|
56
|
+
sendRequest(id, method, params) {
|
|
57
|
+
const message = { jsonrpc: '2.0', id, method };
|
|
58
|
+
if (params !== undefined)
|
|
59
|
+
message.params = params;
|
|
60
|
+
this.writeEnvelope(message);
|
|
61
|
+
}
|
|
62
|
+
sendNotification(method, params) {
|
|
63
|
+
const message = { jsonrpc: '2.0', method };
|
|
64
|
+
if (params !== undefined)
|
|
65
|
+
message.params = params;
|
|
66
|
+
this.writeEnvelope(message);
|
|
67
|
+
}
|
|
68
|
+
sendResult(id, result) {
|
|
69
|
+
this.writeEnvelope({ jsonrpc: '2.0', id, result });
|
|
70
|
+
}
|
|
71
|
+
sendError(id, error) {
|
|
72
|
+
this.writeEnvelope({ jsonrpc: '2.0', id, error });
|
|
73
|
+
}
|
|
74
|
+
// ── Private ─────────────────────────────────────────────────────
|
|
75
|
+
handleChunk = (chunk) => {
|
|
76
|
+
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
77
|
+
this.buffer = Buffer.concat([this.buffer, chunkBuffer]);
|
|
78
|
+
this.drainBuffer();
|
|
79
|
+
};
|
|
80
|
+
drainBuffer() {
|
|
81
|
+
while (true) {
|
|
82
|
+
const framedEnvelope = extractFramedEnvelope(this.buffer);
|
|
83
|
+
let body;
|
|
84
|
+
if (framedEnvelope !== null) {
|
|
85
|
+
this.outputMode = 'framed';
|
|
86
|
+
body = framedEnvelope.body;
|
|
87
|
+
this.buffer = this.buffer.subarray(framedEnvelope.nextOffset);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const bareEnvelope = extractBareJsonEnvelope(this.buffer);
|
|
91
|
+
if (bareEnvelope === null)
|
|
92
|
+
return;
|
|
93
|
+
this.outputMode = 'jsonl';
|
|
94
|
+
body = bareEnvelope.body;
|
|
95
|
+
this.buffer = this.buffer.subarray(bareEnvelope.nextOffset);
|
|
96
|
+
}
|
|
97
|
+
let message;
|
|
98
|
+
try {
|
|
99
|
+
message = JSON.parse(body);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(`Failed to parse JSON-RPC message: ${getErrorMessage(error)}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
this.chain = this.chain
|
|
106
|
+
.then(() => this.onMessage(message))
|
|
107
|
+
.catch((error) => {
|
|
108
|
+
console.error(getErrorMessage(error));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
writeEnvelope(message) {
|
|
113
|
+
const body = JSON.stringify(message);
|
|
114
|
+
if (this.outputMode === 'jsonl') {
|
|
115
|
+
this.endpoints.output.write(`${body}\n`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const header = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n`;
|
|
119
|
+
this.endpoints.output.write(header);
|
|
120
|
+
this.endpoints.output.write(body);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ─── Stdio Server Transport ─────────────────────────────────────────
|
|
124
|
+
export class StdioJsonRpcTransport {
|
|
125
|
+
onMessageHandler;
|
|
126
|
+
transport;
|
|
127
|
+
constructor(onMessageHandler) {
|
|
128
|
+
this.onMessageHandler = onMessageHandler;
|
|
129
|
+
this.transport = new JsonRpcStreamTransport(async (message) => {
|
|
130
|
+
if (!('method' in message)) {
|
|
131
|
+
console.error(`Unexpected JSON-RPC response envelope on stdio server input: ${JSON.stringify(message)}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (this.onMessageHandler) {
|
|
135
|
+
await this.onMessageHandler(message);
|
|
136
|
+
}
|
|
137
|
+
}, { input: process.stdin, output: process.stdout });
|
|
138
|
+
}
|
|
139
|
+
start() {
|
|
140
|
+
this.transport.start();
|
|
141
|
+
}
|
|
142
|
+
stop() {
|
|
143
|
+
this.transport.stop();
|
|
144
|
+
}
|
|
145
|
+
sendNotification(method, params) {
|
|
146
|
+
this.transport.sendNotification(method, params);
|
|
147
|
+
}
|
|
148
|
+
sendResult(id, result) {
|
|
149
|
+
this.transport.sendResult(id, result);
|
|
150
|
+
}
|
|
151
|
+
sendError(id, error) {
|
|
152
|
+
this.transport.sendError(id, error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
156
|
+
function findHeaderBoundary(buffer) {
|
|
157
|
+
const crlfBoundary = buffer.indexOf('\r\n\r\n');
|
|
158
|
+
if (crlfBoundary !== -1)
|
|
159
|
+
return { headerEnd: crlfBoundary, separatorLength: 4 };
|
|
160
|
+
const lfBoundary = buffer.indexOf('\n\n');
|
|
161
|
+
if (lfBoundary !== -1)
|
|
162
|
+
return { headerEnd: lfBoundary, separatorLength: 2 };
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function extractFramedEnvelope(buffer) {
|
|
166
|
+
const headerBoundary = findHeaderBoundary(buffer);
|
|
167
|
+
if (headerBoundary === null)
|
|
168
|
+
return null;
|
|
169
|
+
const { headerEnd, separatorLength } = headerBoundary;
|
|
170
|
+
const header = buffer.subarray(0, headerEnd).toString('utf8');
|
|
171
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
172
|
+
if (match === null)
|
|
173
|
+
return null;
|
|
174
|
+
const contentLength = Number.parseInt(match[1], 10);
|
|
175
|
+
const messageEnd = headerEnd + separatorLength + contentLength;
|
|
176
|
+
if (buffer.length < messageEnd)
|
|
177
|
+
return null;
|
|
178
|
+
return {
|
|
179
|
+
body: buffer.subarray(headerEnd + separatorLength, messageEnd).toString('utf8'),
|
|
180
|
+
nextOffset: messageEnd,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function extractBareJsonEnvelope(buffer) {
|
|
184
|
+
const source = buffer.toString('utf8');
|
|
185
|
+
const leadingWhitespace = source.match(/^\s*/)?.[0].length ?? 0;
|
|
186
|
+
if (leadingWhitespace >= source.length)
|
|
187
|
+
return null;
|
|
188
|
+
const firstCharacter = source[leadingWhitespace];
|
|
189
|
+
if (firstCharacter !== '{' && firstCharacter !== '[')
|
|
190
|
+
return null;
|
|
191
|
+
let depth = 0;
|
|
192
|
+
let inString = false;
|
|
193
|
+
let escaping = false;
|
|
194
|
+
for (let index = leadingWhitespace; index < source.length; index += 1) {
|
|
195
|
+
const character = source[index];
|
|
196
|
+
if (escaping) {
|
|
197
|
+
escaping = false;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (inString) {
|
|
201
|
+
if (character === '\\')
|
|
202
|
+
escaping = true;
|
|
203
|
+
else if (character === '"')
|
|
204
|
+
inString = false;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (character === '"') {
|
|
208
|
+
inString = true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (character === '{' || character === '[') {
|
|
212
|
+
depth += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (character === '}' || character === ']') {
|
|
216
|
+
depth -= 1;
|
|
217
|
+
if (depth === 0) {
|
|
218
|
+
const endIndex = index + 1;
|
|
219
|
+
return {
|
|
220
|
+
body: source.slice(leadingWhitespace, endIndex),
|
|
221
|
+
nextOffset: Buffer.byteLength(source.slice(0, endIndex), 'utf8'),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function getErrorMessage(error) {
|
|
229
|
+
return error instanceof Error ? error.message : String(error);
|
|
230
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic MCP Server — tool registry + JSON-RPC dispatch.
|
|
3
|
+
* Zero business logic: tools are registered externally.
|
|
4
|
+
*/
|
|
5
|
+
import { StdioJsonRpcTransport } from './jsonrpc-stdio.js';
|
|
6
|
+
export interface ToolDefinition {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
inputSchema: Record<string, unknown>;
|
|
10
|
+
handler: (args: unknown) => Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface McpServerOptions {
|
|
13
|
+
protocolVersion?: string;
|
|
14
|
+
serverInfo?: {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
};
|
|
18
|
+
instructions?: string;
|
|
19
|
+
transport?: StdioJsonRpcTransport;
|
|
20
|
+
}
|
|
21
|
+
export declare class McpServer {
|
|
22
|
+
private readonly transport;
|
|
23
|
+
private readonly tools;
|
|
24
|
+
private resourceListHandler?;
|
|
25
|
+
private readonly protocolVersion;
|
|
26
|
+
private readonly serverInfo;
|
|
27
|
+
private readonly instructions?;
|
|
28
|
+
constructor(options?: McpServerOptions);
|
|
29
|
+
registerTool(tool: ToolDefinition): void;
|
|
30
|
+
registerResourceListHandler(handler: () => Promise<{
|
|
31
|
+
resources: unknown[];
|
|
32
|
+
}>): void;
|
|
33
|
+
start(): void;
|
|
34
|
+
private handleMessage;
|
|
35
|
+
private buildInitializeResult;
|
|
36
|
+
private callTool;
|
|
37
|
+
private requireId;
|
|
38
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic MCP Server — tool registry + JSON-RPC dispatch.
|
|
3
|
+
* Zero business logic: tools are registered externally.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { getErrorMessage } from '../domain/errors.js';
|
|
7
|
+
import { JsonRpcError, StdioJsonRpcTransport, } from './jsonrpc-stdio.js';
|
|
8
|
+
const initializeParamsSchema = z
|
|
9
|
+
.object({ protocolVersion: z.string().optional() })
|
|
10
|
+
.passthrough();
|
|
11
|
+
const toolCallParamsSchema = z
|
|
12
|
+
.object({ name: z.string().min(1), arguments: z.unknown().optional() })
|
|
13
|
+
.passthrough();
|
|
14
|
+
export class McpServer {
|
|
15
|
+
transport;
|
|
16
|
+
tools = new Map();
|
|
17
|
+
resourceListHandler;
|
|
18
|
+
protocolVersion;
|
|
19
|
+
serverInfo;
|
|
20
|
+
instructions;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.transport =
|
|
23
|
+
options.transport ??
|
|
24
|
+
new StdioJsonRpcTransport((msg) => this.handleMessage(msg));
|
|
25
|
+
this.protocolVersion = options.protocolVersion ?? '2024-11-05';
|
|
26
|
+
this.serverInfo = options.serverInfo ?? {
|
|
27
|
+
name: 'mcp-server',
|
|
28
|
+
version: '0.0.0',
|
|
29
|
+
};
|
|
30
|
+
this.instructions = options.instructions;
|
|
31
|
+
}
|
|
32
|
+
registerTool(tool) {
|
|
33
|
+
this.tools.set(tool.name, tool);
|
|
34
|
+
}
|
|
35
|
+
registerResourceListHandler(handler) {
|
|
36
|
+
this.resourceListHandler = handler;
|
|
37
|
+
}
|
|
38
|
+
start() {
|
|
39
|
+
this.transport.start();
|
|
40
|
+
}
|
|
41
|
+
// ─── Private ──────────────────────────────────────────────────────
|
|
42
|
+
async handleMessage(message) {
|
|
43
|
+
const id = 'id' in message ? (message.id ?? null) : undefined;
|
|
44
|
+
try {
|
|
45
|
+
switch (message.method) {
|
|
46
|
+
case 'initialize':
|
|
47
|
+
this.requireId(id, message.method);
|
|
48
|
+
this.transport.sendResult(id, this.buildInitializeResult(message.params));
|
|
49
|
+
return;
|
|
50
|
+
case 'notifications/initialized':
|
|
51
|
+
case '$/cancelRequest':
|
|
52
|
+
case '$/setTrace':
|
|
53
|
+
return;
|
|
54
|
+
case 'ping':
|
|
55
|
+
if (id !== undefined)
|
|
56
|
+
this.transport.sendResult(id, {});
|
|
57
|
+
return;
|
|
58
|
+
case 'tools/list':
|
|
59
|
+
this.requireId(id, message.method);
|
|
60
|
+
this.transport.sendResult(id, {
|
|
61
|
+
tools: [...this.tools.values()].map(({ name, description, inputSchema }) => ({
|
|
62
|
+
name, description, inputSchema,
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
case 'tools/call':
|
|
67
|
+
this.requireId(id, message.method);
|
|
68
|
+
this.transport.sendResult(id, await this.callTool(message.params));
|
|
69
|
+
return;
|
|
70
|
+
case 'resources/list':
|
|
71
|
+
this.requireId(id, message.method);
|
|
72
|
+
if (this.resourceListHandler) {
|
|
73
|
+
try {
|
|
74
|
+
this.transport.sendResult(id, await this.resourceListHandler());
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
this.transport.sendResult(id, { resources: [] });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.transport.sendResult(id, { resources: [] });
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
case 'prompts/list':
|
|
85
|
+
this.requireId(id, message.method);
|
|
86
|
+
this.transport.sendResult(id, { prompts: [] });
|
|
87
|
+
return;
|
|
88
|
+
case 'shutdown':
|
|
89
|
+
this.requireId(id, message.method);
|
|
90
|
+
this.transport.sendResult(id, {});
|
|
91
|
+
return;
|
|
92
|
+
case 'exit':
|
|
93
|
+
process.exit(0);
|
|
94
|
+
default:
|
|
95
|
+
throw new JsonRpcError(-32601, `Method not found: ${message.method}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (id === undefined) {
|
|
100
|
+
console.error(getErrorMessage(error));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.transport.sendError(id, toJsonRpcErrorPayload(error));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
buildInitializeResult(params) {
|
|
107
|
+
const parsed = initializeParamsSchema.safeParse(params);
|
|
108
|
+
const protocolVersion = parsed.success && parsed.data.protocolVersion !== undefined
|
|
109
|
+
? parsed.data.protocolVersion
|
|
110
|
+
: this.protocolVersion;
|
|
111
|
+
return {
|
|
112
|
+
protocolVersion,
|
|
113
|
+
capabilities: { tools: { listChanged: false } },
|
|
114
|
+
serverInfo: this.serverInfo,
|
|
115
|
+
...(this.instructions ? { instructions: this.instructions } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async callTool(params) {
|
|
119
|
+
const parsed = toolCallParamsSchema.parse(params);
|
|
120
|
+
const tool = this.tools.get(parsed.name);
|
|
121
|
+
if (tool === undefined) {
|
|
122
|
+
throw new JsonRpcError(-32602, `Unknown tool: ${parsed.name}`);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const payload = await tool.handler(parsed.arguments);
|
|
126
|
+
return toToolResult(payload, false);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof z.ZodError) {
|
|
130
|
+
return toToolResult({ error: `Invalid arguments for ${parsed.name}`, issues: error.issues }, true);
|
|
131
|
+
}
|
|
132
|
+
return toToolResult({ error: getErrorMessage(error) }, true);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
requireId(id, method) {
|
|
136
|
+
if (id === undefined) {
|
|
137
|
+
throw new JsonRpcError(-32600, `Method ${method} must be called as a request with an id`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function toToolResult(payload, isError) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
144
|
+
structuredContent: payload,
|
|
145
|
+
isError,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function toJsonRpcErrorPayload(error) {
|
|
149
|
+
if (error instanceof JsonRpcError) {
|
|
150
|
+
return { code: error.code, message: error.message, data: error.data };
|
|
151
|
+
}
|
|
152
|
+
if (error instanceof z.ZodError) {
|
|
153
|
+
return { code: -32602, message: 'Invalid params', data: error.issues };
|
|
154
|
+
}
|
|
155
|
+
return { code: -32603, message: getErrorMessage(error) };
|
|
156
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const emptyInput = z.object({}).passthrough();
|
|
3
|
+
export function createHealthTool(store) {
|
|
4
|
+
return {
|
|
5
|
+
name: 'health',
|
|
6
|
+
description: 'Check the mem0-mcp server state, local store path, configured embedding model, and Ollama availability.',
|
|
7
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
8
|
+
handler: async (args) => {
|
|
9
|
+
emptyInput.parse(args ?? {});
|
|
10
|
+
return await store.healthCheck();
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { memoryForgetInputSchema } from '../../domain/memory.types.js';
|
|
2
|
+
import { scopeJsonSchema } from './shared-schemas.js';
|
|
3
|
+
export function createMemoryForgetTool(store) {
|
|
4
|
+
return {
|
|
5
|
+
name: 'memory_forget',
|
|
6
|
+
description: 'Delete a memory from the store.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
memoryId: { type: 'string', format: 'uuid' },
|
|
11
|
+
scope: scopeJsonSchema,
|
|
12
|
+
},
|
|
13
|
+
required: ['memoryId', 'scope'],
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
},
|
|
16
|
+
handler: async (args) => {
|
|
17
|
+
const input = memoryForgetInputSchema.parse(args);
|
|
18
|
+
await store.deleteMemory(input);
|
|
19
|
+
return { success: true };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { memoryRecallInputSchema } from '../../domain/memory.types.js';
|
|
2
|
+
import { scopeJsonSchema } from './shared-schemas.js';
|
|
3
|
+
export function createMemoryRecallTool(store) {
|
|
4
|
+
return {
|
|
5
|
+
name: 'memory_recall',
|
|
6
|
+
description: 'Recall a single persisted memory by ID within the provided canonical scope.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
memoryId: { type: 'string', format: 'uuid' },
|
|
11
|
+
scope: scopeJsonSchema,
|
|
12
|
+
},
|
|
13
|
+
required: ['memoryId', 'scope'],
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
},
|
|
16
|
+
handler: async (args) => {
|
|
17
|
+
const input = memoryRecallInputSchema.parse(args);
|
|
18
|
+
const memory = await store.recallMemory(input);
|
|
19
|
+
return { memory };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|