indra_db_mcp 0.1.24 → 0.2.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 +113 -172
- package/package.json +1 -1
- package/src/e2e.test.ts +46 -0
- package/src/index.ts +252 -130
- package/src/indra-client.ts +21 -4
package/README.md
CHANGED
|
@@ -1,190 +1,163 @@
|
|
|
1
1
|
# indra_db_mcp
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **Persistent memory for AI reasoning and decisions.**
|
|
4
4
|
|
|
5
|
-
An MCP
|
|
5
|
+
An MCP server that gives AI agents memory that persists across sessions. Built on [indra_db](https://github.com/moonstripe/indra_db) — a git-like database for versioned thoughts.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## The Problem
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
AI agents start fresh every session. Yesterday's insights evaporate. Decisions get re-made. Reasoning chains vanish.
|
|
10
10
|
|
|
11
|
-
**indra_db_mcp** changes that by giving
|
|
11
|
+
**indra_db_mcp** changes that by giving agents:
|
|
12
12
|
|
|
13
|
-
- 🧠 **
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- 📜 **Track evolution** of understanding over time
|
|
18
|
-
|
|
19
|
-
It's git for thoughts. Version-controlled thinking. A knowledge graph that grows with every conversation.
|
|
13
|
+
- 🧠 **Persistent memory** — Record reasoning that survives session boundaries
|
|
14
|
+
- 🔍 **Semantic search** — Find past decisions by meaning, not keywords
|
|
15
|
+
- 🌿 **Branching** — Explore alternatives without losing the main thread
|
|
16
|
+
- 📜 **History** — See how understanding evolved over time
|
|
20
17
|
|
|
21
18
|
## Quick Start
|
|
22
19
|
|
|
23
|
-
###
|
|
20
|
+
### Install
|
|
21
|
+
|
|
22
|
+
No installation required — use `bunx` for automatic updates:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bunx -y indra_db_mcp@latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install globally:
|
|
29
|
+
```bash
|
|
30
|
+
bun add -g indra_db_mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Configure Your Agent
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
- [Rust/Cargo](https://rustup.rs/) (for auto-installing indra_db CLI)
|
|
35
|
+
**Claude Code** — Add to your project's `CLAUDE.md`:
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
```markdown
|
|
38
|
+
@import node_modules/indra_db_mcp/INDRA_INSTRUCTIONS.md
|
|
39
|
+
```
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
**Claude Desktop** — Add to `claude_desktop_config.json`:
|
|
31
42
|
|
|
32
43
|
```json
|
|
33
44
|
{
|
|
34
45
|
"mcpServers": {
|
|
35
46
|
"indra": {
|
|
36
|
-
"command":
|
|
37
|
-
"
|
|
47
|
+
"command": "bunx",
|
|
48
|
+
"args": ["-y", "indra_db_mcp@latest"]
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
52
|
```
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
**OpenCode** — Add to `~/.config/opencode/opencode.json`:
|
|
44
55
|
|
|
45
|
-
Models won't automatically use Indra unless instructed. Add the bundled instructions file to your config:
|
|
46
|
-
|
|
47
|
-
**OpenCode** (`~/.config/opencode/opencode.json` or project `opencode.json`):
|
|
48
56
|
```json
|
|
49
57
|
{
|
|
50
|
-
"
|
|
58
|
+
"mcp": {
|
|
59
|
+
"indra": {
|
|
60
|
+
"command": ["bunx", "-y", "indra_db_mcp@latest"],
|
|
61
|
+
"type": "local"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"instructions": ["~/.config/opencode/instructions/indra.md"]
|
|
51
65
|
}
|
|
52
66
|
```
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
Then copy INDRA_INSTRUCTIONS.md:
|
|
69
|
+
```bash
|
|
70
|
+
mkdir -p ~/.config/opencode/instructions
|
|
71
|
+
curl -o ~/.config/opencode/instructions/indra.md \
|
|
72
|
+
https://raw.githubusercontent.com/moonstripe/indra_db_mcp/main/INDRA_INSTRUCTIONS.md
|
|
58
73
|
```
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Or with a custom database path:
|
|
75
|
+
**Generic MCP Client:**
|
|
63
76
|
|
|
64
77
|
```json
|
|
65
78
|
{
|
|
66
79
|
"mcpServers": {
|
|
67
80
|
"indra": {
|
|
68
|
-
"command":
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
"command": "bunx",
|
|
82
|
+
"args": ["-y", "indra_db_mcp@latest"],
|
|
83
|
+
"env": {
|
|
84
|
+
"INDRA_DB_PATH": "./.indra"
|
|
85
|
+
}
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
```
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
# Install globally
|
|
82
|
-
bun add -g indra_db_mcp
|
|
83
|
-
|
|
84
|
-
# Or clone and run locally
|
|
85
|
-
git clone https://github.com/moonstripe/indra_db_mcp
|
|
86
|
-
cd indra_db_mcp
|
|
87
|
-
bun install
|
|
88
|
-
bun start
|
|
89
|
-
|
|
90
|
-
# The indra CLI will auto-install on first run via cargo
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Environment Variables
|
|
94
|
-
|
|
95
|
-
| Variable | Description | Default |
|
|
96
|
-
|----------|-------------|---------|
|
|
97
|
-
| `INDRA_DB_PATH` | Path to database file | `./.indra` (hidden file) |
|
|
98
|
-
|
|
99
|
-
When `INDRA_DB_PATH` is set, uses that path (supports `~` for home directory).
|
|
100
|
-
When unset, creates a hidden `.indra` file in the current working directory.
|
|
101
|
-
|
|
102
|
-
## Available Tools
|
|
91
|
+
## Tools
|
|
103
92
|
|
|
104
|
-
|
|
93
|
+
| Tool | Purpose |
|
|
94
|
+
|------|---------|
|
|
95
|
+
| `indra_remember` | Record reasoning, decisions, and insights |
|
|
96
|
+
| `indra_search` | Find past reasoning by meaning (or `"*"` for all) |
|
|
97
|
+
| `indra_status` | Check current branch and entry count |
|
|
98
|
+
| `indra_branch` | Create, switch, or list branches |
|
|
99
|
+
| `indra_experiment` | Quick sandbox for exploring alternatives |
|
|
100
|
+
| `indra_history` | See how reasoning evolved |
|
|
101
|
+
| `indra_diff` | Compare two points in history |
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|------|-------------|
|
|
108
|
-
| `remember` | Capture a thought with optional ID. Embeddings auto-generated for semantic search. |
|
|
109
|
-
| `recall` | Retrieve a specific thought by ID. |
|
|
110
|
-
| `revise` | Update a thought while preserving history. |
|
|
111
|
-
| `forget` | Remove from current state (preserved in history). |
|
|
112
|
-
| `list_thoughts` | See all thoughts in the graph. |
|
|
103
|
+
## Example Usage
|
|
113
104
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
| Tool | Description |
|
|
117
|
-
|------|-------------|
|
|
118
|
-
| `connect` | Create typed relationship between thoughts. |
|
|
119
|
-
| `disconnect` | Remove a relationship. |
|
|
120
|
-
| `explore` | Traverse connections from a thought. |
|
|
121
|
-
|
|
122
|
-
**Built-in relationship types:**
|
|
123
|
-
- `supports` — evidence/backing
|
|
124
|
-
- `contradicts` — conflicts with
|
|
125
|
-
- `derives_from` — evolved from
|
|
126
|
-
- `part_of` — component of larger idea
|
|
127
|
-
- `causes` — leads to
|
|
128
|
-
- `precedes` — temporal ordering
|
|
129
|
-
- `similar_to` — related concepts
|
|
130
|
-
- `relates_to` — general connection
|
|
131
|
-
|
|
132
|
-
### 🔮 Semantic Search
|
|
133
|
-
|
|
134
|
-
| Tool | Description |
|
|
135
|
-
|------|-------------|
|
|
136
|
-
| `search` | Find thoughts by meaning using vector embeddings. |
|
|
137
|
-
|
|
138
|
-
### 📜 Version Control
|
|
139
|
-
|
|
140
|
-
| Tool | Description |
|
|
141
|
-
|------|-------------|
|
|
142
|
-
| `checkpoint` | Commit current state with a message. |
|
|
143
|
-
| `history` | View commit log showing evolution. |
|
|
144
|
-
| `branch` | Create new line of exploration. |
|
|
145
|
-
| `switch_branch` | Move between branches. |
|
|
146
|
-
| `list_branches` | See all branches. |
|
|
147
|
-
| `compare` | Diff between states. |
|
|
148
|
-
| `status` | Current database overview. |
|
|
149
|
-
|
|
150
|
-
## Example Session
|
|
151
|
-
|
|
152
|
-
Here's how an AI might use this during reasoning:
|
|
105
|
+
An agent might use Indra like this:
|
|
153
106
|
|
|
154
107
|
```
|
|
155
|
-
User:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
108
|
+
User: Should I use PostgreSQL or MongoDB for my e-commerce app?
|
|
109
|
+
|
|
110
|
+
Agent thinking:
|
|
111
|
+
→ indra_search({ query: "database recommendations" })
|
|
112
|
+
→ Found: Previously recommended PostgreSQL for relational data with transactions
|
|
113
|
+
|
|
114
|
+
→ Making recommendation based on past reasoning + current context
|
|
115
|
+
|
|
116
|
+
→ indra_remember({
|
|
117
|
+
content: "Recommended PostgreSQL for e-commerce app. User has relational product
|
|
118
|
+
catalog, needs transactions for orders. Consistent with past guidance.",
|
|
119
|
+
id: "ecommerce-db-decision"
|
|
120
|
+
})
|
|
121
|
+
```
|
|
161
122
|
|
|
162
|
-
|
|
163
|
-
simpler deployment, easier debugging"
|
|
123
|
+
Later:
|
|
164
124
|
|
|
165
|
-
|
|
166
|
-
|
|
125
|
+
```
|
|
126
|
+
User: Why did you recommend PostgreSQL?
|
|
167
127
|
|
|
168
|
-
|
|
169
|
-
|
|
128
|
+
Agent:
|
|
129
|
+
→ indra_search({ query: "ecommerce database" })
|
|
130
|
+
→ Found the reasoning from the previous session
|
|
131
|
+
→ Can explain the decision with full context
|
|
132
|
+
```
|
|
170
133
|
|
|
171
|
-
|
|
134
|
+
### Branching for Exploration
|
|
172
135
|
|
|
173
|
-
|
|
136
|
+
```
|
|
137
|
+
Agent: Let me explore an alternative approach...
|
|
138
|
+
|
|
139
|
+
→ indra_experiment({ name: "try-nosql-approach" })
|
|
140
|
+
|
|
141
|
+
[Explores MongoDB path, records reasoning]
|
|
142
|
+
|
|
143
|
+
→ indra_diff({ from: "main" }) // Compare with main reasoning
|
|
144
|
+
|
|
145
|
+
→ indra_branch({ action: "switch", name: "main" }) // Back to main
|
|
146
|
+
```
|
|
174
147
|
|
|
175
|
-
|
|
176
|
-
[Uses switch_branch] "microservices-case"
|
|
148
|
+
## Environment Variables
|
|
177
149
|
|
|
178
|
-
|
|
179
|
-
|
|
150
|
+
| Variable | Description | Default |
|
|
151
|
+
|----------|-------------|---------|
|
|
152
|
+
| `INDRA_DB_PATH` | Path to database file | `./.indra` |
|
|
153
|
+
| `INDRA_API_URL` | API for sync (optional) | `https://api.indradb.net` |
|
|
180
154
|
|
|
181
|
-
|
|
182
|
-
// Finds related thoughts about team dynamics
|
|
155
|
+
## How It Works
|
|
183
156
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
157
|
+
1. **Content-addressed storage** — Every entry is hashed. Identity comes from content.
|
|
158
|
+
2. **Local embeddings** — Uses `sentence-transformers/all-MiniLM-L6-v2` for semantic search.
|
|
159
|
+
3. **Git-like versioning** — Commits create snapshots. Branches enable parallel exploration.
|
|
160
|
+
4. **Single file** — Everything in one `.indra` file. Easy to backup and share.
|
|
188
161
|
|
|
189
162
|
## Architecture
|
|
190
163
|
|
|
@@ -196,7 +169,7 @@ team ownership and deployment"
|
|
|
196
169
|
┌─────────────────────────▼───────────────────────────────┐
|
|
197
170
|
│ indra_db_mcp (Bun) │
|
|
198
171
|
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
|
199
|
-
│ │ MCP SDK │ │ IndraClient │ │
|
|
172
|
+
│ │ MCP SDK │ │ IndraClient │ │ Auto-sync │ │
|
|
200
173
|
│ └─────────────┘ └──────┬───────┘ └───────────────┘ │
|
|
201
174
|
└──────────────────────────┼──────────────────────────────┘
|
|
202
175
|
│ CLI subprocess (JSON)
|
|
@@ -205,51 +178,19 @@ team ownership and deployment"
|
|
|
205
178
|
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
|
206
179
|
│ │ Graph Store │ │ Embeddings │ │ Git-like VCS │ │
|
|
207
180
|
│ └─────────────┘ └──────────────┘ └───────────────┘ │
|
|
208
|
-
└─────────────────────────────────────────────────────────┘
|
|
209
|
-
│
|
|
210
|
-
┌──────────────────────────▼──────────────────────────────┐
|
|
211
|
-
│ thoughts.indra (Single File) │
|
|
212
|
-
│ Content-addressed objects, BLAKE3 hashes, zstd compressed│
|
|
213
181
|
└─────────────────────────────────────────────────────────┘
|
|
214
182
|
```
|
|
215
183
|
|
|
216
|
-
##
|
|
184
|
+
## Visualize on IndraDB
|
|
217
185
|
|
|
218
|
-
|
|
219
|
-
# Run with watch mode
|
|
220
|
-
bun run dev
|
|
186
|
+
Push to [IndraDB](https://indradb.net) for 3D visualization and analytics:
|
|
221
187
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
bun test
|
|
188
|
+
```bash
|
|
189
|
+
indra login
|
|
190
|
+
indra remote add origin username/my-memory
|
|
191
|
+
indra push origin
|
|
227
192
|
```
|
|
228
193
|
|
|
229
|
-
## How It Works
|
|
230
|
-
|
|
231
|
-
1. **Content Addressing**: Every thought is hashed (BLAKE3). Identity comes from content.
|
|
232
|
-
|
|
233
|
-
2. **Embeddings**: Using `sentence-transformers/all-MiniLM-L6-v2` locally via HuggingFace.
|
|
234
|
-
Thoughts are embedded on creation for semantic search.
|
|
235
|
-
|
|
236
|
-
3. **Graph Structure**: Thoughts are nodes, relationships are typed/weighted edges.
|
|
237
|
-
Edges "float" to latest node versions.
|
|
238
|
-
|
|
239
|
-
4. **Version Control**: Git-like commits create snapshots. Branches enable parallel exploration.
|
|
240
|
-
Full history preserved — nothing truly deleted.
|
|
241
|
-
|
|
242
|
-
5. **Single File**: Everything stored in one `.indra` file. Easy to backup, share, version.
|
|
243
|
-
|
|
244
|
-
## Philosophical Note
|
|
245
|
-
|
|
246
|
-
This project is named after [Indra's Net](https://en.wikipedia.org/wiki/Indra%27s_net) —
|
|
247
|
-
a Buddhist metaphor where reality is a vast net of jewels, each reflecting all others.
|
|
248
|
-
|
|
249
|
-
Your thoughts are like those jewels. Each one reflects and connects to others.
|
|
250
|
-
The web of connections *is* your understanding. This tool makes that web visible,
|
|
251
|
-
versionable, and searchable.
|
|
252
|
-
|
|
253
194
|
## License
|
|
254
195
|
|
|
255
196
|
MIT
|
|
@@ -257,5 +198,5 @@ MIT
|
|
|
257
198
|
## Related
|
|
258
199
|
|
|
259
200
|
- [indra_db](https://github.com/moonstripe/indra_db) — The underlying Rust database
|
|
201
|
+
- [IndraDB](https://indradb.net) — Web platform for visualization
|
|
260
202
|
- [MCP Specification](https://modelcontextprotocol.io/) — Model Context Protocol docs
|
|
261
|
-
- [indranet](https://github.com/moonstripe/indranet) — Online viewing tool (coming soon)
|
package/package.json
CHANGED
package/src/e2e.test.ts
CHANGED
|
@@ -166,6 +166,52 @@ describe("End-to-End: Real MCP Usage Patterns", () => {
|
|
|
166
166
|
// Stress Tests
|
|
167
167
|
// ==========================================================================
|
|
168
168
|
|
|
169
|
+
test("stress: concurrent writes should not corrupt database (issue #2)", async () => {
|
|
170
|
+
const client = new IndraClient({ databasePath: dbPath });
|
|
171
|
+
|
|
172
|
+
// Fire off 7+ writes concurrently (the exact pattern that caused corruption)
|
|
173
|
+
const writes = Array.from({ length: 10 }, (_, i) =>
|
|
174
|
+
client.createThought(`Concurrent note ${i}: rapid fire save`, { id: `concurrent-${i}` })
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// All should succeed without corruption
|
|
178
|
+
const results = await Promise.all(writes);
|
|
179
|
+
expect(results.length).toBe(10);
|
|
180
|
+
|
|
181
|
+
// Verify all notes persisted correctly
|
|
182
|
+
const newClient = new IndraClient({ databasePath: dbPath });
|
|
183
|
+
const all = await newClient.listThoughts();
|
|
184
|
+
expect(all.count).toBe(10);
|
|
185
|
+
|
|
186
|
+
// Verify each one is readable
|
|
187
|
+
for (let i = 0; i < 10; i++) {
|
|
188
|
+
const thought = await newClient.getThought(`concurrent-${i}`);
|
|
189
|
+
expect(thought.content).toContain(`Concurrent note ${i}`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("stress: concurrent writes with interleaved reads (issue #2)", async () => {
|
|
194
|
+
const client = new IndraClient({ databasePath: dbPath });
|
|
195
|
+
|
|
196
|
+
// Interleave writes and reads concurrently
|
|
197
|
+
const operations = [];
|
|
198
|
+
for (let i = 0; i < 5; i++) {
|
|
199
|
+
operations.push(client.createThought(`Interleaved ${i}`, { id: `interleaved-${i}` }));
|
|
200
|
+
operations.push(client.listThoughts());
|
|
201
|
+
operations.push(client.search("interleaved", 10));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// None should throw corruption errors
|
|
205
|
+
const results = await Promise.allSettled(operations);
|
|
206
|
+
const failures = results.filter(r => r.status === 'rejected');
|
|
207
|
+
expect(failures.length).toBe(0);
|
|
208
|
+
|
|
209
|
+
// Verify final state
|
|
210
|
+
const newClient = new IndraClient({ databasePath: dbPath });
|
|
211
|
+
const all = await newClient.listThoughts();
|
|
212
|
+
expect(all.count).toBe(5);
|
|
213
|
+
});
|
|
214
|
+
|
|
169
215
|
test("stress: many notes across many sessions", async () => {
|
|
170
216
|
const noteCount = 50;
|
|
171
217
|
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* indra_db MCP Server
|
|
3
|
+
* indra_db MCP Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 1. Minimal tools - each tool does ONE thing well
|
|
9
|
-
* 2. Auto-commit - every mutation persists immediately
|
|
10
|
-
* 3. Auto-sync - pull before search, push after remember
|
|
11
|
-
* 4. Self-contained - no tool depends on another being called first
|
|
12
|
-
* 5. Clear purpose - tool names match what users would say
|
|
5
|
+
* Persistent memory for your reasoning process.
|
|
6
|
+
* Track how your understanding evolves, why you made decisions,
|
|
7
|
+
* and explore alternative approaches through branching.
|
|
13
8
|
*/
|
|
14
9
|
|
|
15
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -24,7 +19,7 @@ import { IndraError } from "./types.js";
|
|
|
24
19
|
|
|
25
20
|
const server = new McpServer({
|
|
26
21
|
name: "indra_db",
|
|
27
|
-
version: "0.
|
|
22
|
+
version: "0.2.2",
|
|
28
23
|
});
|
|
29
24
|
|
|
30
25
|
const client = new IndraClient();
|
|
@@ -38,28 +33,15 @@ interface SyncResult {
|
|
|
38
33
|
warning?: string;
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
/**
|
|
42
|
-
* Check if we have authentication configured.
|
|
43
|
-
* Auth is needed for:
|
|
44
|
-
* - Pushing to any base (you need to be logged in to write)
|
|
45
|
-
* - Pulling from private bases
|
|
46
|
-
*
|
|
47
|
-
* Authentication can come from:
|
|
48
|
-
* - INDRA_API_KEY env var (legacy API key)
|
|
49
|
-
* - OAuth credentials file (from `indra login`)
|
|
50
|
-
*/
|
|
51
36
|
function hasAuth(): boolean {
|
|
52
|
-
// Check for legacy API key
|
|
53
37
|
if (process.env.INDRA_API_KEY) {
|
|
54
38
|
return true;
|
|
55
39
|
}
|
|
56
40
|
|
|
57
|
-
// Check for OAuth credentials file
|
|
58
41
|
const { existsSync } = require("fs");
|
|
59
42
|
const { homedir } = require("os");
|
|
60
43
|
const { join } = require("path");
|
|
61
44
|
|
|
62
|
-
// Check both possible credential locations
|
|
63
45
|
const credentialsPaths = [
|
|
64
46
|
join(homedir(), "Library", "Application Support", "indra", "credentials.json"),
|
|
65
47
|
join(homedir(), ".config", "indra", "credentials.json"),
|
|
@@ -74,40 +56,25 @@ function hasAuth(): boolean {
|
|
|
74
56
|
return false;
|
|
75
57
|
}
|
|
76
58
|
|
|
77
|
-
/**
|
|
78
|
-
* Attempt to pull from remote before read operations.
|
|
79
|
-
* Uses hash comparison for fast merge (ORT-style).
|
|
80
|
-
* Returns warning message if sync failed, but never throws.
|
|
81
|
-
*
|
|
82
|
-
* Pull behavior:
|
|
83
|
-
* - Public bases: works without auth
|
|
84
|
-
* - Private bases: requires INDRA_API_KEY
|
|
85
|
-
* - No remote configured: silently skip (local-only mode is fine)
|
|
86
|
-
*/
|
|
87
59
|
async function tryPullSync(): Promise<SyncResult> {
|
|
88
60
|
try {
|
|
89
|
-
// Check if remote is configured
|
|
90
61
|
const remotes = await client.remoteList();
|
|
91
62
|
if (remotes.count === 0) {
|
|
92
|
-
return { synced: false };
|
|
63
|
+
return { synced: false };
|
|
93
64
|
}
|
|
94
65
|
|
|
95
66
|
const result = await client.pull();
|
|
96
67
|
if (result.status === "ok") {
|
|
97
68
|
return { synced: true };
|
|
98
69
|
} else if (result.status === "pending") {
|
|
99
|
-
// API not connected yet - this is expected during development
|
|
100
70
|
return { synced: false };
|
|
101
71
|
} else if (result.message?.includes("Not found")) {
|
|
102
|
-
// Remote doesn't exist yet or is private without auth - that's ok for reads
|
|
103
72
|
return { synced: false };
|
|
104
73
|
} else {
|
|
105
74
|
return { synced: false, warning: `Sync: ${result.message}` };
|
|
106
75
|
}
|
|
107
76
|
} catch (error) {
|
|
108
|
-
// Network error or other issue - don't block the operation
|
|
109
77
|
const msg = error instanceof Error ? error.message : String(error);
|
|
110
|
-
// Only warn if it's not a "no remote" or "not found" error
|
|
111
78
|
if (!msg.includes("not found") && !msg.includes("No remote") && !msg.includes("Not found")) {
|
|
112
79
|
return { synced: false, warning: `Sync unavailable: ${msg}` };
|
|
113
80
|
}
|
|
@@ -115,27 +82,14 @@ async function tryPullSync(): Promise<SyncResult> {
|
|
|
115
82
|
}
|
|
116
83
|
}
|
|
117
84
|
|
|
118
|
-
/**
|
|
119
|
-
* Attempt to push to remote after write operations.
|
|
120
|
-
* Returns warning message if sync failed, but never throws.
|
|
121
|
-
*
|
|
122
|
-
* Push behavior:
|
|
123
|
-
* - Always requires auth (you need to be logged in to write)
|
|
124
|
-
* - No remote configured: silently skip
|
|
125
|
-
* - No auth: skip silently (user is in local-only mode)
|
|
126
|
-
*/
|
|
127
85
|
async function tryPushSync(): Promise<SyncResult> {
|
|
128
86
|
try {
|
|
129
|
-
// Check if remote is configured
|
|
130
87
|
const remotes = await client.remoteList();
|
|
131
88
|
if (remotes.count === 0) {
|
|
132
|
-
return { synced: false };
|
|
89
|
+
return { synced: false };
|
|
133
90
|
}
|
|
134
91
|
|
|
135
|
-
// Check if we have auth - push always requires it
|
|
136
92
|
if (!hasAuth()) {
|
|
137
|
-
// No auth, but that's fine - user is working locally
|
|
138
|
-
// They can push later with `indra login` + `indra push`
|
|
139
93
|
return { synced: false };
|
|
140
94
|
}
|
|
141
95
|
|
|
@@ -143,13 +97,11 @@ async function tryPushSync(): Promise<SyncResult> {
|
|
|
143
97
|
if (result.status === "ok") {
|
|
144
98
|
return { synced: true };
|
|
145
99
|
} else if (result.status === "pending") {
|
|
146
|
-
// API not connected yet - this is expected during development
|
|
147
100
|
return { synced: false };
|
|
148
101
|
} else {
|
|
149
102
|
return { synced: false, warning: `Sync: ${result.message}` };
|
|
150
103
|
}
|
|
151
104
|
} catch (error) {
|
|
152
|
-
// Network error or other issue - don't block the operation
|
|
153
105
|
const msg = error instanceof Error ? error.message : String(error);
|
|
154
106
|
if (!msg.includes("not found") && !msg.includes("No remote")) {
|
|
155
107
|
return { synced: false, warning: `Sync unavailable: ${msg}` };
|
|
@@ -159,7 +111,7 @@ async function tryPushSync(): Promise<SyncResult> {
|
|
|
159
111
|
}
|
|
160
112
|
|
|
161
113
|
// ============================================================================
|
|
162
|
-
// Helper: Format responses
|
|
114
|
+
// Helper: Format responses
|
|
163
115
|
// ============================================================================
|
|
164
116
|
|
|
165
117
|
function formatSuccess(
|
|
@@ -201,42 +153,44 @@ function formatError(error: unknown): { content: Array<{ type: "text"; text: str
|
|
|
201
153
|
}
|
|
202
154
|
|
|
203
155
|
// ============================================================================
|
|
204
|
-
// TOOL: indra_remember -
|
|
156
|
+
// TOOL: indra_remember - Document your reasoning
|
|
205
157
|
// ============================================================================
|
|
206
158
|
|
|
207
159
|
server.tool(
|
|
208
160
|
"indra_remember",
|
|
209
|
-
`
|
|
161
|
+
`Capture your reasoning — the WHY behind decisions.
|
|
162
|
+
|
|
163
|
+
THIS IS YOUR PRIMARY TOOL. Use it liberally.
|
|
210
164
|
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
165
|
+
CALL THIS WHEN you:
|
|
166
|
+
- Make a recommendation (record WHY you chose this approach)
|
|
167
|
+
- Change direction (capture WHAT SHIFTED your thinking)
|
|
168
|
+
- Reach a conclusion (preserve the reasoning chain)
|
|
169
|
+
- Learn something that changes how you'd approach similar problems
|
|
170
|
+
- Complete a task (document what worked and why)
|
|
215
171
|
|
|
216
|
-
|
|
217
|
-
|
|
172
|
+
WHAT TO RECORD:
|
|
173
|
+
- "Chose X because A, B, C" — not just "using X"
|
|
174
|
+
- "Shifted from X to Y because..." — capture the pivot
|
|
175
|
+
- "Key insight: ..." — distill what you learned
|
|
176
|
+
- "Trade-offs considered: ..." — show your reasoning
|
|
218
177
|
|
|
219
|
-
|
|
178
|
+
This is how you build continuity. Your future self will thank you.`,
|
|
220
179
|
{
|
|
221
|
-
content: z.string().describe("
|
|
222
|
-
id: z.string().optional().describe("Optional ID to update existing
|
|
180
|
+
content: z.string().describe("Your reasoning - include the why, not just the what"),
|
|
181
|
+
id: z.string().optional().describe("Optional ID to update existing entry"),
|
|
223
182
|
},
|
|
224
183
|
async ({ content, id }) => {
|
|
225
184
|
try {
|
|
226
|
-
// Check if this is an update (ID exists)
|
|
227
185
|
if (id) {
|
|
228
186
|
try {
|
|
229
187
|
const existing = await client.getThought(id);
|
|
230
188
|
if (existing) {
|
|
231
|
-
// Update existing thought
|
|
232
189
|
const thought = await client.updateThought(id, content);
|
|
233
|
-
|
|
234
|
-
// Push after write
|
|
235
190
|
const syncResult = await tryPushSync();
|
|
236
|
-
|
|
237
191
|
return formatSuccess(
|
|
238
|
-
{ id: thought.id,
|
|
239
|
-
`✅ Updated
|
|
192
|
+
{ id: thought.id, updated: true },
|
|
193
|
+
`✅ Updated "${id}"`,
|
|
240
194
|
syncResult.warning
|
|
241
195
|
);
|
|
242
196
|
}
|
|
@@ -245,15 +199,12 @@ Call BEFORE responding when you learn something worth keeping.`,
|
|
|
245
199
|
}
|
|
246
200
|
}
|
|
247
201
|
|
|
248
|
-
// Create new thought
|
|
249
202
|
const thought = await client.createThought(content, { id });
|
|
250
|
-
|
|
251
|
-
// Push after write
|
|
252
203
|
const syncResult = await tryPushSync();
|
|
253
204
|
|
|
254
205
|
return formatSuccess(
|
|
255
|
-
{ id: thought.id,
|
|
256
|
-
`✅
|
|
206
|
+
{ id: thought.id, created: true },
|
|
207
|
+
`✅ Recorded "${thought.id}"`,
|
|
257
208
|
syncResult.warning
|
|
258
209
|
);
|
|
259
210
|
} catch (error) {
|
|
@@ -263,59 +214,57 @@ Call BEFORE responding when you learn something worth keeping.`,
|
|
|
263
214
|
);
|
|
264
215
|
|
|
265
216
|
// ============================================================================
|
|
266
|
-
// TOOL: indra_search -
|
|
217
|
+
// TOOL: indra_search - Review past reasoning
|
|
267
218
|
// ============================================================================
|
|
268
219
|
|
|
269
220
|
server.tool(
|
|
270
221
|
"indra_search",
|
|
271
|
-
`
|
|
222
|
+
`Review how you've reasoned about similar topics before.
|
|
272
223
|
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
-
|
|
276
|
-
-
|
|
224
|
+
CALL THIS WHEN you:
|
|
225
|
+
- Are about to make a recommendation (check for prior reasoning)
|
|
226
|
+
- Want to maintain consistency with earlier conclusions
|
|
227
|
+
- Need to recall the journey, not just the destination
|
|
228
|
+
- Are asked about previous discussions
|
|
277
229
|
|
|
278
|
-
|
|
230
|
+
Your past reasoning is context. Use it.`,
|
|
279
231
|
{
|
|
280
|
-
query: z.string().describe('What to search for, or "*" to list all
|
|
281
|
-
limit: z.number().min(1).max(50).default(10).describe("Maximum results
|
|
232
|
+
query: z.string().describe('What to search for, or "*" to list all'),
|
|
233
|
+
limit: z.number().min(1).max(50).default(10).describe("Maximum results"),
|
|
282
234
|
},
|
|
283
235
|
async ({ query, limit }) => {
|
|
284
236
|
try {
|
|
285
|
-
// Pull before read to get latest from remote
|
|
286
237
|
const syncResult = await tryPullSync();
|
|
287
238
|
|
|
288
|
-
// Special case: list all
|
|
289
239
|
if (query === "*") {
|
|
290
240
|
const result = await client.listThoughts();
|
|
291
241
|
if (result.count === 0) {
|
|
242
|
+
return formatSuccess(
|
|
243
|
+
{ count: 0, entries: [] },
|
|
244
|
+
`📭 No reasoning recorded yet.`,
|
|
245
|
+
syncResult.warning
|
|
246
|
+
);
|
|
247
|
+
}
|
|
292
248
|
return formatSuccess(
|
|
293
|
-
{ count:
|
|
294
|
-
|
|
249
|
+
{ count: result.count, entries: result.thoughts },
|
|
250
|
+
`📋 ${result.count} entries:`,
|
|
295
251
|
syncResult.warning
|
|
296
252
|
);
|
|
297
|
-
}
|
|
298
|
-
return formatSuccess(
|
|
299
|
-
{ count: result.count, notes: result.thoughts },
|
|
300
|
-
`📋 Found ${result.count} note(s):`,
|
|
301
|
-
syncResult.warning
|
|
302
|
-
);
|
|
303
253
|
}
|
|
304
254
|
|
|
305
|
-
// Semantic search
|
|
306
255
|
const result = await client.search(query, limit);
|
|
307
256
|
if (result.count === 0) {
|
|
257
|
+
return formatSuccess(
|
|
258
|
+
{ query, count: 0, results: [] },
|
|
259
|
+
`📭 No prior reasoning on "${query}"`,
|
|
260
|
+
syncResult.warning
|
|
261
|
+
);
|
|
262
|
+
}
|
|
308
263
|
return formatSuccess(
|
|
309
|
-
{ query, count:
|
|
310
|
-
|
|
264
|
+
{ query, count: result.count, results: result.results },
|
|
265
|
+
`🔍 ${result.count} relevant entries:`,
|
|
311
266
|
syncResult.warning
|
|
312
267
|
);
|
|
313
|
-
}
|
|
314
|
-
return formatSuccess(
|
|
315
|
-
{ query, count: result.count, results: result.results },
|
|
316
|
-
`🔍 Found ${result.count} note(s) matching "${query}":`,
|
|
317
|
-
syncResult.warning
|
|
318
|
-
);
|
|
319
268
|
} catch (error) {
|
|
320
269
|
return formatError(error);
|
|
321
270
|
}
|
|
@@ -323,26 +272,18 @@ Always worth checking - takes milliseconds, could save back-and-forth.`,
|
|
|
323
272
|
);
|
|
324
273
|
|
|
325
274
|
// ============================================================================
|
|
326
|
-
// TOOL: indra_status -
|
|
275
|
+
// TOOL: indra_status - Current state
|
|
327
276
|
// ============================================================================
|
|
328
277
|
|
|
329
278
|
server.tool(
|
|
330
279
|
"indra_status",
|
|
331
|
-
`Check
|
|
332
|
-
|
|
333
|
-
Shows:
|
|
334
|
-
- Database location
|
|
335
|
-
- Number of notes
|
|
336
|
-
- Current branch (if using versioning)
|
|
337
|
-
|
|
338
|
-
Use this to orient yourself at the start of a session.`,
|
|
280
|
+
`Check current memory state and branch.`,
|
|
339
281
|
{},
|
|
340
282
|
async () => {
|
|
341
283
|
try {
|
|
342
284
|
const status = await client.status();
|
|
343
285
|
const thoughts = await client.listThoughts();
|
|
344
286
|
|
|
345
|
-
// Check remote configuration
|
|
346
287
|
let remoteInfo: { configured: boolean; name?: string; url?: string } = { configured: false };
|
|
347
288
|
try {
|
|
348
289
|
const remotes = await client.remoteList();
|
|
@@ -357,21 +298,204 @@ Use this to orient yourself at the start of a session.`,
|
|
|
357
298
|
// No remotes configured
|
|
358
299
|
}
|
|
359
300
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
301
|
+
let branchInfo: { current: string; count: number } = { current: status.branch, count: 1 };
|
|
302
|
+
try {
|
|
303
|
+
const branches = await client.listBranches();
|
|
304
|
+
branchInfo = {
|
|
305
|
+
current: branches.current,
|
|
306
|
+
count: branches.branches.length,
|
|
307
|
+
};
|
|
308
|
+
} catch {
|
|
309
|
+
// Branching info unavailable
|
|
310
|
+
}
|
|
364
311
|
|
|
365
312
|
return formatSuccess(
|
|
366
313
|
{
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
dirty: status.dirty,
|
|
314
|
+
branch: branchInfo.current,
|
|
315
|
+
branches: branchInfo.count,
|
|
316
|
+
entries: thoughts.count,
|
|
371
317
|
remote: remoteInfo,
|
|
372
|
-
auth: authStatus,
|
|
373
318
|
},
|
|
374
|
-
`📊
|
|
319
|
+
`📊 Current state:`
|
|
320
|
+
);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return formatError(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// TOOL: indra_branch - Manage reasoning threads
|
|
329
|
+
// ============================================================================
|
|
330
|
+
|
|
331
|
+
server.tool(
|
|
332
|
+
"indra_branch",
|
|
333
|
+
`Manage parallel lines of reasoning.
|
|
334
|
+
|
|
335
|
+
CALL THIS WHEN you:
|
|
336
|
+
- Want to explore an alternative without losing the current thread
|
|
337
|
+
- Need to compare two different approaches
|
|
338
|
+
- Are about to go down a path you might want to abandon
|
|
339
|
+
|
|
340
|
+
Branches are cheap. Create them freely.`,
|
|
341
|
+
{
|
|
342
|
+
action: z.enum(["create", "switch", "list"]).describe("What to do"),
|
|
343
|
+
name: z.string().optional().describe("Branch name (required for create/switch)"),
|
|
344
|
+
},
|
|
345
|
+
async ({ action, name }) => {
|
|
346
|
+
try {
|
|
347
|
+
switch (action) {
|
|
348
|
+
case "create": {
|
|
349
|
+
if (!name) {
|
|
350
|
+
return formatError(new Error("Branch name required for 'create'"));
|
|
351
|
+
}
|
|
352
|
+
const branch = await client.createBranch(name);
|
|
353
|
+
const syncResult = await tryPushSync();
|
|
354
|
+
return formatSuccess(
|
|
355
|
+
{ name: branch.name, created: true },
|
|
356
|
+
`🌿 Created and switched to "${name}"`,
|
|
357
|
+
syncResult.warning
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case "switch": {
|
|
362
|
+
if (!name) {
|
|
363
|
+
return formatError(new Error("Branch name required for 'switch'"));
|
|
364
|
+
}
|
|
365
|
+
await client.checkout(name);
|
|
366
|
+
const syncResult = await tryPullSync();
|
|
367
|
+
const thoughts = await client.listThoughts();
|
|
368
|
+
return formatSuccess(
|
|
369
|
+
{ branch: name, entries: thoughts.count },
|
|
370
|
+
`🔀 Switched to "${name}" (${thoughts.count} entries)`,
|
|
371
|
+
syncResult.warning
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case "list": {
|
|
376
|
+
const result = await client.listBranches();
|
|
377
|
+
const branchList = result.branches.map(b => ({
|
|
378
|
+
name: b.name,
|
|
379
|
+
current: b.name === result.current,
|
|
380
|
+
}));
|
|
381
|
+
return formatSuccess(
|
|
382
|
+
{ current: result.current, branches: branchList },
|
|
383
|
+
`🌳 ${result.branches.length} branch(es), on "${result.current}"`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return formatError(error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// ============================================================================
|
|
394
|
+
// TOOL: indra_history - View reasoning evolution
|
|
395
|
+
// ============================================================================
|
|
396
|
+
|
|
397
|
+
server.tool(
|
|
398
|
+
"indra_history",
|
|
399
|
+
`See how your reasoning has evolved.
|
|
400
|
+
|
|
401
|
+
Shows the timeline of recorded thoughts — useful for understanding
|
|
402
|
+
how you arrived at current conclusions.`,
|
|
403
|
+
{
|
|
404
|
+
limit: z.number().min(1).max(100).default(10).describe("Maximum commits to show"),
|
|
405
|
+
},
|
|
406
|
+
async ({ limit }) => {
|
|
407
|
+
try {
|
|
408
|
+
const result = await client.log(limit);
|
|
409
|
+
const commits = result.commits.map(c => ({
|
|
410
|
+
hash: c.hash.substring(0, 8),
|
|
411
|
+
message: c.message,
|
|
412
|
+
timestamp: c.timestamp,
|
|
413
|
+
}));
|
|
414
|
+
return formatSuccess(
|
|
415
|
+
{ branch: result.branch, commits },
|
|
416
|
+
`📜 Reasoning timeline:`
|
|
417
|
+
);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return formatError(error);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// TOOL: indra_diff - Compare reasoning paths
|
|
426
|
+
// ============================================================================
|
|
427
|
+
|
|
428
|
+
server.tool(
|
|
429
|
+
"indra_diff",
|
|
430
|
+
`Compare two points in your reasoning.
|
|
431
|
+
|
|
432
|
+
CALL THIS WHEN you:
|
|
433
|
+
- Want to see what changed between branches
|
|
434
|
+
- Need to understand how your thinking evolved
|
|
435
|
+
- Are deciding whether to merge an experimental branch`,
|
|
436
|
+
{
|
|
437
|
+
from: z.string().optional().describe("Starting point (commit or branch)"),
|
|
438
|
+
to: z.string().optional().describe("Ending point (defaults to HEAD)"),
|
|
439
|
+
},
|
|
440
|
+
async ({ from, to }) => {
|
|
441
|
+
try {
|
|
442
|
+
const result = await client.diff(from, to);
|
|
443
|
+
|
|
444
|
+
const summary = {
|
|
445
|
+
added: result.added.length,
|
|
446
|
+
removed: result.removed.length,
|
|
447
|
+
modified: result.modified.length,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
return formatSuccess(
|
|
451
|
+
{
|
|
452
|
+
summary,
|
|
453
|
+
added: result.added.map(t => ({ id: t.id, content: t.content })),
|
|
454
|
+
removed: result.removed.map(t => ({ id: t.id, content: t.content })),
|
|
455
|
+
modified: result.modified.map(m => ({
|
|
456
|
+
id: m.after.id,
|
|
457
|
+
before: m.before.content,
|
|
458
|
+
after: m.after.content,
|
|
459
|
+
})),
|
|
460
|
+
},
|
|
461
|
+
`📊 Reasoning diff:`
|
|
462
|
+
);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
return formatError(error);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// TOOL: indra_experiment - Quick exploration branch
|
|
471
|
+
// ============================================================================
|
|
472
|
+
|
|
473
|
+
server.tool(
|
|
474
|
+
"indra_experiment",
|
|
475
|
+
`Start exploring an alternative approach.
|
|
476
|
+
|
|
477
|
+
Creates a branch and switches to it. Use when you want to:
|
|
478
|
+
- Think through a different path
|
|
479
|
+
- Try something you might abandon
|
|
480
|
+
- Compare approaches without losing your main thread
|
|
481
|
+
|
|
482
|
+
When done, switch back to main or merge your findings.`,
|
|
483
|
+
{
|
|
484
|
+
name: z.string().describe("Descriptive name for this exploration"),
|
|
485
|
+
},
|
|
486
|
+
async ({ name }) => {
|
|
487
|
+
try {
|
|
488
|
+
const branch = await client.createBranch(name);
|
|
489
|
+
const syncResult = await tryPushSync();
|
|
490
|
+
const thoughts = await client.listThoughts();
|
|
491
|
+
|
|
492
|
+
return formatSuccess(
|
|
493
|
+
{
|
|
494
|
+
branch: name,
|
|
495
|
+
entries: thoughts.count,
|
|
496
|
+
},
|
|
497
|
+
`🧪 Exploring "${name}" — main thread preserved`,
|
|
498
|
+
syncResult.warning
|
|
375
499
|
);
|
|
376
500
|
} catch (error) {
|
|
377
501
|
return formatError(error);
|
|
@@ -386,20 +510,18 @@ Use this to orient yourself at the start of a session.`,
|
|
|
386
510
|
async function main() {
|
|
387
511
|
const transport = new StdioServerTransport();
|
|
388
512
|
|
|
389
|
-
console.error(`[indra_db_mcp] Starting server v0.
|
|
513
|
+
console.error(`[indra_db_mcp] Starting server v0.2.2...`);
|
|
390
514
|
console.error(`[indra_db_mcp] Database path: ${client.getDatabasePath()}`);
|
|
391
515
|
console.error(`[indra_db_mcp] API URL: ${client.getApiUrl()}`);
|
|
392
516
|
if (client.isDevMode()) {
|
|
393
517
|
console.error(`[indra_db_mcp] ⚠️ DEV MODE ACTIVE`);
|
|
394
518
|
}
|
|
395
519
|
|
|
396
|
-
// Initialize the client (ensures binary exists, creates DB if needed)
|
|
397
520
|
try {
|
|
398
521
|
await client.init();
|
|
399
522
|
console.error(`[indra_db_mcp] Database initialized successfully`);
|
|
400
523
|
} catch (error) {
|
|
401
524
|
console.error(`[indra_db_mcp] Warning: ${error}`);
|
|
402
|
-
// Continue anyway - errors will be reported when tools are called
|
|
403
525
|
}
|
|
404
526
|
|
|
405
527
|
await server.connect(transport);
|
package/src/indra-client.ts
CHANGED
|
@@ -200,6 +200,8 @@ export class IndraClient {
|
|
|
200
200
|
private config: Required<IndraClientConfig>;
|
|
201
201
|
private binaryPath: string | null = null;
|
|
202
202
|
private initialized = false;
|
|
203
|
+
/** Promise-based queue to serialize ALL CLI invocations and prevent concurrent file access */
|
|
204
|
+
private execQueue: Promise<void> = Promise.resolve();
|
|
203
205
|
|
|
204
206
|
constructor(config: IndraClientConfig = {}) {
|
|
205
207
|
this.config = {
|
|
@@ -243,7 +245,24 @@ export class IndraClient {
|
|
|
243
245
|
// CLI Execution
|
|
244
246
|
// --------------------------------------------------------------------------
|
|
245
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Serialize all CLI invocations through a promise queue to prevent
|
|
250
|
+
* concurrent process access to the .indra file (cross-process corruption).
|
|
251
|
+
* See: https://github.com/moonstripe/indra_db_mcp/issues/2
|
|
252
|
+
*/
|
|
246
253
|
private async exec<T = unknown>(args: string[], skipInit = false): Promise<T> {
|
|
254
|
+
return new Promise<T>((resolve, reject) => {
|
|
255
|
+
this.execQueue = this.execQueue.then(async () => {
|
|
256
|
+
try {
|
|
257
|
+
resolve(await this._execInternal<T>(args, skipInit));
|
|
258
|
+
} catch (e) {
|
|
259
|
+
reject(e);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async _execInternal<T = unknown>(args: string[], skipInit = false): Promise<T> {
|
|
247
266
|
// Resolve binary if not already done
|
|
248
267
|
if (!this.binaryPath) {
|
|
249
268
|
this.binaryPath = await resolveBinaryPath(this.config.binaryPath);
|
|
@@ -518,15 +537,13 @@ export class IndraClient {
|
|
|
518
537
|
/**
|
|
519
538
|
* Push to a remote repository.
|
|
520
539
|
* Note: Requires IndraNet API connection to actually transfer data.
|
|
540
|
+
* Visualization is computed server-side by IndraNet, not by the CLI.
|
|
521
541
|
*/
|
|
522
|
-
async push(remote: string = "origin", force: boolean = false
|
|
542
|
+
async push(remote: string = "origin", force: boolean = false): Promise<PushResponse> {
|
|
523
543
|
const args = ["push", remote];
|
|
524
544
|
if (force) {
|
|
525
545
|
args.push("--force");
|
|
526
546
|
}
|
|
527
|
-
if (viz) {
|
|
528
|
-
args.push("--viz");
|
|
529
|
-
}
|
|
530
547
|
return this.exec<PushResponse>(args);
|
|
531
548
|
}
|
|
532
549
|
|