shabti 2.0.0 → 2.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/README.md CHANGED
@@ -117,6 +117,53 @@ you> /help
117
117
 
118
118
  Requires `OPENAI_API_KEY` in `.env` for chat functionality. Memory commands (`/remember`, `/recall`) work with the local Qdrant engine.
119
119
 
120
+ ## MCP Server
121
+
122
+ shabti includes an MCP (Model Context Protocol) server for integration with Claude Code, Cursor, and other MCP-compatible tools.
123
+
124
+ ```bash
125
+ # Start the MCP server (stdio transport)
126
+ npx shabti-mcp
127
+ ```
128
+
129
+ ### Claude Code configuration
130
+
131
+ Generate the MCP settings JSON:
132
+
133
+ ```bash
134
+ shabti mcp-config
135
+ ```
136
+
137
+ Or manually add to your MCP settings:
138
+
139
+ ```json
140
+ {
141
+ "mcpServers": {
142
+ "shabti-memory": {
143
+ "command": "npx",
144
+ "args": ["shabti-mcp"]
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### Available tools
151
+
152
+ | Tool | Description |
153
+ | --------------- | -------------------------------------- |
154
+ | `memory_store` | Store a memory entry |
155
+ | `memory_search` | Search memories by semantic similarity |
156
+ | `memory_delete` | Delete a memory entry by ID |
157
+ | `memory_list` | List recent memory entries |
158
+ | `memory_status` | Get engine status |
159
+
160
+ ### Available resources
161
+
162
+ | URI | Description |
163
+ | ----------------- | ---------------------------- |
164
+ | `shabti://status` | Engine status and statistics |
165
+ | `shabti://config` | Current configuration |
166
+
120
167
  ## Node.js API
121
168
 
122
169
  ```javascript
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "shabti",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Agent Memory OS — semantic memory for AI agents",
5
5
  "type": "module",
6
6
  "main": "native.cjs",
7
7
  "types": "native.d.ts",
8
8
  "bin": {
9
- "shabti": "src/index.js"
9
+ "shabti": "src/index.js",
10
+ "shabti-mcp": "src/mcp/server.js"
10
11
  },
11
12
  "files": [
12
13
  "src/",
@@ -42,6 +43,7 @@
42
43
  "vector-database",
43
44
  "qdrant",
44
45
  "embeddings",
46
+ "mcp",
45
47
  "cli"
46
48
  ],
47
49
  "author": "Kaga Hinata",
package/src/index.js CHANGED
@@ -78,6 +78,22 @@ function buildProgram() {
78
78
  registerSpin(program);
79
79
  registerStatus(program);
80
80
  registerStore(program);
81
+
82
+ program
83
+ .command("mcp-config")
84
+ .description("Print MCP server configuration JSON for Claude Code / Cursor")
85
+ .action(() => {
86
+ const config = {
87
+ mcpServers: {
88
+ "shabti-memory": {
89
+ command: "npx",
90
+ args: ["shabti-mcp"],
91
+ },
92
+ },
93
+ };
94
+ console.log(JSON.stringify(config, null, 2));
95
+ });
96
+
81
97
  return program;
82
98
  }
83
99
 
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "readline";
3
+ import { createEngine, loadConfig } from "../core/engine.js";
4
+
5
+ const SERVER_INFO = {
6
+ name: "shabti-memory",
7
+ version: "2.0.0",
8
+ };
9
+
10
+ const TOOLS = [
11
+ {
12
+ name: "memory_store",
13
+ description: "Store a memory entry in shabti",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ content: { type: "string", description: "The text content to store" },
18
+ namespace: {
19
+ type: "string",
20
+ description: "Target namespace (default: 'default')",
21
+ },
22
+ tags: {
23
+ type: "array",
24
+ items: { type: "string" },
25
+ description: "Tags to associate with the memory",
26
+ },
27
+ },
28
+ required: ["content"],
29
+ },
30
+ },
31
+ {
32
+ name: "memory_search",
33
+ description: "Search stored memories by semantic similarity",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ query: { type: "string", description: "Search query text" },
38
+ limit: {
39
+ type: "integer",
40
+ description: "Maximum number of results (default: 10)",
41
+ },
42
+ namespace: { type: "string", description: "Namespace to search within" },
43
+ },
44
+ required: ["query"],
45
+ },
46
+ },
47
+ {
48
+ name: "memory_delete",
49
+ description: "Delete a memory entry by ID",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ id: { type: "string", description: "UUID of the memory entry to delete" },
54
+ },
55
+ required: ["id"],
56
+ },
57
+ },
58
+ {
59
+ name: "memory_list",
60
+ description: "List recent memory entries",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ limit: {
65
+ type: "integer",
66
+ description: "Maximum number of entries to return (default: 10)",
67
+ },
68
+ namespace: { type: "string", description: "Filter by namespace" },
69
+ },
70
+ },
71
+ },
72
+ {
73
+ name: "memory_status",
74
+ description: "Get the current status of the memory engine",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {},
78
+ },
79
+ },
80
+ ];
81
+
82
+ const RESOURCES = [
83
+ {
84
+ uri: "shabti://status",
85
+ name: "Engine status",
86
+ description: "Current shabti engine status and statistics",
87
+ mimeType: "application/json",
88
+ },
89
+ {
90
+ uri: "shabti://config",
91
+ name: "Engine configuration",
92
+ description: "Current engine configuration",
93
+ mimeType: "application/json",
94
+ },
95
+ ];
96
+
97
+ let engine = null;
98
+ let engineInitAttempted = false;
99
+
100
+ function initEngine() {
101
+ if (engine) return engine;
102
+ if (engineInitAttempted) return null;
103
+ engineInitAttempted = true;
104
+ try {
105
+ engine = createEngine();
106
+ } catch {
107
+ // engine stays null — tools will return errors
108
+ }
109
+ return engine;
110
+ }
111
+
112
+ function respond(id, result) {
113
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, result });
114
+ process.stdout.write(msg + "\n");
115
+ }
116
+
117
+ function respondError(id, code, message) {
118
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
119
+ process.stdout.write(msg + "\n");
120
+ }
121
+
122
+ function handleInitialize(id) {
123
+ respond(id, {
124
+ protocolVersion: "2024-11-05",
125
+ serverInfo: SERVER_INFO,
126
+ capabilities: {
127
+ tools: {},
128
+ resources: {},
129
+ },
130
+ });
131
+ }
132
+
133
+ function handleToolsList(id) {
134
+ respond(id, { tools: TOOLS });
135
+ }
136
+
137
+ function handleResourcesList(id) {
138
+ respond(id, { resources: RESOURCES });
139
+ }
140
+
141
+ async function handleToolsCall(id, params) {
142
+ const { name, arguments: args } = params;
143
+
144
+ const eng = initEngine();
145
+
146
+ if (name === "memory_status") {
147
+ if (!eng) {
148
+ return respond(id, {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: JSON.stringify(
153
+ { status: "unavailable", entry_count: 0, model_id: "unknown" },
154
+ null,
155
+ 2,
156
+ ),
157
+ },
158
+ ],
159
+ });
160
+ }
161
+ const status = eng.status();
162
+ return respond(id, {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: JSON.stringify(
167
+ {
168
+ status: "ok",
169
+ entry_count: status.entryCount,
170
+ tier: status.tier,
171
+ model_id: status.modelId,
172
+ qdrant_url: status.qdrantUrl,
173
+ },
174
+ null,
175
+ 2,
176
+ ),
177
+ },
178
+ ],
179
+ });
180
+ }
181
+
182
+ if (name === "memory_store") {
183
+ if (!eng) {
184
+ return respondError(id, -32603, "Engine not available");
185
+ }
186
+ const content = args?.content;
187
+ if (!content) {
188
+ return respondError(id, -32602, "Missing required parameter: content");
189
+ }
190
+ try {
191
+ const opts = {};
192
+ if (args.namespace) opts.namespace = args.namespace;
193
+ if (args.tags) opts.tags = args.tags;
194
+ const result = await eng.store(content, opts);
195
+ return respond(id, {
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: JSON.stringify(
200
+ { status: result.status, id: result.id || result.existingId },
201
+ null,
202
+ 2,
203
+ ),
204
+ },
205
+ ],
206
+ });
207
+ } catch (err) {
208
+ return respondError(id, -32603, err.message);
209
+ }
210
+ }
211
+
212
+ if (name === "memory_search") {
213
+ if (!eng) {
214
+ return respondError(id, -32603, "Engine not available");
215
+ }
216
+ const query = args?.query;
217
+ if (!query) {
218
+ return respondError(id, -32602, "Missing required parameter: query");
219
+ }
220
+ try {
221
+ const limit = args.limit || 10;
222
+ const queryObj = { text: query, limit };
223
+ if (args.namespace) queryObj.namespace = args.namespace;
224
+ const results = await eng.executeQuery(queryObj);
225
+ const formatted = results.map((r) => ({
226
+ content: r.content,
227
+ score: r.score,
228
+ id: r.id,
229
+ namespace: r.namespace,
230
+ }));
231
+ return respond(id, {
232
+ content: [
233
+ {
234
+ type: "text",
235
+ text: JSON.stringify({ query, results: formatted }, null, 2),
236
+ },
237
+ ],
238
+ });
239
+ } catch (err) {
240
+ return respondError(id, -32603, err.message);
241
+ }
242
+ }
243
+
244
+ if (name === "memory_delete") {
245
+ if (!eng) {
246
+ return respondError(id, -32603, "Engine not available");
247
+ }
248
+ const entryId = args?.id;
249
+ if (!entryId) {
250
+ return respondError(id, -32602, "Missing required parameter: id");
251
+ }
252
+ try {
253
+ await eng.delete(entryId);
254
+ return respond(id, {
255
+ content: [
256
+ {
257
+ type: "text",
258
+ text: JSON.stringify({ deleted: true, id: entryId }, null, 2),
259
+ },
260
+ ],
261
+ });
262
+ } catch (err) {
263
+ return respondError(id, -32603, err.message);
264
+ }
265
+ }
266
+
267
+ if (name === "memory_list") {
268
+ if (!eng) {
269
+ return respondError(id, -32603, "Engine not available");
270
+ }
271
+ try {
272
+ const limit = args?.limit || 10;
273
+ const queryObj = { text: "*", limit };
274
+ if (args?.namespace) queryObj.namespace = args.namespace;
275
+ const results = await eng.executeQuery(queryObj);
276
+ const entries = results.map((r) => ({
277
+ id: r.id,
278
+ content: r.content,
279
+ score: r.score,
280
+ namespace: r.namespace,
281
+ createdAt: r.createdAt,
282
+ }));
283
+ return respond(id, {
284
+ content: [
285
+ {
286
+ type: "text",
287
+ text: JSON.stringify({ entries, count: entries.length }, null, 2),
288
+ },
289
+ ],
290
+ });
291
+ } catch (err) {
292
+ return respondError(id, -32603, err.message);
293
+ }
294
+ }
295
+
296
+ respondError(id, -32601, `Unknown tool: ${name}`);
297
+ }
298
+
299
+ function handleResourcesRead(id, params) {
300
+ const { uri } = params;
301
+ const eng = initEngine();
302
+
303
+ if (uri === "shabti://status") {
304
+ if (!eng) {
305
+ return respond(id, {
306
+ contents: [
307
+ {
308
+ uri,
309
+ mimeType: "application/json",
310
+ text: JSON.stringify({ status: "unavailable", entry_count: 0 }),
311
+ },
312
+ ],
313
+ });
314
+ }
315
+ const status = eng.status();
316
+ return respond(id, {
317
+ contents: [
318
+ {
319
+ uri,
320
+ mimeType: "application/json",
321
+ text: JSON.stringify({
322
+ status: "ok",
323
+ entry_count: status.entryCount,
324
+ tier: status.tier,
325
+ model_id: status.modelId,
326
+ }),
327
+ },
328
+ ],
329
+ });
330
+ }
331
+
332
+ if (uri === "shabti://config") {
333
+ const config = loadConfig();
334
+ return respond(id, {
335
+ contents: [
336
+ {
337
+ uri,
338
+ mimeType: "application/json",
339
+ text: JSON.stringify(config),
340
+ },
341
+ ],
342
+ });
343
+ }
344
+
345
+ respondError(id, -32602, `Unknown resource: ${uri}`);
346
+ }
347
+
348
+ async function handleRequest(line) {
349
+ let req;
350
+ try {
351
+ req = JSON.parse(line);
352
+ } catch {
353
+ return respondError(null, -32700, "Parse error");
354
+ }
355
+
356
+ const { id, method, params } = req;
357
+
358
+ switch (method) {
359
+ case "initialize":
360
+ return handleInitialize(id);
361
+ case "notifications/initialized":
362
+ return; // no response needed for notifications
363
+ case "tools/list":
364
+ return handleToolsList(id);
365
+ case "tools/call":
366
+ return handleToolsCall(id, params || {});
367
+ case "resources/list":
368
+ return handleResourcesList(id);
369
+ case "resources/read":
370
+ return handleResourcesRead(id, params || {});
371
+ default:
372
+ return respondError(id, -32601, "Method not found");
373
+ }
374
+ }
375
+
376
+ // stdio transport: read newline-delimited JSON from stdin
377
+ const rl = createInterface({ input: process.stdin, terminal: false });
378
+ rl.on("line", (line) => {
379
+ const trimmed = line.trim();
380
+ if (trimmed) handleRequest(trimmed);
381
+ });
382
+ rl.on("close", () => process.exit(0));