@shodh/memory-mcp 0.1.0 → 0.1.2
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 +9 -37
- package/dist/index.js +237 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,36 +11,25 @@ Persistent AI memory with semantic search. Store observations, decisions, learni
|
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Add to your MCP client config:
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
# Download and run the server
|
|
18
|
-
cargo install shodh-memory
|
|
19
|
-
shodh-memory-server
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Or with Docker:
|
|
23
|
-
```bash
|
|
24
|
-
docker run -p 3030:3030 shodh/memory
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### 2. Configure your MCP client
|
|
28
|
-
|
|
29
|
-
**For Claude Desktop** (`~/.claude/claude_desktop_config.json`):
|
|
16
|
+
**Claude Desktop / Claude Code** (`claude_desktop_config.json`):
|
|
30
17
|
```json
|
|
31
18
|
{
|
|
32
19
|
"mcpServers": {
|
|
33
20
|
"shodh-memory": {
|
|
34
21
|
"command": "npx",
|
|
35
|
-
"args": ["@shodh/memory-mcp"]
|
|
36
|
-
"env": {
|
|
37
|
-
"SHODH_API_URL": "http://127.0.0.1:3030"
|
|
38
|
-
}
|
|
22
|
+
"args": ["-y", "@shodh/memory-mcp"]
|
|
39
23
|
}
|
|
40
24
|
}
|
|
41
25
|
}
|
|
42
26
|
```
|
|
43
27
|
|
|
28
|
+
Config file locations:
|
|
29
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
30
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
31
|
+
- Linux: `~/.config/Claude/claude_desktop_config.json`
|
|
32
|
+
|
|
44
33
|
**For Cursor/other MCP clients**: Similar configuration with the npx command.
|
|
45
34
|
|
|
46
35
|
## Tools
|
|
@@ -49,6 +38,7 @@ docker run -p 3030:3030 shodh/memory
|
|
|
49
38
|
|------|-------------|
|
|
50
39
|
| `remember` | Store a memory with optional type and tags |
|
|
51
40
|
| `recall` | Semantic search to find relevant memories |
|
|
41
|
+
| `context_summary` | Get categorized context for session bootstrap |
|
|
52
42
|
| `list_memories` | List all stored memories |
|
|
53
43
|
| `forget` | Delete a specific memory by ID |
|
|
54
44
|
| `memory_stats` | Get statistics about stored memories |
|
|
@@ -62,24 +52,6 @@ docker run -p 3030:3030 shodh/memory
|
|
|
62
52
|
"Show memory stats"
|
|
63
53
|
```
|
|
64
54
|
|
|
65
|
-
## Environment Variables
|
|
66
|
-
|
|
67
|
-
| Variable | Default | Description |
|
|
68
|
-
|----------|---------|-------------|
|
|
69
|
-
| `SHODH_API_URL` | `http://127.0.0.1:3030` | Backend server URL |
|
|
70
|
-
| `SHODH_API_KEY` | (dev key) | API key for authentication |
|
|
71
|
-
| `SHODH_USER_ID` | `default` | User ID for memory isolation |
|
|
72
|
-
|
|
73
|
-
## Architecture
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
77
|
-
│ AI Client │────▶│ MCP Server │────▶│ Shodh Backend │
|
|
78
|
-
│ (Claude, etc.) │ │ (this package) │ │ (Rust server) │
|
|
79
|
-
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
80
|
-
stdio REST API
|
|
81
|
-
```
|
|
82
|
-
|
|
83
55
|
## License
|
|
84
56
|
|
|
85
57
|
Apache-2.0
|
package/dist/index.js
CHANGED
|
@@ -4881,26 +4881,71 @@ class StdioServerTransport {
|
|
|
4881
4881
|
var API_URL = process.env.SHODH_API_URL || "http://127.0.0.1:3030";
|
|
4882
4882
|
var API_KEY = process.env.SHODH_API_KEY || "shodh-dev-key-change-in-production";
|
|
4883
4883
|
var USER_ID = process.env.SHODH_USER_ID || "claude-code";
|
|
4884
|
+
var RETRY_ATTEMPTS = 3;
|
|
4885
|
+
var RETRY_DELAY_MS = 1000;
|
|
4886
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
4887
|
+
function getContent(m) {
|
|
4888
|
+
return m.experience?.content || "";
|
|
4889
|
+
}
|
|
4890
|
+
function getType(m) {
|
|
4891
|
+
return m.experience?.experience_type || "Observation";
|
|
4892
|
+
}
|
|
4893
|
+
function sleep(ms) {
|
|
4894
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4895
|
+
}
|
|
4884
4896
|
async function apiCall(endpoint, method = "GET", body) {
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4897
|
+
let lastError = null;
|
|
4898
|
+
for (let attempt = 1;attempt <= RETRY_ATTEMPTS; attempt++) {
|
|
4899
|
+
try {
|
|
4900
|
+
const controller = new AbortController;
|
|
4901
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
4902
|
+
const options = {
|
|
4903
|
+
method,
|
|
4904
|
+
headers: {
|
|
4905
|
+
"Content-Type": "application/json",
|
|
4906
|
+
"X-API-Key": API_KEY
|
|
4907
|
+
},
|
|
4908
|
+
signal: controller.signal
|
|
4909
|
+
};
|
|
4910
|
+
if (body) {
|
|
4911
|
+
options.body = JSON.stringify(body);
|
|
4912
|
+
}
|
|
4913
|
+
const response = await fetch(`${API_URL}${endpoint}`, options);
|
|
4914
|
+
clearTimeout(timeoutId);
|
|
4915
|
+
if (!response.ok) {
|
|
4916
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
4917
|
+
throw new Error(`API error ${response.status}: ${errorText}`);
|
|
4918
|
+
}
|
|
4919
|
+
return await response.json();
|
|
4920
|
+
} catch (error) {
|
|
4921
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
4922
|
+
if (lastError.message.includes("API error 4")) {
|
|
4923
|
+
throw lastError;
|
|
4924
|
+
}
|
|
4925
|
+
if (attempt < RETRY_ATTEMPTS) {
|
|
4926
|
+
console.error(`Attempt ${attempt} failed: ${lastError.message}. Retrying in ${RETRY_DELAY_MS}ms...`);
|
|
4927
|
+
await sleep(RETRY_DELAY_MS * attempt);
|
|
4928
|
+
}
|
|
4890
4929
|
}
|
|
4891
|
-
};
|
|
4892
|
-
if (body) {
|
|
4893
|
-
options.body = JSON.stringify(body);
|
|
4894
4930
|
}
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4931
|
+
throw new Error(`Failed after ${RETRY_ATTEMPTS} attempts: ${lastError?.message || "Unknown error"}`);
|
|
4932
|
+
}
|
|
4933
|
+
async function isServerAvailable() {
|
|
4934
|
+
try {
|
|
4935
|
+
const controller = new AbortController;
|
|
4936
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
4937
|
+
const response = await fetch(`${API_URL}/health`, {
|
|
4938
|
+
signal: controller.signal
|
|
4939
|
+
});
|
|
4940
|
+
clearTimeout(timeoutId);
|
|
4941
|
+
return response.ok;
|
|
4942
|
+
} catch {
|
|
4943
|
+
return false;
|
|
4898
4944
|
}
|
|
4899
|
-
return response.json();
|
|
4900
4945
|
}
|
|
4901
4946
|
var server = new Server({
|
|
4902
4947
|
name: "shodh-memory",
|
|
4903
|
-
version: "0.1.
|
|
4948
|
+
version: "0.1.2"
|
|
4904
4949
|
}, {
|
|
4905
4950
|
capabilities: {
|
|
4906
4951
|
tools: {},
|
|
@@ -4954,6 +4999,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4954
4999
|
required: ["query"]
|
|
4955
5000
|
}
|
|
4956
5001
|
},
|
|
5002
|
+
{
|
|
5003
|
+
name: "context_summary",
|
|
5004
|
+
description: "Get a condensed summary of recent learnings, decisions, and context. Use this at the start of a session to quickly understand what you've learned before.",
|
|
5005
|
+
inputSchema: {
|
|
5006
|
+
type: "object",
|
|
5007
|
+
properties: {
|
|
5008
|
+
include_decisions: {
|
|
5009
|
+
type: "boolean",
|
|
5010
|
+
description: "Include recent decisions (default: true)",
|
|
5011
|
+
default: true
|
|
5012
|
+
},
|
|
5013
|
+
include_learnings: {
|
|
5014
|
+
type: "boolean",
|
|
5015
|
+
description: "Include recent learnings (default: true)",
|
|
5016
|
+
default: true
|
|
5017
|
+
},
|
|
5018
|
+
include_context: {
|
|
5019
|
+
type: "boolean",
|
|
5020
|
+
description: "Include project context (default: true)",
|
|
5021
|
+
default: true
|
|
5022
|
+
},
|
|
5023
|
+
max_items: {
|
|
5024
|
+
type: "number",
|
|
5025
|
+
description: "Maximum items per category (default: 5)",
|
|
5026
|
+
default: 5
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
},
|
|
4957
5031
|
{
|
|
4958
5032
|
name: "list_memories",
|
|
4959
5033
|
description: "List all stored memories",
|
|
@@ -4995,6 +5069,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4995
5069
|
});
|
|
4996
5070
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4997
5071
|
const { name, arguments: args } = request.params;
|
|
5072
|
+
const serverUp = await isServerAvailable();
|
|
5073
|
+
if (!serverUp) {
|
|
5074
|
+
return {
|
|
5075
|
+
content: [
|
|
5076
|
+
{
|
|
5077
|
+
type: "text",
|
|
5078
|
+
text: `Memory server unavailable at ${API_URL}. Please ensure shodh-memory-server is running.
|
|
5079
|
+
|
|
5080
|
+
To start: cd shodh-memory && cargo run`
|
|
5081
|
+
}
|
|
5082
|
+
],
|
|
5083
|
+
isError: true
|
|
5084
|
+
};
|
|
5085
|
+
}
|
|
4998
5086
|
try {
|
|
4999
5087
|
switch (name) {
|
|
5000
5088
|
case "remember": {
|
|
@@ -5035,8 +5123,12 @@ Memory ID: ${result.memory_id}`
|
|
|
5035
5123
|
]
|
|
5036
5124
|
};
|
|
5037
5125
|
}
|
|
5038
|
-
const formatted = memories.map((m, i) =>
|
|
5039
|
-
|
|
5126
|
+
const formatted = memories.map((m, i) => {
|
|
5127
|
+
const content = getContent(m);
|
|
5128
|
+
const score = ((m.score || 0) * 100).toFixed(0);
|
|
5129
|
+
return `${i + 1}. [${score}% match] ${content}
|
|
5130
|
+
Type: ${getType(m)} | ID: ${m.id.slice(0, 8)}...`;
|
|
5131
|
+
}).join(`
|
|
5040
5132
|
|
|
5041
5133
|
`);
|
|
5042
5134
|
return {
|
|
@@ -5050,6 +5142,98 @@ ${formatted}`
|
|
|
5050
5142
|
]
|
|
5051
5143
|
};
|
|
5052
5144
|
}
|
|
5145
|
+
case "context_summary": {
|
|
5146
|
+
const {
|
|
5147
|
+
include_decisions = true,
|
|
5148
|
+
include_learnings = true,
|
|
5149
|
+
include_context = true,
|
|
5150
|
+
max_items = 5
|
|
5151
|
+
} = args;
|
|
5152
|
+
const result = await apiCall("/api/memories", "POST", {
|
|
5153
|
+
user_id: USER_ID
|
|
5154
|
+
});
|
|
5155
|
+
const memories = result.memories || [];
|
|
5156
|
+
if (memories.length === 0) {
|
|
5157
|
+
return {
|
|
5158
|
+
content: [
|
|
5159
|
+
{
|
|
5160
|
+
type: "text",
|
|
5161
|
+
text: "No memories stored yet. Start remembering things to build context!"
|
|
5162
|
+
}
|
|
5163
|
+
]
|
|
5164
|
+
};
|
|
5165
|
+
}
|
|
5166
|
+
const decisions = [];
|
|
5167
|
+
const learnings = [];
|
|
5168
|
+
const context = [];
|
|
5169
|
+
const patterns = [];
|
|
5170
|
+
const errors2 = [];
|
|
5171
|
+
for (const m of memories) {
|
|
5172
|
+
const type = getType(m);
|
|
5173
|
+
switch (type) {
|
|
5174
|
+
case "Decision":
|
|
5175
|
+
decisions.push(m);
|
|
5176
|
+
break;
|
|
5177
|
+
case "Learning":
|
|
5178
|
+
learnings.push(m);
|
|
5179
|
+
break;
|
|
5180
|
+
case "Context":
|
|
5181
|
+
context.push(m);
|
|
5182
|
+
break;
|
|
5183
|
+
case "Pattern":
|
|
5184
|
+
patterns.push(m);
|
|
5185
|
+
break;
|
|
5186
|
+
case "Error":
|
|
5187
|
+
errors2.push(m);
|
|
5188
|
+
break;
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
const sections = [];
|
|
5192
|
+
if (include_context && context.length > 0) {
|
|
5193
|
+
const items = context.slice(0, max_items).map((m) => ` - ${getContent(m).slice(0, 100)}`);
|
|
5194
|
+
sections.push(`PROJECT CONTEXT:
|
|
5195
|
+
${items.join(`
|
|
5196
|
+
`)}`);
|
|
5197
|
+
}
|
|
5198
|
+
if (include_decisions && decisions.length > 0) {
|
|
5199
|
+
const items = decisions.slice(0, max_items).map((m) => ` - ${getContent(m).slice(0, 100)}`);
|
|
5200
|
+
sections.push(`DECISIONS MADE:
|
|
5201
|
+
${items.join(`
|
|
5202
|
+
`)}`);
|
|
5203
|
+
}
|
|
5204
|
+
if (include_learnings && learnings.length > 0) {
|
|
5205
|
+
const items = learnings.slice(0, max_items).map((m) => ` - ${getContent(m).slice(0, 100)}`);
|
|
5206
|
+
sections.push(`LEARNINGS:
|
|
5207
|
+
${items.join(`
|
|
5208
|
+
`)}`);
|
|
5209
|
+
}
|
|
5210
|
+
if (patterns.length > 0) {
|
|
5211
|
+
const items = patterns.slice(0, max_items).map((m) => ` - ${getContent(m).slice(0, 100)}`);
|
|
5212
|
+
sections.push(`PATTERNS NOTICED:
|
|
5213
|
+
${items.join(`
|
|
5214
|
+
`)}`);
|
|
5215
|
+
}
|
|
5216
|
+
if (errors2.length > 0) {
|
|
5217
|
+
const items = errors2.slice(0, Math.min(3, max_items)).map((m) => ` - ${getContent(m).slice(0, 100)}`);
|
|
5218
|
+
sections.push(`ERRORS TO AVOID:
|
|
5219
|
+
${items.join(`
|
|
5220
|
+
`)}`);
|
|
5221
|
+
}
|
|
5222
|
+
const summary = sections.length > 0 ? sections.join(`
|
|
5223
|
+
|
|
5224
|
+
`) : `${memories.length} memories stored, but none categorized as decisions, learnings, or context. Consider using those types when remembering.`;
|
|
5225
|
+
return {
|
|
5226
|
+
content: [
|
|
5227
|
+
{
|
|
5228
|
+
type: "text",
|
|
5229
|
+
text: `CONTEXT SUMMARY (${memories.length} total memories)
|
|
5230
|
+
${"=".repeat(40)}
|
|
5231
|
+
|
|
5232
|
+
${summary}`
|
|
5233
|
+
}
|
|
5234
|
+
]
|
|
5235
|
+
};
|
|
5236
|
+
}
|
|
5053
5237
|
case "list_memories": {
|
|
5054
5238
|
const { limit = 20 } = args;
|
|
5055
5239
|
const result = await apiCall("/api/memories", "POST", {
|
|
@@ -5066,8 +5250,11 @@ ${formatted}`
|
|
|
5066
5250
|
]
|
|
5067
5251
|
};
|
|
5068
5252
|
}
|
|
5069
|
-
const formatted = memories.map((m, i) =>
|
|
5070
|
-
|
|
5253
|
+
const formatted = memories.map((m, i) => {
|
|
5254
|
+
const content = getContent(m);
|
|
5255
|
+
return `${i + 1}. ${content.slice(0, 80)}${content.length > 80 ? "..." : ""}
|
|
5256
|
+
Type: ${getType(m)} | ID: ${m.id.slice(0, 8)}...`;
|
|
5257
|
+
}).join(`
|
|
5071
5258
|
|
|
5072
5259
|
`);
|
|
5073
5260
|
return {
|
|
@@ -5110,11 +5297,26 @@ ${JSON.stringify(result, null, 2)}`
|
|
|
5110
5297
|
}
|
|
5111
5298
|
} catch (error) {
|
|
5112
5299
|
const message = error instanceof Error ? error.message : String(error);
|
|
5300
|
+
let helpText = "";
|
|
5301
|
+
if (message.includes("ECONNREFUSED") || message.includes("fetch failed")) {
|
|
5302
|
+
helpText = `
|
|
5303
|
+
|
|
5304
|
+
The memory server appears to be offline. Start it with:
|
|
5305
|
+
cd shodh-memory && cargo run`;
|
|
5306
|
+
} else if (message.includes("API error 401")) {
|
|
5307
|
+
helpText = `
|
|
5308
|
+
|
|
5309
|
+
Authentication failed. Check your SHODH_API_KEY.`;
|
|
5310
|
+
} else if (message.includes("API error 404")) {
|
|
5311
|
+
helpText = `
|
|
5312
|
+
|
|
5313
|
+
Endpoint not found. The server may be running an older version.`;
|
|
5314
|
+
}
|
|
5113
5315
|
return {
|
|
5114
5316
|
content: [
|
|
5115
5317
|
{
|
|
5116
5318
|
type: "text",
|
|
5117
|
-
text: `Error: ${message}`
|
|
5319
|
+
text: `Error: ${message}${helpText}`
|
|
5118
5320
|
}
|
|
5119
5321
|
],
|
|
5120
5322
|
isError: true
|
|
@@ -5128,12 +5330,15 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
|
5128
5330
|
});
|
|
5129
5331
|
const memories = result.memories || [];
|
|
5130
5332
|
return {
|
|
5131
|
-
resources: memories.slice(0, 50).map((m) =>
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5333
|
+
resources: memories.slice(0, 50).map((m) => {
|
|
5334
|
+
const content = getContent(m);
|
|
5335
|
+
return {
|
|
5336
|
+
uri: `memory://${m.id}`,
|
|
5337
|
+
name: content.slice(0, 50) + (content.length > 50 ? "..." : ""),
|
|
5338
|
+
mimeType: "text/plain",
|
|
5339
|
+
description: `Type: ${getType(m)}`
|
|
5340
|
+
};
|
|
5341
|
+
})
|
|
5137
5342
|
};
|
|
5138
5343
|
} catch {
|
|
5139
5344
|
return { resources: [] };
|
|
@@ -5146,20 +5351,21 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
5146
5351
|
const result = await apiCall("/api/memories", "POST", {
|
|
5147
5352
|
user_id: USER_ID
|
|
5148
5353
|
});
|
|
5149
|
-
const memory = (result.memories || []).find((m) => m.
|
|
5354
|
+
const memory = (result.memories || []).find((m) => m.id === memoryId);
|
|
5150
5355
|
if (!memory) {
|
|
5151
5356
|
throw new Error(`Memory not found: ${memoryId}`);
|
|
5152
5357
|
}
|
|
5358
|
+
const content = getContent(memory);
|
|
5153
5359
|
return {
|
|
5154
5360
|
contents: [
|
|
5155
5361
|
{
|
|
5156
5362
|
uri,
|
|
5157
5363
|
mimeType: "text/plain",
|
|
5158
|
-
text: `Content: ${
|
|
5364
|
+
text: `Content: ${content}
|
|
5159
5365
|
|
|
5160
|
-
Type: ${memory
|
|
5366
|
+
Type: ${getType(memory)}
|
|
5161
5367
|
Created: ${memory.created_at || "unknown"}
|
|
5162
|
-
ID: ${memory.
|
|
5368
|
+
ID: ${memory.id}`
|
|
5163
5369
|
}
|
|
5164
5370
|
]
|
|
5165
5371
|
};
|
|
@@ -5171,6 +5377,8 @@ ID: ${memory.memory_id}`
|
|
|
5171
5377
|
async function main() {
|
|
5172
5378
|
const transport = new StdioServerTransport;
|
|
5173
5379
|
await server.connect(transport);
|
|
5174
|
-
console.error("Shodh-Memory MCP server running");
|
|
5380
|
+
console.error("Shodh-Memory MCP server v0.1.2 running");
|
|
5381
|
+
console.error(`Connecting to: ${API_URL}`);
|
|
5382
|
+
console.error(`User ID: ${USER_ID}`);
|
|
5175
5383
|
}
|
|
5176
5384
|
main().catch(console.error);
|
package/package.json
CHANGED