@shodh/memory-mcp 0.1.80 → 0.1.90

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">Shodh-Memory MCP Server</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>v0.1.70</strong> | Persistent cognitive memory for AI agents
8
+ <strong>v0.1.90</strong> | Persistent cognitive memory for AI agents
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -29,9 +29,11 @@
29
29
  - **Semantic Search**: Find memories by meaning using MiniLM-L6 embeddings
30
30
  - **Knowledge Graph**: Entity extraction and relationship tracking
31
31
  - **Memory Consolidation**: Automatic decay, replay, and strengthening
32
+ - **Idempotent**: Content-hash dedup — identical memories are never stored twice
32
33
  - **1-Click Install**: Auto-downloads native server binary for your platform
33
34
  - **Offline-First**: All models auto-downloaded on first run (~38MB total), no internet required after
34
- - **Fast**: Sub-millisecond graph lookup, 30-50ms semantic search
35
+ - **Fast**: <200ms API response, sub-millisecond graph lookup, 30-50ms semantic search
36
+ - **GTD Task Management**: Full todo system with projects, subtasks, comments, and reminders
35
37
 
36
38
  ## Installation
37
39
 
@@ -81,25 +83,80 @@ env = { SHODH_API_KEY = "your-api-key-here" }
81
83
  | `SHODH_STREAM` | Enable/disable streaming ingestion | `true` |
82
84
  | `SHODH_PROACTIVE` | Enable/disable proactive memory surfacing | `true` |
83
85
 
84
- ## MCP Tools (15 total)
86
+ ## MCP Tools (47 total)
87
+
88
+ <details>
89
+ <summary><b>Memory</b> — Store, search, and manage memories</summary>
85
90
 
86
91
  | Tool | Description |
87
92
  |------|-------------|
88
- | `remember` | Store a memory with optional type and tags |
93
+ | `remember` | Store a memory with optional type, tags, and metadata |
89
94
  | `recall` | Semantic search to find relevant memories |
90
95
  | `proactive_context` | Auto-surface relevant memories for current context |
91
96
  | `context_summary` | Get categorized context for session bootstrap |
92
97
  | `list_memories` | List all stored memories |
98
+ | `read_memory` | Read full content of a specific memory by ID |
93
99
  | `forget` | Delete a specific memory by ID |
94
- | `forget_by_tags` | Delete memories matching any of the specified tags |
95
- | `forget_by_date` | Delete memories within a date range |
100
+ | `reinforce` | Reinforce a memory (boost importance) |
101
+ </details>
102
+
103
+ <details>
104
+ <summary><b>Todos (GTD)</b> — Task management with projects and subtasks</summary>
105
+
106
+ | Tool | Description |
107
+ |------|-------------|
108
+ | `add_todo` | Create a task with priority, due date, project, contexts |
109
+ | `list_todos` | List/search todos with semantic or GTD-style filtering |
110
+ | `update_todo` | Update task properties (status, priority, notes) |
111
+ | `complete_todo` | Mark a task as done (auto-creates next for recurring) |
112
+ | `delete_todo` | Permanently delete a task |
113
+ | `reorder_todo` | Move a task up or down within its status group |
114
+ | `list_subtasks` | List subtasks of a parent todo |
115
+ | `add_todo_comment` | Add a comment to a task (progress, resolution) |
116
+ | `list_todo_comments` | List all comments on a task |
117
+ | `update_todo_comment` | Edit an existing comment |
118
+ | `delete_todo_comment` | Delete a comment |
119
+ | `todo_stats` | Get todo statistics by status, overdue items |
120
+ </details>
121
+
122
+ <details>
123
+ <summary><b>Projects</b> — Organize todos into groups</summary>
124
+
125
+ | Tool | Description |
126
+ |------|-------------|
127
+ | `add_project` | Create a project with optional parent (sub-projects) |
128
+ | `list_projects` | List all projects with todo counts |
129
+ | `archive_project` | Archive a project (hidden but restorable) |
130
+ | `delete_project` | Permanently delete a project |
131
+ </details>
132
+
133
+ <details>
134
+ <summary><b>Reminders</b> — Time, duration, and context-triggered reminders</summary>
135
+
136
+ | Tool | Description |
137
+ |------|-------------|
138
+ | `set_reminder` | Set a reminder (time, duration, or keyword trigger) |
139
+ | `list_reminders` | List pending/triggered/dismissed reminders |
140
+ | `dismiss_reminder` | Acknowledge a triggered reminder |
141
+ </details>
142
+
143
+ <details>
144
+ <summary><b>System</b> — Health, backups, and diagnostics</summary>
145
+
146
+ | Tool | Description |
147
+ |------|-------------|
96
148
  | `memory_stats` | Get statistics about stored memories |
97
- | `recall_by_tags` | Find memories by tag |
98
- | `recall_by_date` | Find memories within a date range |
99
- | `verify_index` | Check vector index health |
100
- | `repair_index` | Repair orphaned memories |
149
+ | `verify_index` | Check vector index integrity |
150
+ | `repair_index` | Re-index orphaned memories |
151
+ | `token_status` | Get current session token usage |
152
+ | `reset_token_session` | Reset token counter for new session |
101
153
  | `consolidation_report` | View memory consolidation activity |
102
- | `streaming_status` | Check WebSocket streaming connection status |
154
+ | `backup_create` | Create a backup of all memories |
155
+ | `backup_list` | List available backups |
156
+ | `backup_verify` | Verify backup integrity (SHA-256) |
157
+ | `backup_restore` | Restore from a backup |
158
+ | `backup_purge` | Purge old backups, keep most recent N |
159
+ </details>
103
160
 
104
161
  ## REST API (for Developers)
105
162
 
package/dist/index.js CHANGED
@@ -4881,28 +4881,74 @@ class StdioServerTransport {
4881
4881
  import { spawn } from "child_process";
4882
4882
  import * as path from "path";
4883
4883
  import * as fs from "fs";
4884
+ import * as crypto from "crypto";
4884
4885
  import { fileURLToPath } from "url";
4885
- var __filename2 = fileURLToPath(import.meta.url);
4886
- var __dirname2 = path.dirname(__filename2);
4886
+
4887
+ // security-utils.ts
4888
+ function isLocalHostFromUrl(apiUrl) {
4889
+ try {
4890
+ const url = new URL(apiUrl);
4891
+ const host = url.hostname;
4892
+ return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "0.0.0.0";
4893
+ } catch {
4894
+ return false;
4895
+ }
4896
+ }
4897
+ function shouldWarnInsecureApiUrl(apiUrl, allowHttpEnv) {
4898
+ return !isLocalHostFromUrl(apiUrl) && apiUrl.startsWith("http://") && allowHttpEnv !== "true";
4899
+ }
4900
+ function serializeAndValidateBody(body, maxLength) {
4901
+ const serialized = JSON.stringify(body);
4902
+ if (serialized.length > maxLength) {
4903
+ return { ok: false, error: `Request body exceeds maximum length of ${maxLength} characters` };
4904
+ }
4905
+ return { ok: true, serialized };
4906
+ }
4907
+ function nextReconnectDelay(currentDelayMs, maxDelayMs) {
4908
+ const safeCurrent = Math.max(currentDelayMs, 1000);
4909
+ return Math.min(safeCurrent * 2, maxDelayMs);
4910
+ }
4911
+
4912
+ // index.ts
4913
+ var __filename2 = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath(import.meta.url) : "";
4914
+ var __dirname2 = __filename2 ? path.dirname(__filename2) : process.cwd();
4887
4915
  var API_URL = process.env.SHODH_API_URL || "http://127.0.0.1:3030";
4888
4916
  var WS_URL = API_URL.replace(/^http/, "ws") + "/api/stream";
4889
4917
  var USER_ID = process.env.SHODH_USER_ID || "claude-code";
4890
- var API_KEY = process.env.SHODH_API_KEY;
4918
+ function isLocalServer() {
4919
+ try {
4920
+ const url = new URL(API_URL);
4921
+ const host = url.hostname;
4922
+ return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "0.0.0.0";
4923
+ } catch {
4924
+ return false;
4925
+ }
4926
+ }
4927
+ var SANDBOX_MODE = process.env.SMITHERY_SANDBOX === "true";
4928
+ var API_KEY = process.env.SHODH_API_KEY || (SANDBOX_MODE ? "sandbox" : "");
4891
4929
  if (!API_KEY) {
4892
- console.error("ERROR: SHODH_API_KEY environment variable not set.");
4893
- console.error("");
4894
- console.error("To fix, add to your MCP config (claude_desktop_config.json or mcp.json):");
4895
- console.error(` "env": { "SHODH_API_KEY": "your-api-key" }`);
4896
- console.error("");
4897
- console.error("Or set in your shell:");
4898
- console.error(" export SHODH_API_KEY=your-api-key");
4899
- console.error("");
4900
- console.error("For local development, use the same key set in SHODH_DEV_API_KEY on the server.");
4901
- process.exit(1);
4930
+ if (isLocalServer()) {
4931
+ API_KEY = crypto.randomBytes(32).toString("hex");
4932
+ console.error("[shodh-memory] No API key set auto-generated for local server.");
4933
+ } else {
4934
+ console.error("ERROR: SHODH_API_KEY is required for remote servers.");
4935
+ console.error("");
4936
+ console.error("To fix, add to your MCP config (claude_desktop_config.json or mcp.json):");
4937
+ console.error(` "env": { "SHODH_API_KEY": "your-api-key" }`);
4938
+ console.error("");
4939
+ console.error("Or set in your shell:");
4940
+ console.error(" export SHODH_API_KEY=your-api-key");
4941
+ process.exit(1);
4942
+ }
4902
4943
  }
4903
4944
  var RETRY_ATTEMPTS = 3;
4904
4945
  var RETRY_DELAY_MS = 1000;
4905
4946
  var REQUEST_TIMEOUT_MS = 1e4;
4947
+ var WRITE_TIMEOUT_MS = 30000;
4948
+ if (shouldWarnInsecureApiUrl(API_URL, process.env.SHODH_ALLOW_HTTP)) {
4949
+ console.error("[shodh-memory] WARNING: Using HTTP for a non-localhost server is insecure.");
4950
+ console.error("[shodh-memory] Set SHODH_API_URL to an https:// URL, or set SHODH_ALLOW_HTTP=true to suppress this warning.");
4951
+ }
4906
4952
  var MAX_CONTENT_LENGTH = 1e5;
4907
4953
  var MAX_QUERY_LENGTH = 1e4;
4908
4954
  var MAX_LIMIT = 250;
@@ -4930,10 +4976,28 @@ var STREAM_ENABLED = process.env.SHODH_STREAM !== "false";
4930
4976
  var STREAM_MIN_CONTENT_LENGTH = 50;
4931
4977
  var PROACTIVE_SURFACING = process.env.SHODH_PROACTIVE !== "false";
4932
4978
  var PROACTIVE_MIN_CONTEXT_LENGTH = 30;
4979
+ var MAX_CONTEXT_LENGTH = 4000;
4980
+ function stripSystemNoise(text) {
4981
+ let result = text;
4982
+ const tagPatterns = [
4983
+ /<task-notification>[\s\S]*?<\/task-notification>/g,
4984
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
4985
+ /<shodh-context[\s\S]*?<\/shodh-context>/g,
4986
+ /<shodh-memory[\s\S]*?<\/shodh-memory>/g,
4987
+ /<command-name>[\s\S]*?<\/command-name>/g
4988
+ ];
4989
+ for (const pattern of tagPatterns) {
4990
+ result = result.replace(pattern, "");
4991
+ }
4992
+ result = result.replace(/\s{3,}/g, " ").trim();
4993
+ return result;
4994
+ }
4933
4995
  var lastProactiveResponse = "";
4934
4996
  var streamSocket = null;
4935
4997
  var streamConnecting = false;
4936
4998
  var streamReconnectTimer = null;
4999
+ var streamReconnectDelay = 1000;
5000
+ var STREAM_RECONNECT_MAX_DELAY = 60000;
4937
5001
  var streamBuffer = [];
4938
5002
  var MAX_BUFFER_SIZE = 100;
4939
5003
  var streamHandshakeComplete = false;
@@ -4944,9 +5008,15 @@ async function connectStream() {
4944
5008
  streamConnecting = true;
4945
5009
  streamHandshakeComplete = false;
4946
5010
  try {
4947
- streamSocket = new WebSocket(WS_URL);
5011
+ const wsUrlWithAuth = WS_URL + (WS_URL.includes("?") ? "&" : "?") + "api_key=" + encodeURIComponent(API_KEY);
5012
+ streamSocket = new WebSocket(wsUrlWithAuth, {
5013
+ headers: {
5014
+ "X-API-Key": API_KEY
5015
+ }
5016
+ });
4948
5017
  streamSocket.onopen = () => {
4949
5018
  streamConnecting = false;
5019
+ streamReconnectDelay = 1000;
4950
5020
  console.error("[Stream] WebSocket connected to", WS_URL);
4951
5021
  const handshake = JSON.stringify({
4952
5022
  user_id: USER_ID,
@@ -4988,11 +5058,13 @@ async function connectStream() {
4988
5058
  streamConnecting = false;
4989
5059
  streamHandshakeComplete = false;
4990
5060
  if (STREAM_ENABLED && !streamReconnectTimer) {
5061
+ const delay = streamReconnectDelay;
5062
+ streamReconnectDelay = nextReconnectDelay(streamReconnectDelay, STREAM_RECONNECT_MAX_DELAY);
4991
5063
  streamReconnectTimer = setTimeout(() => {
4992
5064
  streamReconnectTimer = null;
4993
- console.error("[Stream] Attempting reconnect...");
5065
+ console.error(`[Stream] Attempting reconnect (next delay: ${streamReconnectDelay}ms)...`);
4994
5066
  connectStream().catch((e) => console.error("[Stream] Reconnect failed:", e));
4995
- }, 5000);
5067
+ }, delay);
4996
5068
  }
4997
5069
  };
4998
5070
  streamSocket.onerror = (error) => {
@@ -5076,7 +5148,7 @@ async function surfaceRelevant(context, maxResults = 3) {
5076
5148
  function formatSurfacedMemories(memories) {
5077
5149
  if (!memories || memories.length === 0)
5078
5150
  return "";
5079
- const formatted = memories.map((m, i) => ` ${i + 1}. [${(m.relevance_score * 100).toFixed(0)}%] ${m.content.slice(0, 80)}...`).join(`
5151
+ const formatted = memories.map((m, i) => ` ${i + 1}. [${((m.relevance_score ?? 0) * 100).toFixed(0)}%] ${m.content.slice(0, 80)}...`).join(`
5080
5152
  `);
5081
5153
  return `
5082
5154
 
@@ -5097,7 +5169,8 @@ async function apiCall(endpoint, method = "GET", body) {
5097
5169
  for (let attempt = 1;attempt <= RETRY_ATTEMPTS; attempt++) {
5098
5170
  try {
5099
5171
  const controller = new AbortController;
5100
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
5172
+ const timeout = method === "GET" ? REQUEST_TIMEOUT_MS : WRITE_TIMEOUT_MS;
5173
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
5101
5174
  const options = {
5102
5175
  method,
5103
5176
  headers: {
@@ -5107,7 +5180,11 @@ async function apiCall(endpoint, method = "GET", body) {
5107
5180
  signal: controller.signal
5108
5181
  };
5109
5182
  if (body) {
5110
- options.body = JSON.stringify(body);
5183
+ const bodyValidation = serializeAndValidateBody(body, MAX_CONTENT_LENGTH);
5184
+ if (!bodyValidation.ok) {
5185
+ throw new Error(bodyValidation.error);
5186
+ }
5187
+ options.body = bodyValidation.serialized;
5111
5188
  }
5112
5189
  const response = await fetch(`${API_URL}${endpoint}`, options);
5113
5190
  clearTimeout(timeoutId);
@@ -5148,7 +5225,7 @@ async function isServerAvailable() {
5148
5225
  }
5149
5226
  var server = new Server({
5150
5227
  name: "shodh-memory",
5151
- version: "0.1.61"
5228
+ version: "0.1.90"
5152
5229
  }, {
5153
5230
  capabilities: {
5154
5231
  tools: {},
@@ -5375,6 +5452,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5375
5452
  }
5376
5453
  }
5377
5454
  },
5455
+ {
5456
+ name: "backup_restore",
5457
+ description: "Restore a previously created backup by ID. This replaces all current data for the user with the backup contents. Server restart is recommended after restore.",
5458
+ inputSchema: {
5459
+ type: "object",
5460
+ properties: {
5461
+ backup_id: {
5462
+ type: "number",
5463
+ description: "The backup ID to restore (from backup_list)"
5464
+ }
5465
+ },
5466
+ required: ["backup_id"]
5467
+ }
5468
+ },
5378
5469
  {
5379
5470
  name: "consolidation_report",
5380
5471
  description: "Get a report of what the memory system has been learning. Shows memory strengthening/decay events, edge formation, fact extraction, and maintenance cycles. Use this to understand how your memories are evolving.",
@@ -5489,6 +5580,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5489
5580
  type: "array",
5490
5581
  items: { type: "string" },
5491
5582
  description: "Optional tags for categorization"
5583
+ },
5584
+ threshold: {
5585
+ type: "number",
5586
+ description: "Semantic similarity threshold for 'context' trigger (0.0-1.0, default: 0.7). Lower values match more broadly, higher values require closer semantic match."
5492
5587
  }
5493
5588
  },
5494
5589
  required: ["content", "trigger_type"]
@@ -6001,7 +6096,7 @@ To start: cd shodh-memory && cargo run`
6001
6096
  response += ` │ Tags: ${tags.join(", ")}`;
6002
6097
  }
6003
6098
  response += `
6004
- ID: ${result.id.slice(0, 8)}...`;
6099
+ ID: ${result.id}`;
6005
6100
  return {
6006
6101
  content: [{ type: "text", text: response }]
6007
6102
  };
@@ -6028,6 +6123,7 @@ ID: ${result.id.slice(0, 8)}...`;
6028
6123
  const memories = result.memories || [];
6029
6124
  const todos = result.todos || [];
6030
6125
  const stats = result.retrieval_stats;
6126
+ const lineage = result.lineage || [];
6031
6127
  if (memories.length === 0 && todos.length === 0) {
6032
6128
  return {
6033
6129
  content: [
@@ -6084,7 +6180,7 @@ ID: ${result.id.slice(0, 8)}...`;
6084
6180
  `;
6085
6181
  response += ` ${content.slice(0, 200)}${content.length > 200 ? "..." : ""}
6086
6182
  `;
6087
- response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id.slice(0, 8)}...
6183
+ response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id}
6088
6184
  `;
6089
6185
  if (i < memories.length - 1)
6090
6186
  response += `
@@ -6126,12 +6222,34 @@ ID: ${result.id.slice(0, 8)}...`;
6126
6222
  `;
6127
6223
  const graphPct = (stats.graph_weight * 100).toFixed(0);
6128
6224
  const semPct = (stats.semantic_weight * 100).toFixed(0);
6129
- response += ` Graph: ${graphPct}% │ Semantic: ${semPct}% │ Density: ${stats.graph_density.toFixed(2)}
6225
+ response += ` Graph: ${graphPct}% │ Semantic: ${semPct}% │ Density: ${(stats.graph_density ?? 0).toFixed(2)}
6130
6226
  `;
6131
6227
  response += ` Candidates: ${stats.graph_candidates} graph + ${stats.semantic_candidates} semantic
6132
6228
  `;
6133
6229
  response += ` Entities: ${stats.entities_activated} │ Time: ${(stats.retrieval_time_us / 1000).toFixed(1)}ms`;
6134
6230
  }
6231
+ if (lineage.length > 0) {
6232
+ const idShort = (id) => id;
6233
+ const idToContent = new Map;
6234
+ for (const m of memories) {
6235
+ idToContent.set(m.id, getContent(m).slice(0, 40));
6236
+ }
6237
+ response += `
6238
+
6239
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6240
+ `;
6241
+ response += `\uD83D\uDD17 LINEAGE (${lineage.length} causal edge${lineage.length > 1 ? "s" : ""})
6242
+ `;
6243
+ for (const edge of lineage) {
6244
+ const fromLabel = idToContent.get(edge.from) || idShort(edge.from);
6245
+ const toLabel = idToContent.get(edge.to) || idShort(edge.to);
6246
+ const conf = (edge.confidence * 100).toFixed(0);
6247
+ response += ` ${idShort(edge.from)} ──${edge.relation}──▶ ${idShort(edge.to)} (${conf}%)
6248
+ `;
6249
+ response += ` "${fromLabel}..." → "${toLabel}..."
6250
+ `;
6251
+ }
6252
+ }
6135
6253
  return {
6136
6254
  content: [{ type: "text", text: response }]
6137
6255
  };
@@ -6303,7 +6421,7 @@ ID: ${result.id.slice(0, 8)}...`;
6303
6421
  }[getType(m)] || "\uD83D\uDCE6";
6304
6422
  response += `${String(i + 1).padStart(2)}. ${typeIcon} ${content.slice(0, 150)}${content.length > 150 ? "..." : ""}
6305
6423
  `;
6306
- response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id.slice(0, 8)}...
6424
+ response += ` ┗━ ${getType(m)}${m.tier ? ` │ ${m.tier}` : ""} │ ${m.id}
6307
6425
  `;
6308
6426
  }
6309
6427
  return {
@@ -6317,7 +6435,7 @@ ID: ${result.id.slice(0, 8)}...`;
6317
6435
  `;
6318
6436
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6319
6437
  `;
6320
- response += `✓ Removed: ${id.slice(0, 8)}...`;
6438
+ response += `✓ Removed: ${id}`;
6321
6439
  return {
6322
6440
  content: [{ type: "text", text: response }]
6323
6441
  };
@@ -6463,7 +6581,7 @@ By Type:
6463
6581
  response += `Created: ${new Date(b.created_at).toLocaleString()}
6464
6582
  `;
6465
6583
  } else {
6466
- response += `✗ Failed: ${result.message}
6584
+ response += `✗ Failed: ${result.message || "Unknown backup creation error"}
6467
6585
  `;
6468
6586
  }
6469
6587
  return {
@@ -6520,7 +6638,7 @@ By Type:
6520
6638
  `;
6521
6639
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6522
6640
  `;
6523
- response += result.message;
6641
+ response += result.message || "No verification details provided";
6524
6642
  return {
6525
6643
  content: [{ type: "text", text: response }]
6526
6644
  };
@@ -6546,6 +6664,32 @@ By Type:
6546
6664
  content: [{ type: "text", text: response }]
6547
6665
  };
6548
6666
  }
6667
+ case "backup_restore": {
6668
+ const { backup_id } = args;
6669
+ const result = await apiCall("/api/backup/restore", "POST", {
6670
+ user_id: USER_ID,
6671
+ backup_id
6672
+ });
6673
+ let response = `\uD83D\uDD04 Backup Restore
6674
+ `;
6675
+ response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6676
+ `;
6677
+ if (result.success) {
6678
+ response += `✓ Backup #${backup_id} restored successfully
6679
+ `;
6680
+ if (result.restored_stores.length > 0) {
6681
+ response += `Restored stores: ${result.restored_stores.join(", ")}
6682
+ `;
6683
+ }
6684
+ response += `
6685
+ ⚠️ ${result.message || "Restore completed with no additional details"}`;
6686
+ } else {
6687
+ response += `✗ Restore failed: ${result.message || "Unknown restore error"}`;
6688
+ }
6689
+ return {
6690
+ content: [{ type: "text", text: response }]
6691
+ };
6692
+ }
6549
6693
  case "proactive_context": {
6550
6694
  const {
6551
6695
  context,
@@ -6556,9 +6700,17 @@ By Type:
6556
6700
  memory_types = [],
6557
6701
  auto_ingest = true
6558
6702
  } = args;
6703
+ const cleanedContext = stripSystemNoise(context).slice(0, MAX_CONTEXT_LENGTH);
6704
+ if (cleanedContext.length < PROACTIVE_MIN_CONTEXT_LENGTH) {
6705
+ return {
6706
+ content: [{ type: "text", text: `No relevant memories surfaced (context too short after cleaning).
6707
+
6708
+ [Latency: 0.0ms]` }]
6709
+ };
6710
+ }
6559
6711
  const result = await apiCall("/api/proactive_context", "POST", {
6560
6712
  user_id: USER_ID,
6561
- context,
6713
+ context: cleanedContext,
6562
6714
  max_results,
6563
6715
  semantic_threshold,
6564
6716
  entity_match_weight,
@@ -6566,7 +6718,7 @@ By Type:
6566
6718
  memory_types,
6567
6719
  auto_ingest,
6568
6720
  previous_response: lastProactiveResponse || undefined,
6569
- user_followup: lastProactiveResponse ? context : undefined
6721
+ user_followup: lastProactiveResponse ? cleanedContext : undefined
6570
6722
  });
6571
6723
  const memories = result.memories || [];
6572
6724
  const entities = result.detected_entities || [];
@@ -6579,7 +6731,7 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6579
6731
  [Feedback: ${result.feedback_processed.memories_evaluated} evaluated, ${result.feedback_processed.reinforced.length} reinforced, ${result.feedback_processed.weakened.length} weakened]` : "";
6580
6732
  const emptyText = `No relevant memories surfaced for this context.${entityList}${feedbackNote2}
6581
6733
 
6582
- [Latency: ${result.latency_ms.toFixed(1)}ms]`;
6734
+ [Latency: ${(result.latency_ms ?? 0).toFixed(1)}ms]`;
6583
6735
  lastProactiveResponse = emptyText;
6584
6736
  return {
6585
6737
  content: [{ type: "text", text: emptyText }]
@@ -6605,7 +6757,7 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6605
6757
  for (const r of uniqueReminders) {
6606
6758
  const icon = r.overdue_seconds && r.overdue_seconds > 0 ? "⏰" : "\uD83D\uDCCC";
6607
6759
  const contentText = r.content.slice(0, 38);
6608
- reminderBlock += `┃ ${icon} ${contentText.padEnd(44)} [${r.id.slice(0, 8)}] ┃
6760
+ reminderBlock += `┃ ${icon} ${contentText.padEnd(44)} [${r.id}] ┃
6609
6761
  `;
6610
6762
  if (r.overdue_seconds && r.overdue_seconds > 0) {
6611
6763
  const mins = Math.round(r.overdue_seconds / 60);
@@ -6701,7 +6853,7 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6701
6853
  const feedbackNote = result.feedback_processed ? `
6702
6854
  [Feedback loop: ${result.feedback_processed.memories_evaluated} evaluated, ${result.feedback_processed.reinforced.length} reinforced, ${result.feedback_processed.weakened.length} weakened]` : "";
6703
6855
  const ingestNote = result.ingested_memory_id ? `
6704
- [Context ingested: ${result.ingested_memory_id.slice(0, 8)}]` : "";
6856
+ [Context ingested: ${result.ingested_memory_id}]` : "";
6705
6857
  const summaryParts = [];
6706
6858
  if (memories.length > 0)
6707
6859
  summaryParts.push(`${memories.length} memories`);
@@ -6716,7 +6868,7 @@ Detected entities: ${entities.map((e) => `"${e.name}" (${e.entity_type})`).join(
6716
6868
 
6717
6869
  ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${feedbackNote}${ingestNote}
6718
6870
 
6719
- [Latency: ${result.latency_ms.toFixed(1)}ms | Threshold: ${(semantic_threshold * 100).toFixed(0)}%]`;
6871
+ [Latency: ${(result.latency_ms ?? 0).toFixed(1)}ms | Threshold: ${(semantic_threshold * 100).toFixed(0)}%]`;
6720
6872
  lastProactiveResponse = responseText;
6721
6873
  return {
6722
6874
  content: [{ type: "text", text: responseText }]
@@ -6855,7 +7007,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6855
7007
  };
6856
7008
  }
6857
7009
  case "set_reminder": {
6858
- const { content, trigger_type, trigger_at, after_seconds, keywords, priority = 3, tags = [] } = args;
7010
+ const { content, trigger_type, trigger_at, after_seconds, keywords, priority = 3, tags = [], threshold } = args;
6859
7011
  if (!content || content.length === 0) {
6860
7012
  return { content: [{ type: "text", text: "Error: 'content' is required and cannot be empty" }], isError: true };
6861
7013
  }
@@ -6892,7 +7044,8 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6892
7044
  isError: true
6893
7045
  };
6894
7046
  }
6895
- trigger = { type: "context", keywords, threshold: 0.7 };
7047
+ const ctxThreshold = threshold !== undefined && threshold >= 0 && threshold <= 1 ? threshold : 0.7;
7048
+ trigger = { type: "context", keywords, threshold: ctxThreshold };
6896
7049
  break;
6897
7050
  default:
6898
7051
  return {
@@ -6911,7 +7064,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6911
7064
  `;
6912
7065
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6913
7066
  `;
6914
- response += `ID: ${result.id.slice(0, 8)}...
7067
+ response += `ID: ${result.id}
6915
7068
  `;
6916
7069
  response += `Content: ${content}
6917
7070
  `;
@@ -6954,7 +7107,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6954
7107
  const statusBadge = r.status === "triggered" ? " [TRIGGERED]" : "";
6955
7108
  response += `${icon} ${r.content.slice(0, 50)}${r.content.length > 50 ? "..." : ""}${statusBadge}
6956
7109
  `;
6957
- response += ` Type: ${r.trigger_type} | Priority: ${"★".repeat(r.priority)} | ID: ${r.id.slice(0, 8)}...
7110
+ response += ` Type: ${r.trigger_type} | Priority: ${"★".repeat(r.priority)} | ID: ${r.id}
6958
7111
  `;
6959
7112
  if (r.due_at) {
6960
7113
  response += ` Due: ${new Date(r.due_at).toLocaleString()}
@@ -6981,7 +7134,7 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
6981
7134
  content: [
6982
7135
  {
6983
7136
  type: "text",
6984
- text: result.success ? `✓ Reminder dismissed: ${reminder_id.slice(0, 8)}...` : `⚠️ ${result.message}`
7137
+ text: result.success ? `✓ Reminder dismissed: ${reminder_id}` : `⚠️ ${result.message || "No message returned"}`
6985
7138
  }
6986
7139
  ]
6987
7140
  };
@@ -7199,7 +7352,13 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
7199
7352
  };
7200
7353
  }
7201
7354
  case "read_memory": {
7202
- const { memory_id } = args;
7355
+ const memory_id = args.memory_id || args.id;
7356
+ if (!memory_id || typeof memory_id !== "string" || memory_id.trim().length === 0) {
7357
+ return {
7358
+ content: [{ type: "text", text: "Error: 'memory_id' is required. Pass the full UUID or 8+ character prefix from recall results." }],
7359
+ isError: true
7360
+ };
7361
+ }
7203
7362
  let memory = null;
7204
7363
  try {
7205
7364
  memory = await apiCall(`/api/memory/${memory_id}?user_id=${encodeURIComponent(USER_ID)}`, "GET");
@@ -7222,11 +7381,11 @@ ${formattedWithTime}${entitySummary}${factsBlock}${reminderBlock}${todoBlock}${f
7222
7381
  response += `Tier: ${memory.tier || "Unknown"} | Created: ${created} | Importance: ${(memory.importance * 100).toFixed(0)}%
7223
7382
  `;
7224
7383
  if (memory.parent_id) {
7225
- response += `Parent: ${memory.parent_id.slice(0, 8)}...
7384
+ response += `Parent: ${memory.parent_id}
7226
7385
  `;
7227
7386
  }
7228
7387
  if (memory.children_count > 0) {
7229
- response += `Children: ${memory.children_count} (${memory.children_ids.map((id) => id.slice(0, 8)).join(", ")})
7388
+ response += `Children: ${memory.children_count} (${memory.children_ids.join(", ")})
7230
7389
  `;
7231
7390
  }
7232
7391
  response += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -7878,14 +8037,14 @@ function getBinaryPath() {
7878
8037
  wrapperName = "shodh-memory";
7879
8038
  fallbackName = "shodh-memory-server";
7880
8039
  }
7881
- const wrapperPath = path.join(binDir, wrapperName);
7882
- if (fs.existsSync(wrapperPath)) {
7883
- return wrapperPath;
7884
- }
7885
8040
  const binaryPath = path.join(binDir, fallbackName);
7886
8041
  if (fs.existsSync(binaryPath)) {
7887
8042
  return binaryPath;
7888
8043
  }
8044
+ const wrapperPath = path.join(binDir, wrapperName);
8045
+ if (fs.existsSync(wrapperPath)) {
8046
+ return wrapperPath;
8047
+ }
7889
8048
  return null;
7890
8049
  }
7891
8050
  async function isServerRunning() {
@@ -7910,9 +8069,32 @@ async function waitForServer(maxAttempts = 30) {
7910
8069
  }
7911
8070
  return false;
7912
8071
  }
8072
+ async function validateApiKey() {
8073
+ try {
8074
+ const controller = new AbortController;
8075
+ const timeout = setTimeout(() => controller.abort(), 3000);
8076
+ const response = await fetch(`${API_URL}/api/health`, {
8077
+ headers: { "X-API-Key": API_KEY },
8078
+ signal: controller.signal
8079
+ });
8080
+ clearTimeout(timeout);
8081
+ return response.ok;
8082
+ } catch {
8083
+ return false;
8084
+ }
8085
+ }
7913
8086
  async function ensureServerRunning() {
7914
8087
  if (await isServerRunning()) {
7915
8088
  console.error("[shodh-memory] Backend server already running at", API_URL);
8089
+ if (!process.env.SHODH_API_KEY && isLocalServer()) {
8090
+ const keyWorks = await validateApiKey();
8091
+ if (!keyWorks) {
8092
+ console.error("[shodh-memory] WARNING: Auto-generated key rejected by running server.");
8093
+ console.error("[shodh-memory] The server was started with a different API key.");
8094
+ console.error("[shodh-memory] Set SHODH_API_KEY to match the server's key, or restart");
8095
+ console.error("[shodh-memory] the server without SHODH_DEV_API_KEY to use auto-generated keys.");
8096
+ }
8097
+ }
7916
8098
  return;
7917
8099
  }
7918
8100
  if (!AUTO_SPAWN_ENABLED) {
@@ -7930,6 +8112,13 @@ async function ensureServerRunning() {
7930
8112
  console.error("[shodh-memory] Or download from: https://github.com/varun29ankuS/shodh-memory/releases");
7931
8113
  return;
7932
8114
  }
8115
+ const expectedBinDir = fs.realpathSync(path.join(__dirname2, "..", "bin"));
8116
+ const resolvedBinary = fs.realpathSync(binaryPath);
8117
+ if (!resolvedBinary.startsWith(expectedBinDir + path.sep) && resolvedBinary !== expectedBinDir) {
8118
+ console.error(`[shodh-memory] WARNING: Binary path resolves outside expected directory: ${resolvedBinary}`);
8119
+ console.error(`[shodh-memory] Expected: ${expectedBinDir}`);
8120
+ return;
8121
+ }
7933
8122
  console.error("[shodh-memory] Starting backend server...");
7934
8123
  const serverEnv = {};
7935
8124
  const SERVER_ENV_ALLOWLIST = new Set([
@@ -7966,10 +8155,12 @@ async function ensureServerRunning() {
7966
8155
  }
7967
8156
  }
7968
8157
  serverEnv["SHODH_DEV_API_KEY"] = API_KEY;
8158
+ const isBat = binaryPath.endsWith(".bat");
7969
8159
  serverProcess = spawn(binaryPath, [], {
7970
8160
  detached: true,
7971
8161
  stdio: "ignore",
7972
- env: serverEnv
8162
+ env: serverEnv,
8163
+ ...isBat && { shell: true }
7973
8164
  });
7974
8165
  serverProcess.unref();
7975
8166
  console.error("[shodh-memory] Waiting for server to start...");
@@ -8005,14 +8196,23 @@ process.on("SIGTERM", () => {
8005
8196
  cleanupServer();
8006
8197
  process.exit(0);
8007
8198
  });
8199
+ function createSandboxServer() {
8200
+ process.env.SMITHERY_SANDBOX = "true";
8201
+ return server;
8202
+ }
8008
8203
  async function main() {
8204
+ if (SANDBOX_MODE)
8205
+ return;
8009
8206
  await ensureServerRunning();
8010
8207
  const transport = new StdioServerTransport;
8011
8208
  await server.connect(transport);
8012
- console.error("Shodh-Memory MCP server v0.1.80 running");
8209
+ console.error("Shodh-Memory MCP server v0.1.90 running");
8013
8210
  console.error(`Connecting to: ${API_URL}`);
8014
8211
  console.error(`User ID: ${USER_ID}`);
8015
8212
  console.error(`Streaming: ${STREAM_ENABLED ? "enabled" : "disabled"}`);
8016
8213
  console.error(`Proactive surfacing: ${PROACTIVE_SURFACING ? "enabled" : "disabled (SHODH_PROACTIVE=false)"}`);
8017
8214
  }
8018
8215
  main().catch(console.error);
8216
+ export {
8217
+ createSandboxServer
8218
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shodh/memory-mcp",
3
- "version": "0.1.80",
3
+ "version": "0.1.90",
4
4
  "mcpName": "io.github.varun29ankuS/shodh-memory",
5
5
  "description": "MCP server for persistent AI memory - store and recall context across sessions",
6
6
  "type": "module",
@@ -11,6 +11,9 @@
11
11
  "scripts": {
12
12
  "start": "bun run index.ts",
13
13
  "build": "bun build index.ts --outdir dist --target node",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage",
14
17
  "postinstall": "node scripts/postinstall.cjs",
15
18
  "prepublishOnly": "npm run build"
16
19
  },
@@ -22,6 +25,10 @@
22
25
  "dependencies": {
23
26
  "@modelcontextprotocol/sdk": "^1.24.0"
24
27
  },
28
+ "devDependencies": {
29
+ "@vitest/coverage-v8": "^2.1.9",
30
+ "vitest": "^2.1.8"
31
+ },
25
32
  "keywords": [
26
33
  "mcp",
27
34
  "model-context-protocol",
@@ -11,7 +11,7 @@ const path = require('path');
11
11
  const https = require('https');
12
12
  const { execSync } = require('child_process');
13
13
 
14
- const VERSION = '0.1.74';
14
+ const VERSION = require('../package.json').version;
15
15
  const REPO = 'varun29ankuS/shodh-memory';
16
16
  const BIN_DIR = path.join(__dirname, '..', 'bin');
17
17