memoryai-mcp 0.2.1

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/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # memoryai-mcp
2
+
3
+ MCP server for [MemoryAI](https://memoryai.dev) — persistent AI memory as a service.
4
+
5
+ Gives your IDE agent long-term memory that persists across sessions.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx memoryai-mcp
11
+ ```
12
+
13
+ Or install globally:
14
+
15
+ ```bash
16
+ npm install -g memoryai-mcp
17
+ ```
18
+
19
+ ## IDE Setup
20
+
21
+ ### Cursor (`~/.cursor/mcp.json`)
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "memoryai": {
27
+ "command": "npx",
28
+ "args": ["-y", "memoryai-mcp"],
29
+ "env": {
30
+ "HM_ENDPOINT": "https://memoryai.dev",
31
+ "HM_API_KEY": "hm_sk_your_key_here"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### VS Code (`.vscode/mcp.json`)
39
+
40
+ ```json
41
+ {
42
+ "servers": {
43
+ "memoryai": {
44
+ "command": "npx",
45
+ "args": ["-y", "memoryai-mcp"],
46
+ "env": {
47
+ "HM_ENDPOINT": "https://memoryai.dev",
48
+ "HM_API_KEY": "hm_sk_your_key_here"
49
+ }
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ ### Claude Desktop (`claude_desktop_config.json`)
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "memoryai": {
61
+ "command": "npx",
62
+ "args": ["-y", "memoryai-mcp"],
63
+ "env": {
64
+ "HM_ENDPOINT": "https://memoryai.dev",
65
+ "HM_API_KEY": "hm_sk_your_key_here"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ### Windsurf (`~/.codeium/windsurf/mcp_config.json`)
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "memoryai": {
78
+ "command": "npx",
79
+ "args": ["-y", "memoryai-mcp"],
80
+ "env": {
81
+ "HM_ENDPOINT": "https://memoryai.dev",
82
+ "HM_API_KEY": "hm_sk_your_key_here"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Kiro (`.kiro/settings/mcp.json`)
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "memoryai": {
95
+ "command": "npx",
96
+ "args": ["-y", "memoryai-mcp"],
97
+ "env": {
98
+ "HM_ENDPOINT": "https://memoryai.dev",
99
+ "HM_API_KEY": "hm_sk_your_key_here"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### OpenClaw (`openclaw.yaml`)
107
+
108
+ ```yaml
109
+ mcp:
110
+ servers:
111
+ memoryai:
112
+ command: npx
113
+ args: ["-y", "memoryai-mcp"]
114
+ env:
115
+ HM_ENDPOINT: "https://memoryai.dev"
116
+ HM_API_KEY: "hm_sk_..."
117
+ ```
118
+
119
+ ## Tools
120
+
121
+ | Tool | Description |
122
+ |------|-------------|
123
+ | `memory_bootstrap` | Load context block at session start |
124
+ | `memory_store` | Store information in persistent memory |
125
+ | `memory_recall` | Search memories by semantic query |
126
+ | `memory_compact` | Compact text into memory chunks |
127
+ | `memory_recover` | Recover session context after break |
128
+ | `memory_health` | Check context window health |
129
+ | `memory_explore` | Explore memory connections (neural graph) |
130
+ | `memory_clusters` | View topic clusters |
131
+ | `learn` | Store action + result + lesson |
132
+ | `entity_list` | List tracked entities |
133
+ | `reasoning_store/recall` | Deep reasoning banks (Pro+) |
134
+ | `snapshot_create/restore` | Backup and restore memory |
135
+
136
+ ## Auto-Bootstrap
137
+
138
+ Add to your project rules so the agent auto-loads memory each session:
139
+
140
+ **Cursor** (`.cursor/rules/memoryai.mdc`):
141
+ ```
142
+ At the start of every session, call memory_bootstrap to load context.
143
+ After completing tasks, call memory_compact to save context.
144
+ ```
145
+
146
+ ## Get an API Key
147
+
148
+ ```bash
149
+ curl -X POST https://memoryai.dev/v1/admin/provision \
150
+ -H "Content-Type: application/json" \
151
+ -d '{"name": "my-agent", "tos_accepted": true}'
152
+ ```
153
+
154
+ ## Links
155
+
156
+ - Website: https://memoryai.dev
157
+ - Python SDK: https://pypi.org/project/hmc-memory/
158
+ - GitHub: https://github.com/memoryai-dev/memoryai
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MemoryAI — MCP Server
4
+ *
5
+ * Exposes persistent memory tools for AI IDEs via the Model Context Protocol.
6
+ * Transport: stdio
7
+ */
8
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,711 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MemoryAI — MCP Server
4
+ *
5
+ * Exposes persistent memory tools for AI IDEs via the Model Context Protocol.
6
+ * Transport: stdio
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z } from "zod";
11
+ const API_URL = process.env.HM_ENDPOINT || "http://localhost:8420";
12
+ const API_KEY = process.env.HM_API_KEY || "";
13
+ // --- HTTP helper ---
14
+ async function api(method, path, body) {
15
+ const resp = await fetch(`${API_URL}${path}`, {
16
+ method,
17
+ headers: {
18
+ Authorization: `Bearer ${API_KEY}`,
19
+ "Content-Type": "application/json",
20
+ },
21
+ body: body ? JSON.stringify(body) : undefined,
22
+ });
23
+ if (!resp.ok) {
24
+ const text = await resp.text();
25
+ throw new Error(`API ${resp.status}: ${text}`);
26
+ }
27
+ return resp.json();
28
+ }
29
+ function ok(text) {
30
+ return { content: [{ type: "text", text }] };
31
+ }
32
+ function err(e) {
33
+ const msg = e instanceof Error ? e.message : String(e);
34
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
35
+ }
36
+ // --- MCP Server ---
37
+ const server = new McpServer({ name: "memoryai", version: "0.2.0" }, { capabilities: { tools: {} } });
38
+ // 1. memory_store
39
+ server.tool("memory_store", "Store information in persistent memory. Use when you learn something important — project context, user preferences, architectural decisions, patterns, or bugs.", {
40
+ content: z.string().describe("What to remember"),
41
+ source: z.string().optional().describe("Source context (e.g. file path, conversation)"),
42
+ tags: z.array(z.string()).optional().describe("Categories: preferences, architecture, bugs, patterns, decisions"),
43
+ priority: z.enum(["hot", "warm", "cold"]).optional().describe("Memory priority (default: warm)"),
44
+ content_type: z.enum(["conversation", "code", "decision", "preference", "architecture", "lesson_learned", "todo", "entity", "pattern", "environment", "bug_fix", "action_log"]).optional().describe("Content type — helps with filtering and recall accuracy"),
45
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata (JSONB)"),
46
+ zone: z.enum(["critical", "important", "standard", "ephemeral"]).optional().describe("Memory zone (default: standard). critical=never evict, ephemeral=auto-expire"),
47
+ importance: z.number().min(0).max(1).optional().describe("Importance score 0.0-1.0 (default: 0.5). Higher = slower decay, prioritized in recall"),
48
+ }, async (args) => {
49
+ try {
50
+ const r = (await api("POST", "/v1/store", {
51
+ content: args.content,
52
+ source: args.source,
53
+ tags: args.tags || [],
54
+ priority: args.priority || "warm",
55
+ content_type: args.content_type,
56
+ metadata: args.metadata,
57
+ zone: args.zone || "standard",
58
+ importance: args.importance ?? 0.5,
59
+ }));
60
+ let msg = `Stored (id=${r.id})`;
61
+ if (r.deduplicated) {
62
+ msg += " [deduplicated]";
63
+ if (r.similar_chunk_id)
64
+ msg += ` (similar to #${r.similar_chunk_id}, ${((r.similarity || 0) * 100).toFixed(1)}% match)`;
65
+ }
66
+ return ok(msg);
67
+ }
68
+ catch (e) {
69
+ return err(e);
70
+ }
71
+ });
72
+ // 2. memory_recall
73
+ server.tool("memory_recall", "Search persistent memory for relevant context. Use before starting work to check what you already know about the project or task.", {
74
+ query: z.string().describe("What to search for"),
75
+ depth: z.enum(["fast", "deep", "exhaustive"]).optional().describe("Search depth (default: deep)"),
76
+ limit: z.number().optional().describe("Max results (default: 5)"),
77
+ min_score: z.number().optional().describe("Minimum relevance score 0-1 (default: 0)"),
78
+ tags: z.array(z.string()).optional().describe("Filter by tags"),
79
+ max_tokens: z.number().optional().describe("Token budget limit — results truncated to fit within this budget"),
80
+ priority_min: z.enum(["critical", "important", "standard", "ephemeral"]).optional().describe("Minimum zone priority filter (default: all zones)"),
81
+ }, async (args) => {
82
+ try {
83
+ const body = {
84
+ query: args.query,
85
+ depth: args.depth || "deep",
86
+ limit: args.limit || 5,
87
+ min_score: args.min_score || 0,
88
+ tags: args.tags || [],
89
+ };
90
+ if (args.max_tokens)
91
+ body.max_tokens = args.max_tokens;
92
+ if (args.priority_min)
93
+ body.priority_min = args.priority_min;
94
+ const r = (await api("POST", "/v1/recall", body));
95
+ if (!r.results?.length)
96
+ return ok("No relevant memories found.");
97
+ const text = r.results
98
+ .map((m) => `[${(m.score * 100).toFixed(0)}%] ${m.content}${m.tags?.length ? ` (tags: ${m.tags.join(", ")})` : ""}`)
99
+ .join("\n\n");
100
+ const budget = r.tokens_budget ? ` | budget: ${r.tokens_used}/${r.tokens_budget} tokens` : "";
101
+ const trunc = r.results_truncated ? ` | ${r.results_truncated} results truncated` : "";
102
+ return ok(`Found ${r.results.length} memories (${r.query_time_ms}ms${budget}${trunc}):\n\n${text}`);
103
+ }
104
+ catch (e) {
105
+ return err(e);
106
+ }
107
+ });
108
+ // 3. memory_stats
109
+ server.tool("memory_stats", "Get memory usage statistics — chunk count, storage size, monthly usage.", {}, async () => {
110
+ try {
111
+ const r = (await api("GET", "/v1/stats"));
112
+ return ok(`Memory Stats:\n` +
113
+ `- Chunks: ${r.chunks}\n` +
114
+ `- Storage: ${r.storage_mb}MB\n` +
115
+ `- Plan: ${r.plan}\n` +
116
+ `- This month: ${r.usage_this_month?.stores ?? 0} stores, ${r.usage_this_month?.recalls ?? 0} recalls`);
117
+ }
118
+ catch (e) {
119
+ return err(e);
120
+ }
121
+ });
122
+ // 4. memory_compact
123
+ server.tool("memory_compact", "Compact long text into memory chunks for long-term storage. Use at end of session or when context is getting large.", {
124
+ content: z.string().describe("Text content to compact"),
125
+ task_context: z.string().optional().describe("Brief description of the task/session"),
126
+ content_type: z.enum(["conversation", "code"]).optional().describe("Content type"),
127
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata"),
128
+ strategy: z.enum(["chronological", "priority", "semantic"]).optional().describe("Compact strategy (default: chronological)"),
129
+ preserve_tags: z.array(z.string()).optional().describe("Tags to preserve from compression"),
130
+ create_snapshot: z.boolean().optional().describe("Create backup snapshot before compacting"),
131
+ }, async (args) => {
132
+ try {
133
+ const body = {
134
+ content: args.content,
135
+ task_context: args.task_context,
136
+ content_type: args.content_type,
137
+ metadata: args.metadata,
138
+ };
139
+ // Smart compact params go to /v1/compact
140
+ if (args.strategy || args.preserve_tags || args.create_snapshot) {
141
+ body.strategy = args.strategy || "chronological";
142
+ if (args.preserve_tags)
143
+ body.preserve_tags = args.preserve_tags;
144
+ if (args.create_snapshot)
145
+ body.create_snapshot = args.create_snapshot;
146
+ const r = (await api("POST", "/v1/compact", body));
147
+ let text = `Compacted: ${r.chunks_created} chunks created, ${r.chunks_deduplicated} deduplicated`;
148
+ if (r.tokens_saved)
149
+ text += `, ${r.tokens_saved} tokens saved`;
150
+ if (r.snapshot_id)
151
+ text += ` (snapshot #${r.snapshot_id})`;
152
+ if (r.zones_compressed) {
153
+ const zc = Object.entries(r.zones_compressed).map(([k, v]) => `${k}: ${v}`).join(", ");
154
+ text += ` | zones: ${zc}`;
155
+ }
156
+ return ok(text);
157
+ }
158
+ const r = (await api("POST", "/v1/context/compact", body));
159
+ return ok(`Compacted: ${r.chunks_created} chunks created, ${r.chunks_deduplicated} deduplicated`);
160
+ }
161
+ catch (e) {
162
+ return err(e);
163
+ }
164
+ });
165
+ // 5. context_check
166
+ server.tool("context_check", "Check current context token usage and urgency level.", {
167
+ content_type: z.enum(["conversation", "code"]).optional().describe("Check conversation or code context"),
168
+ }, async (args) => {
169
+ try {
170
+ const path = args.content_type === "code" ? "/v1/context/check-code" : "/v1/context/check";
171
+ const r = (await api("POST", path));
172
+ return ok(`Context Check:\n` +
173
+ `- Tokens (est): ${r.total_tokens_est}\n` +
174
+ `- Characters: ${r.total_chars}\n` +
175
+ `- Chunks: ${r.chunk_count}\n` +
176
+ `- Urgency: ${r.urgency}`);
177
+ }
178
+ catch (e) {
179
+ return err(e);
180
+ }
181
+ });
182
+ // 6. context_restore
183
+ server.tool("context_restore", "Restore context for a task by recalling the most relevant memory chunks.", {
184
+ task_description: z.string().describe("Description of the task to restore context for"),
185
+ limit: z.number().optional().describe("Max chunks to restore (default: 5)"),
186
+ }, async (args) => {
187
+ try {
188
+ const r = (await api("POST", "/v1/context/restore", {
189
+ task_description: args.task_description,
190
+ limit: args.limit || 5,
191
+ }));
192
+ if (!r.chunks?.length)
193
+ return ok("No relevant context found.");
194
+ const text = r.chunks
195
+ .map((c) => `[${(c.score * 100).toFixed(0)}%] ${c.content.substring(0, 200)}...`)
196
+ .join("\n\n");
197
+ return ok(`Restored ${r.count} chunks (~${r.total_tokens_est} tokens):\n\n${text}`);
198
+ }
199
+ catch (e) {
200
+ return err(e);
201
+ }
202
+ });
203
+ // 7. project_index
204
+ server.tool("project_index", "Index a code project's file tree and key files into memory. IDE use only.", {
205
+ file_tree: z.array(z.string()).describe("List of file paths in the project"),
206
+ key_files: z.record(z.string(), z.string()).optional().describe("Map of file_path → file_content for important files"),
207
+ git_info: z.record(z.string(), z.unknown()).optional().describe("Git metadata (branch, remote, last commit)"),
208
+ }, async (args) => {
209
+ try {
210
+ const r = (await api("POST", "/v1/context/index-project", {
211
+ file_tree: args.file_tree,
212
+ key_files: args.key_files,
213
+ git_info: args.git_info,
214
+ }));
215
+ return ok(`Project indexed:\n` +
216
+ `- Files: ${r.files_indexed}\n` +
217
+ `- Chunks stored: ${r.chunks_stored}\n` +
218
+ `- Deduplicated: ${r.chunks_deduplicated}\n` +
219
+ `- Tree size: ${r.tree_size}`);
220
+ }
221
+ catch (e) {
222
+ return err(e);
223
+ }
224
+ });
225
+ // --- Collective tools hidden from public MCP ---
226
+ // These endpoints still work internally on the server but are not exposed via MCP.
227
+ /*
228
+ // 8. collective_contribute
229
+ server.tool(
230
+ "collective_contribute",
231
+ "Contribute knowledge to the collective memory pool. Requires opt-in. Content is anonymized before storage. Use for sharing bug fixes, patterns, decisions that could help other developers.",
232
+ {
233
+ content: z.string().describe("Knowledge to contribute (will be anonymized)"),
234
+ content_type: z.enum(["bug_fix", "pattern", "decision", "migration", "tip", "performance", "security"]).optional().describe("Type of knowledge (default: bug_fix)"),
235
+ stack_tags: z.array(z.string()).optional().describe("Technology stack tags: react, nextjs, prisma, etc."),
236
+ language: z.string().optional().describe("Programming language (default: unknown)"),
237
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata"),
238
+ },
239
+ async (args) => {
240
+ try {
241
+ const r = (await api("POST", "/v1/collective/contribute", {
242
+ content: args.content,
243
+ content_type: args.content_type || "bug_fix",
244
+ stack_tags: args.stack_tags || [],
245
+ language: args.language || "unknown",
246
+ metadata: args.metadata,
247
+ })) as any;
248
+ return ok(
249
+ `Contributed to collective (id=${r.collective_chunk_id}):\n` +
250
+ `- New: ${r.is_new}\n` +
251
+ `- Contributors: ${r.contributor_count}\n` +
252
+ `- Confidence: ${(r.confidence_score * 100).toFixed(0)}%`,
253
+ );
254
+ } catch (e) { return err(e); }
255
+ },
256
+ );
257
+
258
+ // 9. collective_recall
259
+ server.tool(
260
+ "collective_recall",
261
+ "Search the collective knowledge pool for solutions, patterns, and tips from the developer community. Free for all users.",
262
+ {
263
+ query: z.string().describe("What to search for"),
264
+ depth: z.enum(["fast", "deep"]).optional().describe("Search depth (default: deep)"),
265
+ limit: z.number().optional().describe("Max results (default: 10)"),
266
+ stack_tags: z.array(z.string()).optional().describe("Filter by stack tags"),
267
+ content_type: z.enum(["bug_fix", "pattern", "decision", "migration", "tip", "performance", "security", "knowledge_card"]).optional().describe("Filter by content type"),
268
+ shard_filter: z.array(z.number()).optional().describe("Filter by shard IDs (from collective_shards)"),
269
+ },
270
+ async (args) => {
271
+ try {
272
+ const body: Record<string, unknown> = {
273
+ query: args.query,
274
+ depth: args.depth || "deep",
275
+ limit: args.limit || 10,
276
+ stack_tags: args.stack_tags || [],
277
+ };
278
+ if (args.content_type) body.content_type = args.content_type;
279
+ if (args.shard_filter) body.shard_filter = args.shard_filter;
280
+ const r = (await api("POST", "/v1/collective/recall", body)) as any;
281
+ if (!r.results?.length) return ok("No collective knowledge found for this query.");
282
+ const text = r.results
283
+ .map((m: any) =>
284
+ `[${(m.score * 100).toFixed(0)}% | ${(m.confidence_score * 100).toFixed(0)}% confidence | ${m.contributor_count} contributors] ${m.content}${m.stack_tags?.length ? ` (${m.stack_tags.join(", ")})` : ""}`)
285
+ .join("\n\n");
286
+ return ok(`Found ${r.results.length} collective results (${r.query_time_ms}ms):\n\n${text}`);
287
+ } catch (e) { return err(e); }
288
+ },
289
+ );
290
+
291
+ // 10. collective_confirm
292
+ server.tool(
293
+ "collective_confirm",
294
+ "Confirm whether a collective solution worked or not. Helps improve confidence scores.",
295
+ {
296
+ collective_chunk_id: z.number().describe("ID of the collective chunk to confirm"),
297
+ worked: z.boolean().describe("Did the solution work? true/false"),
298
+ },
299
+ async (args) => {
300
+ try {
301
+ const r = (await api("POST", "/v1/collective/confirm", {
302
+ collective_chunk_id: args.collective_chunk_id,
303
+ worked: args.worked,
304
+ })) as any;
305
+ return ok(
306
+ `Confirmation recorded:\n` +
307
+ `- Confidence: ${(r.confidence_score * 100).toFixed(0)}%\n` +
308
+ `- Positive: ${r.positive_count}/${r.total_count}`,
309
+ );
310
+ } catch (e) { return err(e); }
311
+ },
312
+ );
313
+
314
+ // 11. collective_stats
315
+ server.tool(
316
+ "collective_stats",
317
+ "Get statistics about the collective knowledge pool.",
318
+ {},
319
+ async () => {
320
+ try {
321
+ const r = (await api("GET", "/v1/collective/stats")) as any;
322
+ const tags = Object.entries(r.top_tags || {}).map(([k, v]) => `${k}: ${v}`).join(", ");
323
+ const types = Object.entries(r.top_types || {}).map(([k, v]) => `${k}: ${v}`).join(", ");
324
+ return ok(
325
+ `Collective Stats:\n` +
326
+ `- Chunks: ${r.total_chunks}\n` +
327
+ `- Contributions: ${r.total_contributions}\n` +
328
+ `- Confirmations: ${r.total_confirmations}\n` +
329
+ `- Top tags: ${tags || "none"}\n` +
330
+ `- Top types: ${types || "none"}`,
331
+ );
332
+ } catch (e) { return err(e); }
333
+ },
334
+ );
335
+ */
336
+ // 12. l2_store
337
+ server.tool("reasoning_store", "Store content in a reasoning bank. Reasoning banks hold context for deep analysis. Requires Pro plan or higher.", {
338
+ bank_name: z.string().describe("Name of the L2 bank"),
339
+ content: z.string().describe("Content to store in the bank"),
340
+ token_count: z.number().optional().describe("Approximate token count of the content"),
341
+ }, async (args) => {
342
+ try {
343
+ const r = (await api("POST", "/v1/l2/store", {
344
+ bank_name: args.bank_name,
345
+ content: args.content,
346
+ token_count: args.token_count || 0,
347
+ }));
348
+ return ok(`Stored in bank "${r.bank_name}" (id=${r.id}, tokens=${r.token_count})${r.expires_at ? ` expires: ${r.expires_at}` : ""}`);
349
+ }
350
+ catch (e) {
351
+ return err(e);
352
+ }
353
+ });
354
+ // 13. l2_recall
355
+ server.tool("reasoning_recall", "Recall from reasoning banks via deep analysis. Requires Pro plan or higher.", {
356
+ query: z.string().describe("Question to answer using L2 bank context"),
357
+ bank_names: z.array(z.string()).optional().describe("Specific banks to search (default: all)"),
358
+ }, async (args) => {
359
+ try {
360
+ const body = {
361
+ query: args.query,
362
+ };
363
+ if (args.bank_names)
364
+ body.bank_names = args.bank_names;
365
+ const r = (await api("POST", "/v1/l2/recall", body));
366
+ return ok(`Answer (${r.tokens_used} tokens, banks: ${r.banks_searched?.join(", ") || "all"}):\n\n${r.answer}`);
367
+ }
368
+ catch (e) {
369
+ return err(e);
370
+ }
371
+ });
372
+ // 14. l2_compress
373
+ server.tool("reasoning_compress", "Compress old reasoning bank entries into a digest. Reduces storage while preserving key information.", {
374
+ bank_name: z.string().describe("Name of the L2 bank to compress"),
375
+ }, async (args) => {
376
+ try {
377
+ const r = (await api("POST", "/v1/l2/compress", {
378
+ bank_name: args.bank_name,
379
+ }));
380
+ if (!r.compressed)
381
+ return ok(`Not compressed: ${r.reason || "unknown"}`);
382
+ return ok(`Compressed ${r.entries_compressed} entries: ${r.original_tokens} → ${r.digest_tokens} tokens`);
383
+ }
384
+ catch (e) {
385
+ return err(e);
386
+ }
387
+ });
388
+ // 15. l2_stats
389
+ server.tool("reasoning_stats", "Get reasoning layer statistics — bank sizes, usage, and plan limits.", {}, async () => {
390
+ try {
391
+ const r = (await api("GET", "/v1/l2/stats"));
392
+ const bankList = (r.banks || [])
393
+ .map((b) => ` ${b.name}: ${b.entries} entries, ${b.tokens} tokens${b.digests ? ` (${b.digests} digests)` : ""}`)
394
+ .join("\n");
395
+ return ok(`Reasoning Stats:\n` +
396
+ `- Banks:\n${bankList || " (none)"}\n` +
397
+ `- Today: ${r.today_calls} calls, ${r.today_tokens} tokens\n` +
398
+ `- Limits: ${r.plan_limit?.l2_max_tokens ?? "?"} max tokens, ${r.plan_limit?.l2_calls_per_day ?? "?"} calls/day`);
399
+ }
400
+ catch (e) {
401
+ return err(e);
402
+ }
403
+ });
404
+ // 16. entity_list
405
+ server.tool("entity_list", "List tracked entities (files, URLs, people, packages, code symbols) extracted from stored memories.", {
406
+ entity_type: z.string().optional().describe("Filter by type: file, url, person, package, code_symbol"),
407
+ limit: z.number().optional().describe("Max results (default: 50)"),
408
+ }, async (args) => {
409
+ try {
410
+ const params = new URLSearchParams();
411
+ if (args.entity_type)
412
+ params.set("entity_type", args.entity_type);
413
+ if (args.limit)
414
+ params.set("limit", String(args.limit));
415
+ const qs = params.toString() ? `?${params.toString()}` : "";
416
+ const r = (await api("GET", `/v1/entities${qs}`));
417
+ if (!r.entities?.length)
418
+ return ok("No entities found.");
419
+ const text = r.entities
420
+ .map((e) => `- ${e.name} (${e.entity_type}) — ${e.mention_count} mentions`)
421
+ .join("\n");
422
+ return ok(`Entities (${r.total}):\n${text}`);
423
+ }
424
+ catch (e) {
425
+ return err(e);
426
+ }
427
+ });
428
+ // 17. entity_search
429
+ server.tool("entity_search", "Find memory chunks linked to a specific entity by name.", {
430
+ name: z.string().describe("Entity name to search for"),
431
+ limit: z.number().optional().describe("Max chunk IDs to return (default: 20)"),
432
+ }, async (args) => {
433
+ try {
434
+ const params = args.limit ? `?limit=${args.limit}` : "";
435
+ const r = (await api("GET", `/v1/entities/${encodeURIComponent(args.name)}/chunks${params}`));
436
+ if (!r.chunk_ids?.length)
437
+ return ok(`No chunks found for entity "${args.name}".`);
438
+ return ok(`Entity "${r.entity_name}" found in ${r.total} chunks: [${r.chunk_ids.join(", ")}]`);
439
+ }
440
+ catch (e) {
441
+ return err(e);
442
+ }
443
+ });
444
+ // 18. learn
445
+ server.tool("learn", "Store an action, its result, and lesson learned. Use after completing tasks, fixing bugs, or making decisions to build action memory.", {
446
+ action: z.string().describe("What was done"),
447
+ result: z.string().describe("What happened"),
448
+ outcome: z.enum(["success", "failure", "partial"]).optional().describe("Outcome (default: success)"),
449
+ lesson: z.string().optional().describe("Key takeaway or lesson learned"),
450
+ tags: z.array(z.string()).optional().describe("Additional tags"),
451
+ }, async (args) => {
452
+ try {
453
+ const r = (await api("POST", "/v1/learn", {
454
+ action: args.action,
455
+ result: args.result,
456
+ outcome: args.outcome || "success",
457
+ lesson: args.lesson,
458
+ tags: args.tags,
459
+ }));
460
+ return ok(`Learned (chunk=${r.chunk_id}, entities=${r.entities_extracted})`);
461
+ }
462
+ catch (e) {
463
+ return err(e);
464
+ }
465
+ });
466
+ // --- More collective tools hidden from public MCP ---
467
+ /*
468
+ // 12. collective_shards
469
+ server.tool(
470
+ "collective_shards",
471
+ "List all domain shards in the collective knowledge pool with chunk counts.",
472
+ {},
473
+ async () => {
474
+ try {
475
+ const r = (await api("GET", "/v1/collective/shards")) as any;
476
+ if (!r.shards?.length) return ok("No shards found.");
477
+ const text = r.shards
478
+ .map((s: any) => `- ${s.domain}/${s.subdomain || "(root)"}: ${s.chunk_count} chunks`)
479
+ .join("\n");
480
+ return ok(`Collective Shards (${r.shards.length}):\n${text}`);
481
+ } catch (e) { return err(e); }
482
+ },
483
+ );
484
+
485
+ // 13. collective_synthesize
486
+ server.tool(
487
+ "collective_synthesize",
488
+ "Trigger knowledge card synthesis from collective chunks. Groups similar chunks and creates merged knowledge cards. Admin operation.",
489
+ {},
490
+ async () => {
491
+ try {
492
+ const r = (await api("POST", "/v1/collective/synthesize")) as any;
493
+ const shardText = (r.by_shard || [])
494
+ .map((s: any) => ` ${s.domain}/${s.subdomain || "(root)"}: ${s.card_count} cards`)
495
+ .join("\n");
496
+ return ok(
497
+ `Synthesis complete:\n` +
498
+ `- Cards created: ${r.cards_created}\n` +
499
+ `- Chunks superseded: ${r.chunks_superseded}\n` +
500
+ `- Total cards: ${r.total_cards}\n` +
501
+ (shardText ? `- By shard:\n${shardText}` : ""),
502
+ );
503
+ } catch (e) { return err(e); }
504
+ },
505
+ );
506
+
507
+ // 14. pool_stats
508
+ server.tool(
509
+ "pool_stats",
510
+ "Get content pool dedup statistics — pool size, ref count, dedup ratio, and estimated savings.",
511
+ {},
512
+ async () => {
513
+ try {
514
+ const r = (await api("GET", "/v1/pool/stats")) as any;
515
+ return ok(
516
+ `Content Pool Stats:\n` +
517
+ `- Pool entries: ${r.pool_entries}\n` +
518
+ `- Total refs: ${r.total_refs}\n` +
519
+ `- Dedup ratio: ${r.dedup_ratio}x\n` +
520
+ `- Pool size: ${r.pool_size_mb}MB\n` +
521
+ `- Estimated savings: ${r.estimated_savings_mb}MB`,
522
+ );
523
+ } catch (e) { return err(e); }
524
+ },
525
+ );
526
+ */
527
+ // --- Start ---
528
+ // 16. memory_recover
529
+ server.tool("memory_recover", "Recover session context from recent memory. Use when resuming work after a break to quickly understand what was happening — active files, pending tasks, timeline, and key references.", {
530
+ task_context: z.string().optional().describe("Hint about what you were working on"),
531
+ time_range_hours: z.number().optional().describe("Look back N hours (default: 24)"),
532
+ max_tokens: z.number().optional().describe("Token budget for response (default: 8000)"),
533
+ }, async (args) => {
534
+ try {
535
+ const r = (await api("POST", "/v1/session/recover", {
536
+ task_context: args.task_context,
537
+ time_range_hours: args.time_range_hours || 24,
538
+ max_tokens: args.max_tokens || 8000,
539
+ }));
540
+ const ctx = r.active_context || {};
541
+ const pending = (r.pending_actions || []).map((a) => ` - ${a}`).join("\n");
542
+ const timeline = (r.recent_timeline || []).slice(-5)
543
+ .map((t) => ` ${t.timestamp} | ${t.action} [${t.status}]`).join("\n");
544
+ const refs = r.key_references || {};
545
+ const files = (refs.files || []).slice(0, 5).join(", ");
546
+ return ok(`Session Recovery (${r.confidence} confidence, ${r.tokens_used} tokens):\n\n` +
547
+ `Project: ${ctx.project || "unknown"} | Branch: ${ctx.branch || "unknown"}\n` +
548
+ `Key files: ${(ctx.key_files || []).slice(0, 5).join(", ") || "none"}\n\n` +
549
+ (pending ? `Pending:\n${pending}\n\n` : "") +
550
+ (timeline ? `Recent:\n${timeline}\n\n` : "") +
551
+ (files ? `References: ${files}\n` : "") +
552
+ (r.suggested_next ? `\nSuggested next: ${r.suggested_next}` : ""));
553
+ }
554
+ catch (e) {
555
+ return err(e);
556
+ }
557
+ });
558
+ // 17. memory_health
559
+ server.tool("memory_health", "Check context window health — usage percentage, compaction recommendation, and memory freshness stats.", {
560
+ current_tokens: z.number().describe("Current token count in context window"),
561
+ max_tokens: z.number().optional().describe("Max context window size (default: 200000)"),
562
+ }, async (args) => {
563
+ try {
564
+ const r = (await api("POST", "/v1/context/monitor", {
565
+ current_tokens: args.current_tokens,
566
+ max_tokens: args.max_tokens || 200000,
567
+ }));
568
+ const pct = r.usage_percent;
569
+ const barLen = 20;
570
+ const filled = Math.round(pct / 100 * barLen);
571
+ const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
572
+ return ok(`Context Health:\n` +
573
+ `[${bar}] ${pct.toFixed(1)}%\n` +
574
+ `Recommendation: ${r.recommendation.toUpperCase()}${r.should_compact ? " ⚠️ compact now" : ""}\n` +
575
+ `Tokens remaining: ~${r.estimated_tokens_remaining.toLocaleString()}\n` +
576
+ (r.hot_memories_count != null ? `Hot memories (7d): ${r.hot_memories_count}\n` : "") +
577
+ (r.stale_memories_count != null ? `Stale memories (30d+): ${r.stale_memories_count}` : ""));
578
+ }
579
+ catch (e) {
580
+ return err(e);
581
+ }
582
+ });
583
+ // 18. memory_health_detailed
584
+ server.tool("memory_health_detailed", "Get detailed memory health — chunk distribution, stale chunk count, entity stats, and actionable recommendations.", {}, async () => {
585
+ try {
586
+ const r = (await api("GET", "/v1/health/detailed"));
587
+ const recs = (r.recommendations || []).map((rec) => ` ⚠ ${rec}`).join("\n");
588
+ const statusIcon = r.status === "healthy" ? "✅" : r.status === "warning" ? "⚠️" : "🔴";
589
+ return ok(`${statusIcon} Memory Health: ${r.status.toUpperCase()}\n` +
590
+ `Usage: ${r.memory_usage_percent?.toFixed(1) ?? "?"}%\n` +
591
+ `Chunks: ${r.total_chunks} total (hot: ${r.hot_chunks}, warm: ${r.warm_chunks}, cold: ${r.cold_chunks})\n` +
592
+ `Stale (30d+): ${r.stale_chunks}\n` +
593
+ `Entities: ${r.entities_count}\n` +
594
+ (r.oldest_chunk_days != null ? `Oldest chunk: ${r.oldest_chunk_days} days\n` : "") +
595
+ (recs ? `\nRecommendations:\n${recs}` : "\nNo recommendations — looking good."));
596
+ }
597
+ catch (e) {
598
+ return err(e);
599
+ }
600
+ });
601
+ // 19. snapshot_create
602
+ server.tool("snapshot_create", "Create a snapshot backup of all current memory chunks. Use before risky operations.", {}, async () => {
603
+ try {
604
+ const r = (await api("POST", "/v1/snapshots/create"));
605
+ return ok(`Snapshot created (id=${r.snapshot_id}): ${r.chunks_count} chunks, ${r.size_bytes} bytes`);
606
+ }
607
+ catch (e) {
608
+ return err(e);
609
+ }
610
+ });
611
+ // 19. snapshot_list
612
+ server.tool("snapshot_list", "List all available memory snapshots.", {}, async () => {
613
+ try {
614
+ const r = (await api("GET", "/v1/snapshots"));
615
+ if (!r.snapshots?.length)
616
+ return ok("No snapshots found.");
617
+ const text = r.snapshots
618
+ .map((s) => `- #${s.snapshot_id} | ${s.created_at} | ${s.chunks_count} chunks | ${s.size_bytes} bytes`)
619
+ .join("\n");
620
+ return ok(`Snapshots (${r.snapshots.length}):\n${text}`);
621
+ }
622
+ catch (e) {
623
+ return err(e);
624
+ }
625
+ });
626
+ // 20. snapshot_restore
627
+ server.tool("snapshot_restore", "Restore memory from a snapshot. Current chunks will be soft-deleted and replaced.", {
628
+ snapshot_id: z.string().describe("Snapshot ID to restore from"),
629
+ }, async (args) => {
630
+ try {
631
+ const r = (await api("POST", `/v1/snapshots/${args.snapshot_id}/restore`));
632
+ return ok(`Restored ${r.restored} chunks from snapshot #${r.snapshot_id}`);
633
+ }
634
+ catch (e) {
635
+ return err(e);
636
+ }
637
+ });
638
+ // 21. memory_bootstrap
639
+ server.tool("memory_bootstrap", "Get a ready-to-use context block at session start. Automatically assembles preferences, recent activity, project context, and key entities. Call this at the beginning of every session instead of manual recalls.", {
640
+ task_description: z.string().optional().describe("What you're about to work on"),
641
+ project_name: z.string().optional().describe("Project name to focus on"),
642
+ max_tokens: z.number().optional().describe("Token budget for context block (default: 4000)"),
643
+ include_entities: z.boolean().optional().describe("Include entity list (default: true)"),
644
+ include_recent: z.boolean().optional().describe("Include recent 24h activity (default: true)"),
645
+ include_preferences: z.boolean().optional().describe("Include user preferences (default: true)"),
646
+ }, async (args) => {
647
+ try {
648
+ const r = (await api("POST", "/v1/memory/bootstrap", {
649
+ task_description: args.task_description,
650
+ project_name: args.project_name,
651
+ max_tokens: args.max_tokens || 4000,
652
+ include_entities: args.include_entities !== false,
653
+ include_recent: args.include_recent !== false,
654
+ include_preferences: args.include_preferences !== false,
655
+ }));
656
+ if (!r.memories_included)
657
+ return ok("No context found. This is a fresh session.");
658
+ return ok(`Context loaded (${r.memories_included} memories, ~${r.tokens_used} tokens):\n\n${r.context_block}`);
659
+ }
660
+ catch (e) {
661
+ return err(e);
662
+ }
663
+ });
664
+ // 22. memory_explore
665
+ server.tool("memory_explore", "Explore memory connections — find chunks related to a specific memory. Reveals hidden connections and associations.", {
666
+ chunk_id: z.number().describe("ID of the memory chunk to explore from"),
667
+ limit: z.number().optional().describe("Max neighbors to return (default: 10)"),
668
+ }, async (args) => {
669
+ try {
670
+ const r = (await api("GET", `/v1/graph/neighbors/${args.chunk_id}?limit=${args.limit || 10}`));
671
+ if (!r.neighbors?.length)
672
+ return ok(`No connections found for chunk #${args.chunk_id}.`);
673
+ const text = r.neighbors
674
+ .map((n) => {
675
+ const rel = n.relationship !== "associated" ? ` [${n.relationship}]` : "";
676
+ const neurons = n.shared_neurons?.length ? ` via: ${n.shared_neurons.slice(0, 3).join(", ")}` : "";
677
+ return `[${(n.score * 100).toFixed(0)}%${rel}] #${n.chunk_id}: ${n.content.substring(0, 200)}${neurons}`;
678
+ })
679
+ .join("\n\n");
680
+ return ok(`Exploring from #${r.anchor_id} — ${r.total} connections:\n\n${text}`);
681
+ }
682
+ catch (e) {
683
+ return err(e);
684
+ }
685
+ });
686
+ // 23. memory_clusters
687
+ server.tool("memory_clusters", "View memory topic clusters — groups of related memories organized by topic. Useful for understanding what topics are stored.", {
688
+ limit: z.number().optional().describe("Max clusters to return (default: 20)"),
689
+ }, async (args) => {
690
+ try {
691
+ const r = (await api("GET", `/v1/graph/clusters?limit=${args.limit || 20}`));
692
+ if (!r.clusters?.length)
693
+ return ok("No clusters found yet. Store more memories to build the graph.");
694
+ const text = r.clusters
695
+ .map((c) => `📌 ${c.label} (${c.size} chunks): [${c.chunk_ids.slice(0, 5).join(", ")}${c.size > 5 ? "..." : ""}]`)
696
+ .join("\n");
697
+ return ok(`Memory Clusters (${r.total_clusters}):\n\n${text}`);
698
+ }
699
+ catch (e) {
700
+ return err(e);
701
+ }
702
+ });
703
+ async function main() {
704
+ const transport = new StdioServerTransport();
705
+ await server.connect(transport);
706
+ console.error("MemoryAI MCP server running on stdio");
707
+ }
708
+ main().catch((e) => {
709
+ console.error("Fatal:", e);
710
+ process.exit(1);
711
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "memoryai-mcp",
3
+ "version": "0.2.1",
4
+ "description": "MCP server for MemoryAI — persistent AI memory as a service",
5
+ "homepage": "https://memoryai.dev",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/memoryai-dev/memoryai"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "bin": {
13
+ "memoryai-mcp": "./dist/index.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "start": "node dist/index.js",
18
+ "dev": "tsx src/index.ts"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "memory",
23
+ "ai",
24
+ "llm",
25
+ "context"
26
+ ],
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.12.0"
38
+ },
39
+ "devDependencies": {
40
+ "typescript": "^5.5.0",
41
+ "tsx": "^4.0.0",
42
+ "@types/node": "^22.0.0"
43
+ }
44
+ }