@brainbank/mcp 0.2.0 → 0.3.1
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 +88 -42
- package/dist/mcp-server.js +301 -315
- package/dist/mcp-server.js.map +1 -1
- package/package.json +15 -3
- package/src/mcp-server.ts +265 -0
- package/src/workspace-factory.ts +66 -0
- package/src/workspace-pool.ts +224 -0
package/dist/mcp-server.js
CHANGED
|
@@ -6,200 +6,258 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { z } from "zod/v3";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (rerankerEnv === "qwen3") {
|
|
20
|
-
const { Qwen3Reranker } = await import("@brainbank/reranker");
|
|
21
|
-
return new Qwen3Reranker();
|
|
22
|
-
}
|
|
23
|
-
return void 0;
|
|
9
|
+
import { existsSync as existsSync2 } from "fs";
|
|
10
|
+
|
|
11
|
+
// src/workspace-pool.ts
|
|
12
|
+
var DEFAULT_MAX_MEMORY_MB = 2048;
|
|
13
|
+
var DEFAULT_TTL_MINUTES = 30;
|
|
14
|
+
var EVICTION_INTERVAL_MS = 6e4;
|
|
15
|
+
function formatAgo(ms) {
|
|
16
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s ago`;
|
|
17
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
|
|
18
|
+
return `${Math.round(ms / 36e5)}h ago`;
|
|
24
19
|
}
|
|
25
|
-
__name(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const { OpenAIEmbedding } = await import("brainbank");
|
|
30
|
-
return new OpenAIEmbedding();
|
|
20
|
+
__name(formatAgo, "formatAgo");
|
|
21
|
+
var WorkspacePool = class {
|
|
22
|
+
static {
|
|
23
|
+
__name(this, "WorkspacePool");
|
|
31
24
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
_pool = /* @__PURE__ */ new Map();
|
|
26
|
+
_timer = null;
|
|
27
|
+
_maxMemoryBytes;
|
|
28
|
+
_ttlMs;
|
|
29
|
+
_factory;
|
|
30
|
+
_onEvict;
|
|
31
|
+
_onError;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this._maxMemoryBytes = (options.maxMemoryMB ?? DEFAULT_MAX_MEMORY_MB) * 1024 * 1024;
|
|
34
|
+
this._ttlMs = (options.ttlMinutes ?? DEFAULT_TTL_MINUTES) * 60 * 1e3;
|
|
35
|
+
this._factory = options.factory;
|
|
36
|
+
this._onEvict = options.onEvict;
|
|
37
|
+
this._onError = options.onError;
|
|
38
|
+
this._timer = setInterval(() => this._evictStale(), EVICTION_INTERVAL_MS);
|
|
39
|
+
if (this._timer.unref) this._timer.unref();
|
|
35
40
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
41
|
+
/** Number of cached workspaces. */
|
|
42
|
+
get size() {
|
|
43
|
+
return this._pool.size;
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Get a BrainBank for the given repo path.
|
|
47
|
+
* Returns a cached instance (with hot-reload) or creates a new one.
|
|
48
|
+
*/
|
|
49
|
+
async get(repoPath) {
|
|
50
|
+
const key = repoPath.replace(/\/+$/, "");
|
|
51
|
+
const existing = this._pool.get(key);
|
|
52
|
+
if (existing) {
|
|
53
|
+
existing.lastAccess = Date.now();
|
|
54
|
+
try {
|
|
55
|
+
await existing.brain.ensureFresh();
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return existing.brain;
|
|
59
|
+
}
|
|
60
|
+
this._evictByMemoryPressure();
|
|
61
|
+
const brain = await this._factory(key);
|
|
62
|
+
this._pool.set(key, {
|
|
63
|
+
brain,
|
|
64
|
+
repoPath: key,
|
|
65
|
+
lastAccess: Date.now(),
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
activeOps: 0
|
|
68
|
+
});
|
|
69
|
+
return brain;
|
|
61
70
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Execute an operation with active-op tracking.
|
|
73
|
+
* Prevents the workspace from being evicted while the operation runs.
|
|
74
|
+
*/
|
|
75
|
+
async withBrain(repoPath, fn) {
|
|
76
|
+
const brain = await this.get(repoPath);
|
|
77
|
+
const key = repoPath.replace(/\/+$/, "");
|
|
78
|
+
const entry = this._pool.get(key);
|
|
79
|
+
if (entry) entry.activeOps++;
|
|
65
80
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (dbSize > 1e5) {
|
|
71
|
-
evictPool(resolved);
|
|
72
|
-
} else {
|
|
73
|
-
entry.lastAccess = Date.now();
|
|
74
|
-
return entry.brain;
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
81
|
+
return await fn(brain);
|
|
82
|
+
} finally {
|
|
83
|
+
if (entry) {
|
|
84
|
+
entry.activeOps--;
|
|
77
85
|
entry.lastAccess = Date.now();
|
|
78
|
-
return entry.brain;
|
|
79
86
|
}
|
|
80
|
-
} catch {
|
|
81
|
-
entry.lastAccess = Date.now();
|
|
82
|
-
return entry.brain;
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
/** Manually evict a specific workspace. */
|
|
90
|
+
evict(repoPath) {
|
|
91
|
+
const key = repoPath.replace(/\/+$/, "");
|
|
92
|
+
this._evictEntry(key);
|
|
93
|
+
}
|
|
94
|
+
/** Get pool statistics. */
|
|
95
|
+
stats() {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
let totalMemory = 0;
|
|
98
|
+
const entries = [];
|
|
99
|
+
for (const entry of this._pool.values()) {
|
|
100
|
+
const memBytes = entry.brain.memoryHint();
|
|
101
|
+
const memMB = Math.round(memBytes / 1024 / 1024 * 100) / 100;
|
|
102
|
+
totalMemory += memBytes;
|
|
103
|
+
entries.push({
|
|
104
|
+
repoPath: entry.repoPath,
|
|
105
|
+
lastAccessAgo: formatAgo(now - entry.lastAccess),
|
|
106
|
+
memoryMB: memMB,
|
|
107
|
+
activeOps: entry.activeOps
|
|
108
|
+
});
|
|
94
109
|
}
|
|
95
|
-
|
|
110
|
+
return {
|
|
111
|
+
size: this._pool.size,
|
|
112
|
+
totalMemoryMB: Math.round(totalMemory / 1024 / 1024 * 100) / 100,
|
|
113
|
+
entries
|
|
114
|
+
};
|
|
96
115
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
opts.embeddingDims = _sharedEmbedding.dims;
|
|
116
|
+
/** Close all entries and stop the eviction timer. */
|
|
117
|
+
close() {
|
|
118
|
+
if (this._timer) {
|
|
119
|
+
clearInterval(this._timer);
|
|
120
|
+
this._timer = null;
|
|
121
|
+
}
|
|
122
|
+
for (const key of [...this._pool.keys()]) {
|
|
123
|
+
this._evictEntry(key);
|
|
124
|
+
}
|
|
107
125
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
fs.unlinkSync(dbPath);
|
|
116
|
-
} catch {
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
fs.unlinkSync(dbPath + "-wal");
|
|
120
|
-
} catch {
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
fs.unlinkSync(dbPath + "-shm");
|
|
124
|
-
} catch {
|
|
126
|
+
/** Evict workspaces that haven't been accessed within the TTL. */
|
|
127
|
+
_evictStale() {
|
|
128
|
+
const cutoff = Date.now() - this._ttlMs;
|
|
129
|
+
for (const [key, entry] of this._pool) {
|
|
130
|
+
if (entry.lastAccess < cutoff && entry.activeOps === 0) {
|
|
131
|
+
this._evictEntry(key);
|
|
125
132
|
}
|
|
126
|
-
const fresh = new BrainBank(opts).use(code({ repoPath: resolved })).use(git({ repoPath: resolved })).use(docs());
|
|
127
|
-
await fresh.initialize();
|
|
128
|
-
return fresh;
|
|
129
133
|
}
|
|
130
|
-
throw err;
|
|
131
134
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
/** Evict oldest idle entries until total memory is under the limit. */
|
|
136
|
+
_evictByMemoryPressure() {
|
|
137
|
+
let totalMemory = 0;
|
|
138
|
+
for (const entry of this._pool.values()) {
|
|
139
|
+
totalMemory += entry.brain.memoryHint();
|
|
140
|
+
}
|
|
141
|
+
if (totalMemory < this._maxMemoryBytes) return;
|
|
142
|
+
const candidates = [...this._pool.entries()].filter(([, e]) => e.activeOps === 0).sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
|
|
143
|
+
for (const [key, entry] of candidates) {
|
|
144
|
+
if (totalMemory < this._maxMemoryBytes) break;
|
|
145
|
+
totalMemory -= entry.brain.memoryHint();
|
|
146
|
+
this._evictEntry(key);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Evict a single entry by key. */
|
|
150
|
+
_evictEntry(key) {
|
|
151
|
+
const entry = this._pool.get(key);
|
|
152
|
+
if (!entry) return;
|
|
138
153
|
try {
|
|
139
154
|
entry.brain.close();
|
|
140
|
-
} catch {
|
|
155
|
+
} catch (err) {
|
|
156
|
+
this._onError?.(key, err);
|
|
141
157
|
}
|
|
142
|
-
_pool.delete(
|
|
158
|
+
this._pool.delete(key);
|
|
159
|
+
this._onEvict?.(key);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/workspace-factory.ts
|
|
164
|
+
import * as fs from "fs";
|
|
165
|
+
import * as path from "path";
|
|
166
|
+
function findRepoRoot(startDir) {
|
|
167
|
+
let dir = path.resolve(startDir);
|
|
168
|
+
while (true) {
|
|
169
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
170
|
+
const parent = path.dirname(dir);
|
|
171
|
+
if (parent === dir) break;
|
|
172
|
+
dir = parent;
|
|
143
173
|
}
|
|
174
|
+
return path.resolve(startDir);
|
|
144
175
|
}
|
|
145
|
-
__name(
|
|
176
|
+
__name(findRepoRoot, "findRepoRoot");
|
|
177
|
+
function resolveRepoPath(targetRepo) {
|
|
178
|
+
const rp = targetRepo ?? process.env.BRAINBANK_REPO ?? findRepoRoot(process.cwd());
|
|
179
|
+
return rp.replace(/\/+$/, "");
|
|
180
|
+
}
|
|
181
|
+
__name(resolveRepoPath, "resolveRepoPath");
|
|
182
|
+
async function createWorkspaceBrain(repoPath) {
|
|
183
|
+
const { createBrain, resetFactoryCache } = await import("brainbank");
|
|
184
|
+
resetFactoryCache();
|
|
185
|
+
const context = {
|
|
186
|
+
repoPath,
|
|
187
|
+
env: process.env
|
|
188
|
+
};
|
|
189
|
+
const origLog = console.log;
|
|
190
|
+
console.log = (...args) => console.error(...args);
|
|
191
|
+
try {
|
|
192
|
+
const brain = await createBrain(context);
|
|
193
|
+
await brain.initialize();
|
|
194
|
+
return brain;
|
|
195
|
+
} finally {
|
|
196
|
+
console.log = origLog;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
__name(createWorkspaceBrain, "createWorkspaceBrain");
|
|
200
|
+
|
|
201
|
+
// src/mcp-server.ts
|
|
202
|
+
var pool = new WorkspacePool({
|
|
203
|
+
factory: createWorkspaceBrain,
|
|
204
|
+
maxMemoryMB: parseInt(process.env.BRAINBANK_MAX_MEMORY_MB ?? "2048", 10),
|
|
205
|
+
ttlMinutes: parseInt(process.env.BRAINBANK_TTL_MINUTES ?? "30", 10),
|
|
206
|
+
onError: /* @__PURE__ */ __name((repo, err) => {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
console.error(`BrainBank pool error [${repo}]: ${msg}`);
|
|
209
|
+
}, "onError")
|
|
210
|
+
});
|
|
211
|
+
async function getBrainBank(targetRepo) {
|
|
212
|
+
return pool.get(resolveRepoPath(targetRepo));
|
|
213
|
+
}
|
|
214
|
+
__name(getBrainBank, "getBrainBank");
|
|
146
215
|
var server = new McpServer({
|
|
147
216
|
name: "brainbank",
|
|
148
|
-
version: "0.
|
|
217
|
+
version: "0.4.0"
|
|
149
218
|
});
|
|
150
|
-
server.registerTool(
|
|
151
|
-
"brainbank_search",
|
|
152
|
-
{
|
|
153
|
-
title: "BrainBank Search",
|
|
154
|
-
description: "Search indexed code and git commits. Supports three modes:\n- hybrid (default): vector + BM25 fused with RRF \u2014 best quality\n- vector: semantic similarity only\n- keyword: instant BM25 for exact terms, function names, error messages",
|
|
155
|
-
inputSchema: z.object({
|
|
156
|
-
query: z.string().describe("Search query \u2014 works with both keywords and natural language"),
|
|
157
|
-
mode: z.enum(["hybrid", "vector", "keyword"]).optional().default("hybrid").describe("Search strategy"),
|
|
158
|
-
codeK: z.number().optional().default(8).describe("Max code results"),
|
|
159
|
-
gitK: z.number().optional().default(5).describe("Max git results"),
|
|
160
|
-
minScore: z.number().optional().default(0.25).describe("Minimum similarity score (0-1), only for vector mode"),
|
|
161
|
-
collections: z.record(z.string(), z.number()).optional().describe(
|
|
162
|
-
'Max results per source. Reserved: "code", "git", "docs". Any other key = KV collection.'
|
|
163
|
-
),
|
|
164
|
-
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)")
|
|
165
|
-
})
|
|
166
|
-
},
|
|
167
|
-
async ({ query, mode, codeK, gitK, minScore, collections, repo }) => {
|
|
168
|
-
const brainbank = await getBrainBank(repo);
|
|
169
|
-
let results;
|
|
170
|
-
if (mode === "keyword") {
|
|
171
|
-
results = await brainbank.searchBM25(query, { codeK, gitK });
|
|
172
|
-
} else if (mode === "vector") {
|
|
173
|
-
results = await brainbank.search(query, { codeK, gitK, minScore });
|
|
174
|
-
} else {
|
|
175
|
-
results = await brainbank.hybridSearch(query, { codeK, gitK, collections });
|
|
176
|
-
}
|
|
177
|
-
if (results.length === 0) {
|
|
178
|
-
return { content: [{ type: "text", text: "No results found." }] };
|
|
179
|
-
}
|
|
180
|
-
const modeLabel = mode === "keyword" ? "Keyword (BM25)" : mode === "vector" ? "Vector" : "Hybrid (Vector + BM25 \u2192 RRF)";
|
|
181
|
-
return { content: [{ type: "text", text: formatResults(results, modeLabel) }] };
|
|
182
|
-
}
|
|
183
|
-
);
|
|
184
219
|
server.registerTool(
|
|
185
220
|
"brainbank_context",
|
|
186
221
|
{
|
|
187
222
|
title: "BrainBank Context",
|
|
188
|
-
description: "Get a formatted knowledge context block for a task. Returns
|
|
223
|
+
description: "Get a formatted knowledge context block for a task. Returns a Workflow Trace: search hits + full call tree with `called by` annotations, topologically ordered. All source code included \u2014 no trimming, no truncation.",
|
|
189
224
|
inputSchema: z.object({
|
|
190
225
|
task: z.string().describe("Description of the task you need context for"),
|
|
191
226
|
affectedFiles: z.array(z.string()).optional().default([]).describe("Files you plan to modify (improves co-edit suggestions)"),
|
|
192
|
-
codeResults: z.number().optional().default(
|
|
227
|
+
codeResults: z.number().optional().default(20).describe("Max code results"),
|
|
193
228
|
gitResults: z.number().optional().default(5).describe("Max git commit results"),
|
|
194
|
-
|
|
229
|
+
docsResults: z.number().optional().describe("Max document results (omit to skip docs)"),
|
|
230
|
+
sources: z.record(z.number()).optional().describe("Per-source result limits, overrides codeResults/gitResults/docsResults (e.g. { code: 10, git: 0, docs: 5 })"),
|
|
231
|
+
path: z.string().optional().describe("Filter results to files under this path prefix (e.g. src/services/)"),
|
|
232
|
+
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)"),
|
|
233
|
+
// BrainBankQL context fields
|
|
234
|
+
lines: z.boolean().optional().describe("Prefix each code line with its source line number (e.g. 127| code)"),
|
|
235
|
+
symbols: z.boolean().optional().describe("Append symbol index (all functions, classes, interfaces) for matched files"),
|
|
236
|
+
compact: z.boolean().optional().describe("Show only function/class signatures, skip bodies"),
|
|
237
|
+
callTree: z.union([z.boolean(), z.object({ depth: z.number() })]).optional().describe("Include call tree expansion. Pass { depth: N } to control depth"),
|
|
238
|
+
imports: z.boolean().optional().describe("Include dependency/import summary section"),
|
|
239
|
+
expander: z.boolean().optional().describe("Enable LLM-powered context expansion to discover related chunks not found by search")
|
|
195
240
|
})
|
|
196
241
|
},
|
|
197
|
-
async ({ task, affectedFiles, codeResults, gitResults, repo }) => {
|
|
242
|
+
async ({ task, affectedFiles, codeResults, gitResults, docsResults, sources, path: path2, repo, lines, symbols, compact, callTree, imports, expander }) => {
|
|
243
|
+
const repoPath = resolveRepoPath(repo);
|
|
198
244
|
const brainbank = await getBrainBank(repo);
|
|
245
|
+
const base = { code: codeResults, git: gitResults };
|
|
246
|
+
if (docsResults !== void 0) base.docs = docsResults;
|
|
247
|
+
const resolvedSources = sources ? { ...base, ...sources } : base;
|
|
248
|
+
const fields = {};
|
|
249
|
+
if (lines !== void 0) fields.lines = lines;
|
|
250
|
+
if (symbols !== void 0) fields.symbols = symbols;
|
|
251
|
+
if (compact !== void 0) fields.compact = compact;
|
|
252
|
+
if (callTree !== void 0) fields.callTree = callTree;
|
|
253
|
+
if (imports !== void 0) fields.imports = imports;
|
|
254
|
+
if (expander !== void 0) fields.expander = expander;
|
|
199
255
|
const context = await brainbank.getContext(task, {
|
|
200
256
|
affectedFiles,
|
|
201
|
-
|
|
202
|
-
|
|
257
|
+
sources: resolvedSources,
|
|
258
|
+
pathPrefix: path2,
|
|
259
|
+
source: "mcp",
|
|
260
|
+
fields: Object.keys(fields).length > 0 ? fields : void 0
|
|
203
261
|
});
|
|
204
262
|
return { content: [{ type: "text", text: context }] };
|
|
205
263
|
}
|
|
@@ -208,195 +266,123 @@ server.registerTool(
|
|
|
208
266
|
"brainbank_index",
|
|
209
267
|
{
|
|
210
268
|
title: "BrainBank Index",
|
|
211
|
-
description: "
|
|
269
|
+
description: "Re-index code, git history, and docs. Requires .brainbank/config.json to exist. Incremental \u2014 only changed files are processed.",
|
|
212
270
|
inputSchema: z.object({
|
|
213
|
-
modules: z.array(z.enum(["code", "git", "docs"])).optional().describe("Which modules to index (default: all)"),
|
|
214
|
-
docsPath: z.string().optional().describe("Path to a docs folder to register and index"),
|
|
215
271
|
forceReindex: z.boolean().optional().default(false).describe("Force re-index of all files"),
|
|
216
|
-
gitDepth: z.number().optional().default(500).describe("Number of git commits to index"),
|
|
217
272
|
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)")
|
|
218
273
|
})
|
|
219
274
|
},
|
|
220
|
-
async ({
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
async ({ forceReindex, repo }) => {
|
|
276
|
+
const repoPath = resolveRepoPath(repo);
|
|
277
|
+
if (!existsSync2(`${repoPath}/.brainbank/config.json`)) {
|
|
278
|
+
return {
|
|
279
|
+
content: [{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: `BrainBank: No .brainbank/config.json found at ${repoPath}.
|
|
282
|
+
|
|
283
|
+
## How to set up
|
|
284
|
+
|
|
285
|
+
Create \`${repoPath}/.brainbank/config.json\` with:
|
|
286
|
+
|
|
287
|
+
\`\`\`json
|
|
288
|
+
{
|
|
289
|
+
"plugins": ["code"],
|
|
290
|
+
"code": {
|
|
291
|
+
"embedding": "perplexity-context",
|
|
292
|
+
"ignore": [
|
|
293
|
+
"node_modules/**", "dist/**", "build/**",
|
|
294
|
+
".next/**", "coverage/**", "__pycache__/**",
|
|
295
|
+
"**/*.min.js", "**/*.min.css",
|
|
296
|
+
"tests/**", "test/**",
|
|
297
|
+
"**/test_*.py", "**/*_test.py",
|
|
298
|
+
"**/*.test.ts", "**/*.spec.ts"
|
|
299
|
+
]
|
|
300
|
+
},
|
|
301
|
+
"embedding": "perplexity-context"
|
|
302
|
+
}
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
**Embedding options:** \`local\` (free, offline), \`openai\`, \`perplexity\`, \`perplexity-context\` (best quality)
|
|
306
|
+
**Plugins available:** \`code\`, \`git\`, \`docs\`
|
|
307
|
+
|
|
308
|
+
Then run:
|
|
309
|
+
\`\`\`bash
|
|
310
|
+
brainbank index . --force --yes
|
|
311
|
+
\`\`\``
|
|
312
|
+
}]
|
|
313
|
+
};
|
|
234
314
|
}
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
315
|
+
const brainbank = await getBrainBank(repo);
|
|
316
|
+
const result = await brainbank.index({ forceReindex });
|
|
317
|
+
const lines = ["## Indexing Complete", ""];
|
|
318
|
+
const codeResult = result.code;
|
|
319
|
+
const gitResult = result.git;
|
|
320
|
+
lines.push(`**Code**: ${codeResult?.indexed ?? 0} files indexed, ${codeResult?.skipped ?? 0} skipped, ${codeResult?.chunks ?? 0} chunks`);
|
|
321
|
+
lines.push(`**Git**: ${gitResult?.indexed ?? 0} commits indexed, ${gitResult?.skipped ?? 0} skipped`);
|
|
322
|
+
const docsResult = result.docs;
|
|
323
|
+
if (docsResult) {
|
|
324
|
+
for (const [name, stat] of Object.entries(docsResult)) {
|
|
244
325
|
lines.push(`**Docs [${name}]**: ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);
|
|
245
326
|
}
|
|
246
327
|
}
|
|
247
328
|
const stats = brainbank.stats();
|
|
329
|
+
const codeStats = stats.code;
|
|
330
|
+
const gitStats = stats.git;
|
|
331
|
+
const docStats = stats.documents;
|
|
248
332
|
lines.push("");
|
|
249
|
-
lines.push(`**Totals**: ${
|
|
250
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
251
|
-
}
|
|
252
|
-
);
|
|
253
|
-
server.registerTool(
|
|
254
|
-
"brainbank_stats",
|
|
255
|
-
{
|
|
256
|
-
title: "BrainBank Stats",
|
|
257
|
-
description: "Get index statistics: file count, code chunks, git commits, HNSW sizes, KV collections.",
|
|
258
|
-
inputSchema: z.object({
|
|
259
|
-
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)")
|
|
260
|
-
})
|
|
261
|
-
},
|
|
262
|
-
async ({ repo }) => {
|
|
263
|
-
const brainbank = await getBrainBank(repo);
|
|
264
|
-
const s = brainbank.stats();
|
|
265
|
-
const lines = ["## BrainBank Stats", ""];
|
|
266
|
-
if (s.code) {
|
|
267
|
-
lines.push(`**Code**: ${s.code.files} files, ${s.code.chunks} chunks, ${s.code.hnswSize} vectors`);
|
|
268
|
-
}
|
|
269
|
-
if (s.git) {
|
|
270
|
-
lines.push(`**Git**: ${s.git.commits} commits, ${s.git.filesTracked} files, ${s.git.coEdits} co-edit pairs`);
|
|
271
|
-
}
|
|
272
|
-
if (s.documents) {
|
|
273
|
-
lines.push(`**Docs**: ${s.documents.collections} collections, ${s.documents.documents} documents`);
|
|
274
|
-
}
|
|
275
|
-
const kvNames = brainbank.listCollectionNames();
|
|
276
|
-
if (kvNames.length > 0) {
|
|
277
|
-
lines.push("");
|
|
278
|
-
lines.push("**KV Collections**:");
|
|
279
|
-
for (const name of kvNames) {
|
|
280
|
-
const coll = brainbank.collection(name);
|
|
281
|
-
lines.push(`- ${name}: ${coll.count()} items`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
333
|
+
lines.push(`**Totals**: ${codeStats?.chunks ?? 0} code chunks, ${gitStats?.commits ?? 0} commits, ${docStats?.documents ?? 0} docs`);
|
|
284
334
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
285
335
|
}
|
|
286
336
|
);
|
|
287
337
|
server.registerTool(
|
|
288
|
-
"
|
|
338
|
+
"brainbank_files",
|
|
289
339
|
{
|
|
290
|
-
title: "BrainBank
|
|
291
|
-
description:
|
|
340
|
+
title: "BrainBank Files",
|
|
341
|
+
description: 'Fetch full file contents from the index. Use AFTER brainbank_context to view complete files identified by search. No semantic search runs \u2014 this is a direct file viewer.\n\nSupports:\n- Exact paths: "src/auth/login.ts"\n- Directories: "src/graph/" (trailing / = all files under path)\n- Glob patterns: "src/**/*.service.ts"\n- Fuzzy basename: "plugin.ts" (matches src/plugin.ts when exact fails)',
|
|
292
342
|
inputSchema: z.object({
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
async ({ filePath, limit, repo }) => {
|
|
299
|
-
const brainbank = await getBrainBank(repo);
|
|
300
|
-
const history = await brainbank.fileHistory(filePath, limit);
|
|
301
|
-
if (history.length === 0) {
|
|
302
|
-
return { content: [{ type: "text", text: `No git history found for "${filePath}"` }] };
|
|
303
|
-
}
|
|
304
|
-
const lines = [`## Git History: ${filePath}`, ""];
|
|
305
|
-
for (const h of history) {
|
|
306
|
-
lines.push(`**[${h.short_hash}]** ${h.message} *(${h.author}, +${h.additions}/-${h.deletions})*`);
|
|
307
|
-
}
|
|
308
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
309
|
-
}
|
|
310
|
-
);
|
|
311
|
-
server.registerTool(
|
|
312
|
-
"brainbank_collection",
|
|
313
|
-
{
|
|
314
|
-
title: "BrainBank Collection",
|
|
315
|
-
description: "Operate on KV collections (auto-created). Actions:\n- add: store content with optional metadata\n- search: hybrid vector + keyword search\n- trim: keep only N most recent items",
|
|
316
|
-
inputSchema: z.object({
|
|
317
|
-
action: z.enum(["add", "search", "trim"]).describe("Operation to perform"),
|
|
318
|
-
collection: z.string().describe('Collection name (e.g. "errors", "decisions")'),
|
|
319
|
-
content: z.string().optional().describe("Content to store (required for add)"),
|
|
320
|
-
query: z.string().optional().describe("Search query (required for search)"),
|
|
321
|
-
metadata: z.record(z.any()).optional().default({}).describe("Metadata for add"),
|
|
322
|
-
k: z.number().optional().default(5).describe("Max results for search"),
|
|
323
|
-
keep: z.number().optional().describe("Items to keep for trim"),
|
|
324
|
-
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)")
|
|
343
|
+
files: z.array(z.string()).describe(
|
|
344
|
+
"File paths to fetch. Exact paths, directories (trailing /), glob patterns (e.g. src/**/*.ts), or fuzzy basenames."
|
|
345
|
+
),
|
|
346
|
+
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)"),
|
|
347
|
+
lines: z.boolean().optional().describe("Prefix each line with source line number")
|
|
325
348
|
})
|
|
326
349
|
},
|
|
327
|
-
async ({
|
|
350
|
+
async ({ files, repo, lines }) => {
|
|
328
351
|
const brainbank = await getBrainBank(repo);
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
const id = await coll.add(content, metadata);
|
|
333
|
-
return {
|
|
334
|
-
content: [{ type: "text", text: `\u2713 Item #${id} added to '${collection}' (${coll.count()} total)` }]
|
|
335
|
-
};
|
|
352
|
+
const results = brainbank.resolveFiles(files);
|
|
353
|
+
if (results.length === 0) {
|
|
354
|
+
return { content: [{ type: "text", text: "No matching files found in the index." }] };
|
|
336
355
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
356
|
+
const parts = [];
|
|
357
|
+
for (const r of results) {
|
|
358
|
+
const meta = r.metadata;
|
|
359
|
+
const lang = meta.language ?? "";
|
|
360
|
+
const startLine = meta.startLine ?? 1;
|
|
361
|
+
parts.push(`## ${r.filePath}
|
|
362
|
+
`);
|
|
363
|
+
parts.push("```" + lang);
|
|
364
|
+
if (lines) {
|
|
365
|
+
const codeLines = r.content.split("\n");
|
|
366
|
+
const pad = String(startLine + codeLines.length - 1).length;
|
|
367
|
+
parts.push(codeLines.map(
|
|
368
|
+
(l, i) => `${String(startLine + i).padStart(pad)}| ${l}`
|
|
369
|
+
).join("\n"));
|
|
370
|
+
} else {
|
|
371
|
+
parts.push(r.content);
|
|
351
372
|
}
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
if (action === "trim") {
|
|
355
|
-
if (keep == null) throw new Error("BrainBank: keep is required for trim action.");
|
|
356
|
-
const result = await coll.trim({ keep });
|
|
357
|
-
return {
|
|
358
|
-
content: [{ type: "text", text: `\u2713 Trimmed ${result.removed} items from '${collection}' (kept ${keep})` }]
|
|
359
|
-
};
|
|
373
|
+
parts.push("```\n");
|
|
360
374
|
}
|
|
361
|
-
|
|
375
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
362
376
|
}
|
|
363
377
|
);
|
|
364
|
-
function formatResults(results, mode) {
|
|
365
|
-
const lines = [`## ${mode}`, ""];
|
|
366
|
-
for (const r of results) {
|
|
367
|
-
const score = Math.round(r.score * 100);
|
|
368
|
-
if (r.type === "code") {
|
|
369
|
-
const m = r.metadata;
|
|
370
|
-
lines.push(`[CODE ${score}%] ${r.filePath} \u2014 ${m.name || m.chunkType} (L${m.startLine}-${m.endLine})`);
|
|
371
|
-
lines.push(r.content);
|
|
372
|
-
lines.push("");
|
|
373
|
-
} else if (r.type === "commit") {
|
|
374
|
-
const m = r.metadata;
|
|
375
|
-
lines.push(`[COMMIT ${score}%] ${m.shortHash} \u2014 ${r.content} (${m.author})`);
|
|
376
|
-
if (m.files?.length) lines.push(` Files: ${m.files.join(", ")}`);
|
|
377
|
-
lines.push("");
|
|
378
|
-
} else if (r.type === "document") {
|
|
379
|
-
const ctx = r.context ? ` \u2014 ${r.context}` : "";
|
|
380
|
-
lines.push(`[DOC ${score}%] ${r.filePath} [${r.metadata.collection}]${ctx}`);
|
|
381
|
-
lines.push(r.content);
|
|
382
|
-
lines.push("");
|
|
383
|
-
} else if (r.type === "collection") {
|
|
384
|
-
const col = r.metadata?.collection ?? "unknown";
|
|
385
|
-
lines.push(`[COLLECTION ${score}%] [${col}]`);
|
|
386
|
-
lines.push(r.content);
|
|
387
|
-
lines.push("");
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return lines.join("\n");
|
|
391
|
-
}
|
|
392
|
-
__name(formatResults, "formatResults");
|
|
393
378
|
async function main() {
|
|
394
379
|
const transport = new StdioServerTransport();
|
|
395
380
|
await server.connect(transport);
|
|
396
381
|
}
|
|
397
382
|
__name(main, "main");
|
|
398
383
|
main().catch((err) => {
|
|
399
|
-
|
|
384
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
385
|
+
console.error(`BrainBank MCP Server Error: ${message}`);
|
|
400
386
|
process.exit(1);
|
|
401
387
|
});
|
|
402
388
|
//# sourceMappingURL=mcp-server.js.map
|