indra_db_mcp 0.1.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/src/index.ts ADDED
@@ -0,0 +1,529 @@
1
+ /**
2
+ * indra_db MCP Server
3
+ *
4
+ * A Model Context Protocol server that provides tools for managing a
5
+ * content-addressed graph database of thoughts. Perfect for:
6
+ *
7
+ * - Externalizing your reasoning process
8
+ * - Building evolving knowledge graphs
9
+ * - Tracking how understanding changes over time
10
+ * - Creating branching paths of exploration
11
+ * - Finding semantic connections between ideas
12
+ *
13
+ * Think of it as version-controlled thinking - git for your mind.
14
+ */
15
+
16
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
+ import { z } from "zod";
19
+ import { IndraClient } from "./indra-client.js";
20
+ import { EdgeTypes, IndraError } from "./types.js";
21
+
22
+ // ============================================================================
23
+ // Server Setup
24
+ // ============================================================================
25
+
26
+ const server = new McpServer({
27
+ name: "indra_db",
28
+ version: "0.1.0",
29
+ });
30
+
31
+ const client = new IndraClient({
32
+ autoCommit: false, // We'll handle commits explicitly for better control
33
+ });
34
+
35
+ // ============================================================================
36
+ // Helper: Format responses for LLM consumption
37
+ // ============================================================================
38
+
39
+ function formatSuccess(data: unknown, context?: string): { content: Array<{ type: "text"; text: string }> } {
40
+ const response = context
41
+ ? `${context}\n\n${JSON.stringify(data, null, 2)}`
42
+ : JSON.stringify(data, null, 2);
43
+
44
+ return {
45
+ content: [{ type: "text", text: response }],
46
+ };
47
+ }
48
+
49
+ function formatError(error: unknown): { content: Array<{ type: "text"; text: string }>; isError: true } {
50
+ if (error instanceof IndraError) {
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: `Error: ${error.message}\n\nDetails:\n${error.stderr}\n\nCommand: ${error.command.join(" ")}`,
55
+ }],
56
+ isError: true,
57
+ };
58
+ }
59
+
60
+ return {
61
+ content: [{
62
+ type: "text",
63
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
64
+ }],
65
+ isError: true,
66
+ };
67
+ }
68
+
69
+ // ============================================================================
70
+ // THOUGHT TOOLS - Capture and evolve ideas
71
+ // ============================================================================
72
+
73
+ server.tool(
74
+ "remember",
75
+ `🧠 CAPTURE A THOUGHT - Crystallize an idea, insight, or realization into the knowledge graph.
76
+
77
+ Use this when you:
78
+ - Have an insight worth preserving
79
+ - Want to externalize part of your reasoning
80
+ - Need to create a reference point for later
81
+ - Are building understanding incrementally
82
+
83
+ The thought will be embedded for semantic search, allowing you to find it later
84
+ by meaning rather than exact words. Each thought becomes a node that can be
85
+ connected to others, forming a web of understanding.
86
+
87
+ This is how you think out loud - make your reasoning visible and traceable.`,
88
+ {
89
+ content: z.string().describe("The thought to capture - be specific and self-contained"),
90
+ id: z.string().optional().describe("Optional memorable identifier (e.g., 'key-insight-about-X'). Auto-generated if not provided."),
91
+ },
92
+ async ({ content, id }) => {
93
+ try {
94
+ const thought = await client.createThought(content, { id });
95
+ await client.commit(`Remember: ${id || thought.id}`);
96
+ return formatSuccess(thought, `✅ Thought captured and committed. ID: "${thought.id}"`);
97
+ } catch (error) {
98
+ return formatError(error);
99
+ }
100
+ }
101
+ );
102
+
103
+ server.tool(
104
+ "recall",
105
+ `🔍 RETRIEVE A THOUGHT - Fetch a specific thought by its identifier.
106
+
107
+ Use this when you:
108
+ - Need to review a previous insight
109
+ - Want to check what you recorded earlier
110
+ - Are building on a specific prior thought
111
+ - Need exact content for a connection
112
+
113
+ Returns the full thought including its content and metadata.`,
114
+ {
115
+ id: z.string().describe("The identifier of the thought to retrieve"),
116
+ },
117
+ async ({ id }) => {
118
+ try {
119
+ const thought = await client.getThought(id);
120
+ return formatSuccess(thought, `📖 Retrieved thought "${id}":`);
121
+ } catch (error) {
122
+ return formatError(error);
123
+ }
124
+ }
125
+ );
126
+
127
+ server.tool(
128
+ "revise",
129
+ `✏️ REVISE A THOUGHT - Update your understanding while preserving history.
130
+
131
+ Use this when:
132
+ - Your understanding has evolved
133
+ - You need to correct or refine an idea
134
+ - New information changes a previous insight
135
+ - You want to improve how something is expressed
136
+
137
+ Unlike editing a document, this creates a new version. The old understanding
138
+ is preserved in history - you can always see how your thinking evolved.
139
+ This is the heart of versioned thinking.`,
140
+ {
141
+ id: z.string().describe("The thought to revise"),
142
+ content: z.string().describe("The new, revised content"),
143
+ },
144
+ async ({ id, content }) => {
145
+ try {
146
+ const thought = await client.updateThought(id, content);
147
+ await client.commit(`Revise: ${id}`);
148
+ return formatSuccess(thought, `✅ Thought revised. The previous version is preserved in history.`);
149
+ } catch (error) {
150
+ return formatError(error);
151
+ }
152
+ }
153
+ );
154
+
155
+ server.tool(
156
+ "forget",
157
+ `🗑️ FORGET A THOUGHT - Remove a thought from the current state.
158
+
159
+ Use sparingly. The thought remains in history and can be recovered by:
160
+ - Viewing commit history
161
+ - Branching from a previous state
162
+ - Using diff to see what was removed
163
+
164
+ This isn't true deletion - it's more like archiving. Version control means
165
+ nothing is ever truly lost.`,
166
+ {
167
+ id: z.string().describe("The thought to forget"),
168
+ },
169
+ async ({ id }) => {
170
+ try {
171
+ await client.deleteThought(id);
172
+ await client.commit(`Forget: ${id}`);
173
+ return formatSuccess({ forgotten: id }, `✅ Thought "${id}" removed from current state. It remains in history.`);
174
+ } catch (error) {
175
+ return formatError(error);
176
+ }
177
+ }
178
+ );
179
+
180
+ server.tool(
181
+ "list_thoughts",
182
+ `📋 LIST ALL THOUGHTS - See everything currently in the knowledge graph.
183
+
184
+ Use this to:
185
+ - Get an overview of what's been captured
186
+ - Find thoughts to connect
187
+ - Review the current state of understanding
188
+ - Plan what connections to make
189
+
190
+ Returns all thoughts with their IDs and content.`,
191
+ {},
192
+ async () => {
193
+ try {
194
+ const result = await client.listThoughts();
195
+ return formatSuccess(result, `📋 ${result.count} thought(s) in the knowledge graph:`);
196
+ } catch (error) {
197
+ return formatError(error);
198
+ }
199
+ }
200
+ );
201
+
202
+ // ============================================================================
203
+ // RELATIONSHIP TOOLS - Build the web of understanding
204
+ // ============================================================================
205
+
206
+ server.tool(
207
+ "connect",
208
+ `🔗 CONNECT THOUGHTS - Create a typed relationship between two ideas.
209
+
210
+ This is where the graph comes alive. Connections reveal structure in your thinking.
211
+
212
+ Relationship types (use what fits, or create your own):
213
+ - "supports" → This thought provides evidence for another
214
+ - "contradicts" → This thought conflicts with another
215
+ - "derives_from" → This thought evolved from another
216
+ - "part_of" → This thought is a component of a larger idea
217
+ - "causes" → This thought leads to another
218
+ - "precedes" → This thought comes before another temporally
219
+ - "similar_to" → These thoughts express related ideas
220
+ - "relates_to" → General connection (when type is unclear)
221
+
222
+ The web of connections IS your understanding made visible.`,
223
+ {
224
+ from: z.string().describe("Source thought ID - the starting point of the relationship"),
225
+ to: z.string().describe("Target thought ID - what the source connects to"),
226
+ relationship: z.string().default("relates_to").describe("Type of relationship (see description for built-in types)"),
227
+ strength: z.number().min(0).max(1).optional().describe("Optional weight 0.0-1.0 indicating relationship strength"),
228
+ },
229
+ async ({ from, to, relationship, strength }) => {
230
+ try {
231
+ const edge = await client.relate(from, to, relationship, { weight: strength });
232
+ await client.commit(`Connect: ${from} --[${relationship}]--> ${to}`);
233
+ return formatSuccess(edge, `✅ Connected: "${from}" --[${relationship}]--> "${to}"`);
234
+ } catch (error) {
235
+ return formatError(error);
236
+ }
237
+ }
238
+ );
239
+
240
+ server.tool(
241
+ "disconnect",
242
+ `✂️ DISCONNECT THOUGHTS - Remove a relationship between thoughts.
243
+
244
+ Use when:
245
+ - A connection no longer makes sense
246
+ - You're restructuring your understanding
247
+ - A relationship was created in error
248
+
249
+ The thoughts themselves remain - only the connection is removed.`,
250
+ {
251
+ from: z.string().describe("Source thought ID"),
252
+ to: z.string().describe("Target thought ID"),
253
+ relationship: z.string().optional().describe("Specific relationship type to remove (removes all if not specified)"),
254
+ },
255
+ async ({ from, to, relationship }) => {
256
+ try {
257
+ await client.unrelate(from, to, relationship);
258
+ await client.commit(`Disconnect: ${from} from ${to}`);
259
+ return formatSuccess(
260
+ { disconnected: { from, to, relationship } },
261
+ `✅ Disconnected "${from}" from "${to}"`
262
+ );
263
+ } catch (error) {
264
+ return formatError(error);
265
+ }
266
+ }
267
+ );
268
+
269
+ server.tool(
270
+ "explore",
271
+ `🌐 EXPLORE CONNECTIONS - See what's connected to a thought.
272
+
273
+ This is how you traverse the knowledge graph. From any thought, see:
274
+ - What it connects TO (outgoing)
275
+ - What connects to IT (incoming)
276
+ - Or both directions
277
+
278
+ Each neighbor comes with the edge that connects them, showing the
279
+ relationship type and strength. Use this to follow chains of reasoning,
280
+ find related concepts, or understand context.`,
281
+ {
282
+ thought_id: z.string().describe("The thought to explore from"),
283
+ direction: z.enum(["outgoing", "incoming", "both"]).default("both")
284
+ .describe("Which connections to follow"),
285
+ },
286
+ async ({ thought_id, direction }) => {
287
+ try {
288
+ const result = await client.getNeighbors(thought_id, direction);
289
+ const directionEmoji = direction === "outgoing" ? "→" : direction === "incoming" ? "←" : "↔";
290
+ return formatSuccess(result, `🌐 Connections from "${thought_id}" (${directionEmoji} ${direction}):`);
291
+ } catch (error) {
292
+ return formatError(error);
293
+ }
294
+ }
295
+ );
296
+
297
+ // ============================================================================
298
+ // SEARCH TOOLS - Find by meaning
299
+ // ============================================================================
300
+
301
+ server.tool(
302
+ "search",
303
+ `🔮 SEMANTIC SEARCH - Find thoughts by meaning, not just keywords.
304
+
305
+ This is powerful: describe what you're looking for conceptually, and find
306
+ thoughts that match semantically. The embeddings capture meaning, so:
307
+
308
+ - "initial hypothesis" might find "my first theory about X"
309
+ - "things that went wrong" might find "problems encountered"
310
+ - "key decisions" might find "we chose to..."
311
+
312
+ Use this to:
313
+ - Rediscover relevant prior thinking
314
+ - Find thoughts to connect
315
+ - Check if you've already captured something similar
316
+ - Surface related ideas you may have forgotten
317
+
318
+ Higher scores = more semantically similar.`,
319
+ {
320
+ query: z.string().describe("What you're looking for - describe the meaning/concept"),
321
+ limit: z.number().min(1).max(100).default(10).describe("Maximum results to return"),
322
+ },
323
+ async ({ query, limit }) => {
324
+ try {
325
+ const result = await client.search(query, limit);
326
+ return formatSuccess(result, `🔮 Found ${result.count} thought(s) matching "${query}":`);
327
+ } catch (error) {
328
+ return formatError(error);
329
+ }
330
+ }
331
+ );
332
+
333
+ // ============================================================================
334
+ // VERSION CONTROL TOOLS - Track the evolution of understanding
335
+ // ============================================================================
336
+
337
+ server.tool(
338
+ "checkpoint",
339
+ `💾 CHECKPOINT - Commit current state with a meaningful message.
340
+
341
+ Like git commit, but for thoughts. Creates a snapshot you can return to.
342
+
343
+ Good checkpoint messages describe WHY, not just what:
344
+ - "Completed initial analysis of problem space"
345
+ - "Refined hypothesis after finding contradicting evidence"
346
+ - "Branching to explore alternative approach"
347
+
348
+ Checkpoints let you see how understanding evolved over time.`,
349
+ {
350
+ message: z.string().describe("What this checkpoint represents - focus on the WHY"),
351
+ },
352
+ async ({ message }) => {
353
+ try {
354
+ const result = await client.commit(message);
355
+ return formatSuccess(result, `💾 Checkpoint created: "${message}"`);
356
+ } catch (error) {
357
+ return formatError(error);
358
+ }
359
+ }
360
+ );
361
+
362
+ server.tool(
363
+ "history",
364
+ `📜 VIEW HISTORY - See how understanding has evolved.
365
+
366
+ Returns the commit log showing each checkpoint. This is the trajectory
367
+ of your thinking - not just where you are, but how you got here.
368
+
369
+ Use this to:
370
+ - Review the evolution of understanding
371
+ - Find a point to branch from
372
+ - Understand context of current state
373
+ - Track decision points`,
374
+ {
375
+ limit: z.number().min(1).max(100).optional().describe("Maximum commits to show"),
376
+ },
377
+ async ({ limit }) => {
378
+ try {
379
+ const result = await client.log(limit);
380
+ return formatSuccess(result, `📜 Commit history for branch "${result.branch}":`);
381
+ } catch (error) {
382
+ return formatError(error);
383
+ }
384
+ }
385
+ );
386
+
387
+ server.tool(
388
+ "branch",
389
+ `🌿 CREATE BRANCH - Start a new line of exploration.
390
+
391
+ Branches let you explore alternatives without losing your main line of thought.
392
+ Like git branches, they're cheap and fast.
393
+
394
+ Use this when:
395
+ - You want to explore a "what if" scenario
396
+ - Testing a hypothesis that might not pan out
397
+ - Trying an alternative approach
398
+ - Saving current state before major changes
399
+
400
+ You can always come back to main, or merge insights later.`,
401
+ {
402
+ name: z.string().describe("Name for the new branch (e.g., 'explore-alternative', 'hypothesis-b')"),
403
+ },
404
+ async ({ name }) => {
405
+ try {
406
+ const branch = await client.createBranch(name);
407
+ return formatSuccess(branch, `🌿 Branch "${name}" created. Use 'switch_branch' to explore it.`);
408
+ } catch (error) {
409
+ return formatError(error);
410
+ }
411
+ }
412
+ );
413
+
414
+ server.tool(
415
+ "switch_branch",
416
+ `🔀 SWITCH BRANCH - Move to a different line of thinking.
417
+
418
+ Changes which branch you're working on. All thoughts and connections
419
+ reflect that branch's state.
420
+
421
+ Use this to:
422
+ - Return to main after exploring
423
+ - Switch between different approaches
424
+ - Compare different lines of reasoning`,
425
+ {
426
+ name: z.string().describe("Branch name to switch to"),
427
+ },
428
+ async ({ name }) => {
429
+ try {
430
+ await client.checkout(name);
431
+ return formatSuccess({ branch: name }, `🔀 Switched to branch "${name}"`);
432
+ } catch (error) {
433
+ return formatError(error);
434
+ }
435
+ }
436
+ );
437
+
438
+ server.tool(
439
+ "list_branches",
440
+ `🌳 LIST BRANCHES - See all lines of exploration.
441
+
442
+ Shows all branches and which one is currently active.
443
+ Each branch is an independent line of thought that can evolve separately.`,
444
+ {},
445
+ async () => {
446
+ try {
447
+ const result = await client.listBranches();
448
+ return formatSuccess(result, `🌳 Branches (current: "${result.current}"):`);
449
+ } catch (error) {
450
+ return formatError(error);
451
+ }
452
+ }
453
+ );
454
+
455
+ server.tool(
456
+ "compare",
457
+ `🔍 COMPARE - See what changed between states.
458
+
459
+ Shows differences between commits or branches:
460
+ - Thoughts added, removed, modified
461
+ - Connections added or removed
462
+
463
+ Use this to:
464
+ - Understand how thinking evolved
465
+ - See what a branch explored
466
+ - Review changes before merging ideas`,
467
+ {
468
+ from: z.string().optional().describe("Starting commit/branch (defaults to parent)"),
469
+ to: z.string().optional().describe("Ending commit/branch (defaults to HEAD)"),
470
+ },
471
+ async ({ from, to }) => {
472
+ try {
473
+ const result = await client.diff(from, to);
474
+ return formatSuccess(result, `🔍 Diff${from ? ` from ${from}` : ""}${to ? ` to ${to}` : ""}:`);
475
+ } catch (error) {
476
+ return formatError(error);
477
+ }
478
+ }
479
+ );
480
+
481
+ server.tool(
482
+ "status",
483
+ `📊 STATUS - Get current database state overview.
484
+
485
+ Shows:
486
+ - Current branch
487
+ - Number of thoughts and connections
488
+ - Uncommitted changes
489
+ - Database location
490
+
491
+ Use this to orient yourself - where am I in the knowledge graph?`,
492
+ {},
493
+ async () => {
494
+ try {
495
+ const result = await client.status();
496
+ return formatSuccess(result, `📊 Database status:`);
497
+ } catch (error) {
498
+ return formatError(error);
499
+ }
500
+ }
501
+ );
502
+
503
+ // ============================================================================
504
+ // Server Startup
505
+ // ============================================================================
506
+
507
+ async function main() {
508
+ const transport = new StdioServerTransport();
509
+
510
+ console.error(`[indra_db_mcp] Starting server...`);
511
+ console.error(`[indra_db_mcp] Database path: ${client.getDatabasePath()}`);
512
+
513
+ // Initialize the client (ensures binary exists, creates DB if needed)
514
+ try {
515
+ await client.init();
516
+ console.error(`[indra_db_mcp] Database initialized successfully`);
517
+ } catch (error) {
518
+ console.error(`[indra_db_mcp] Warning: ${error}`);
519
+ // Continue anyway - errors will be reported when tools are called
520
+ }
521
+
522
+ await server.connect(transport);
523
+ console.error(`[indra_db_mcp] Server connected and ready`);
524
+ }
525
+
526
+ main().catch((error) => {
527
+ console.error(`[indra_db_mcp] Fatal error:`, error);
528
+ process.exit(1);
529
+ });