@brainbank/mcp 0.2.0 → 0.3.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 +88 -42
- package/package.json +14 -4
- 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 +0 -402
- package/dist/mcp-server.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @brainbank/mcp
|
|
2
2
|
|
|
3
|
-
[MCP](https://modelcontextprotocol.io/) server for [BrainBank](https://github.com/pinecall/brainbank) — exposes code
|
|
3
|
+
[MCP](https://modelcontextprotocol.io/) server for [BrainBank](https://github.com/pinecall/brainbank) — exposes code context as a single tool for AI agents via stdio transport.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
npm install @brainbank/mcp
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Plugin packages are optional peer dependencies — install whichever you need:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @brainbank/code @brainbank/git @brainbank/docs
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
## Quick Start
|
|
12
18
|
|
|
13
19
|
### Antigravity
|
|
@@ -19,10 +25,7 @@ Add to `~/.gemini/antigravity/mcp_config.json`:
|
|
|
19
25
|
"mcpServers": {
|
|
20
26
|
"brainbank": {
|
|
21
27
|
"command": "npx",
|
|
22
|
-
"args": ["-y", "@brainbank/mcp"]
|
|
23
|
-
"env": {
|
|
24
|
-
"BRAINBANK_EMBEDDING": "openai"
|
|
25
|
-
}
|
|
28
|
+
"args": ["-y", "@brainbank/mcp"]
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
}
|
|
@@ -37,11 +40,7 @@ Add to Claude Desktop settings → Developer → MCP Servers:
|
|
|
37
40
|
"mcpServers": {
|
|
38
41
|
"brainbank": {
|
|
39
42
|
"command": "npx",
|
|
40
|
-
"args": ["-y", "@brainbank/mcp"]
|
|
41
|
-
"env": {
|
|
42
|
-
"BRAINBANK_EMBEDDING": "openai",
|
|
43
|
-
"OPENAI_API_KEY": "sk-..."
|
|
44
|
-
}
|
|
43
|
+
"args": ["-y", "@brainbank/mcp"]
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
}
|
|
@@ -56,10 +55,7 @@ Add to `.cursor/mcp.json` in your project:
|
|
|
56
55
|
"mcpServers": {
|
|
57
56
|
"brainbank": {
|
|
58
57
|
"command": "npx",
|
|
59
|
-
"args": ["-y", "@brainbank/mcp"]
|
|
60
|
-
"env": {
|
|
61
|
-
"BRAINBANK_EMBEDDING": "openai"
|
|
62
|
-
}
|
|
58
|
+
"args": ["-y", "@brainbank/mcp"]
|
|
63
59
|
}
|
|
64
60
|
}
|
|
65
61
|
}
|
|
@@ -71,41 +67,90 @@ Add to `.cursor/mcp.json` in your project:
|
|
|
71
67
|
brainbank serve
|
|
72
68
|
```
|
|
73
69
|
|
|
74
|
-
##
|
|
70
|
+
## Zero-Config
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|----------|-------------|---------|
|
|
78
|
-
| `BRAINBANK_REPO` | Default repo path (fallback if `repo` param not provided) | — |
|
|
79
|
-
| `BRAINBANK_EMBEDDING` | Embedding provider: `local`, `openai`, `perplexity`, `perplexity-context` | `local` |
|
|
80
|
-
| `OPENAI_API_KEY` | Required when using `openai` embeddings | — |
|
|
81
|
-
| `PERPLEXITY_API_KEY` | Required when using `perplexity` or `perplexity-context` embeddings | — |
|
|
72
|
+
The MCP server auto-detects everything:
|
|
82
73
|
|
|
83
|
-
|
|
74
|
+
- **Repo path** — from `repo` tool param > `BRAINBANK_REPO` env > `findRepoRoot(cwd)`
|
|
75
|
+
- **Embedding provider** — from `.brainbank/config.json` > `BRAINBANK_EMBEDDING` env > `provider_key` stored in DB > falls back to local
|
|
76
|
+
- **Plugins** — reads `plugins` array from `config.json` (default: `['code']`). Loaded dynamically by the core factory — no hardcoded imports
|
|
84
77
|
|
|
85
|
-
|
|
78
|
+
Index your repo once with the CLI:
|
|
86
79
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| `brainbank_context` | Formatted context block for a task (code + git + co-edits) |
|
|
91
|
-
| `brainbank_index` | Trigger incremental code/git/docs indexing |
|
|
92
|
-
| `brainbank_stats` | Index statistics (files, commits, chunks, collections) |
|
|
93
|
-
| `brainbank_history` | Git history for a specific file |
|
|
94
|
-
| `brainbank_collection` | KV collection ops — `action: add`, `search`, or `trim` |
|
|
80
|
+
```bash
|
|
81
|
+
brainbank index . --yes
|
|
82
|
+
```
|
|
95
83
|
|
|
96
|
-
|
|
84
|
+
After that, the MCP server auto-resolves the correct provider — no env vars needed.
|
|
85
|
+
|
|
86
|
+
## Tools (2)
|
|
87
|
+
|
|
88
|
+
### `brainbank_context`
|
|
89
|
+
|
|
90
|
+
**Primary tool.** Returns a Workflow Trace:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
brainbank_context({
|
|
94
|
+
task: string, // what you're trying to understand or implement
|
|
95
|
+
affectedFiles?: string[], // files you plan to modify (improves co-edit suggestions)
|
|
96
|
+
codeResults?: number, // max code results (default: 6)
|
|
97
|
+
gitResults?: number, // max git commit results (default: 5)
|
|
98
|
+
repo?: string, // repository path (default: auto-detect)
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Returns a **Workflow Trace** — a single flat `## Code Context` section with:
|
|
103
|
+
- Search hits with `% match` scores
|
|
104
|
+
- Full call tree (3 levels deep) with `called by` annotations
|
|
105
|
+
- Part adjacency boost (multi-part functions shown complete)
|
|
106
|
+
- Trivial wrapper collapse (one-liners for delegation)
|
|
107
|
+
- All source code included — no trimming, no truncation
|
|
97
108
|
|
|
98
|
-
|
|
109
|
+
If the project is **not indexed**, the tool returns an error with the CLI command to run.
|
|
110
|
+
|
|
111
|
+
### `brainbank_index`
|
|
112
|
+
|
|
113
|
+
Re-index code/git/docs. Requires `.brainbank/config.json`. Incremental — only changed files are processed.
|
|
99
114
|
|
|
100
115
|
```typescript
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
brainbank_index({
|
|
117
|
+
forceReindex?: boolean, // force full re-index (default: false)
|
|
118
|
+
repo?: string, // repository path
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Multi-Workspace
|
|
103
123
|
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
The MCP server manages a `WorkspacePool` of BrainBank instances — one per unique `repo` path. The pool uses memory-pressure eviction (configurable max memory) and TTL eviction (configurable idle timeout):
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
brainbank_context({ task: "login form", repo: "/project-a" })
|
|
128
|
+
brainbank_context({ task: "API routes", repo: "/project-b" })
|
|
106
129
|
```
|
|
107
130
|
|
|
108
|
-
Instances are cached in memory after first initialization (~480ms)
|
|
131
|
+
Instances are cached in memory after first initialization (~480ms). Active operations are tracked — the pool never evicts a workspace with in-flight queries.
|
|
132
|
+
|
|
133
|
+
## Environment Variables
|
|
134
|
+
|
|
135
|
+
All optional — the server works without any env vars.
|
|
136
|
+
|
|
137
|
+
| Variable | Description | Default |
|
|
138
|
+
|----------|-------------|---------|
|
|
139
|
+
| `BRAINBANK_REPO` | Fallback repo path | auto-detect from cwd |
|
|
140
|
+
| `BRAINBANK_EMBEDDING` | Embedding provider key | from `config.json` or DB |
|
|
141
|
+
| `BRAINBANK_MAX_MEMORY_MB` | Maximum total pool memory in MB | `2048` |
|
|
142
|
+
| `BRAINBANK_TTL_MINUTES` | Idle workspace eviction timeout in minutes | `30` |
|
|
143
|
+
| `OPENAI_API_KEY` | Required when embedding provider is `openai` | — |
|
|
144
|
+
| `PERPLEXITY_API_KEY` | Required when embedding provider is `perplexity` / `perplexity-context` | — |
|
|
145
|
+
|
|
146
|
+
## Architecture
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
@brainbank/mcp
|
|
150
|
+
├── mcp-server.ts ← MCP stdio server (1 tool: brainbank_context)
|
|
151
|
+
├── workspace-pool.ts ← Memory-pressure + TTL eviction, active-op tracking
|
|
152
|
+
└── workspace-factory.ts ← Delegates to core createBrain() — no plugin hardcoding
|
|
153
|
+
```
|
|
109
154
|
|
|
110
155
|
## How it works
|
|
111
156
|
|
|
@@ -113,10 +158,11 @@ Instances are cached in memory after first initialization (~480ms), so subsequen
|
|
|
113
158
|
AI Agent ←→ stdio ←→ @brainbank/mcp ←→ BrainBank core ←→ SQLite
|
|
114
159
|
```
|
|
115
160
|
|
|
116
|
-
1. Agent sends
|
|
117
|
-
2.
|
|
118
|
-
3. BrainBank
|
|
119
|
-
4.
|
|
161
|
+
1. Agent sends `brainbank_context({ task: "..." })`
|
|
162
|
+
2. `WorkspacePool` resolves `repo` → gets/creates a BrainBank instance
|
|
163
|
+
3. BrainBank calls `ensureFresh()` → hot-reloads stale HNSW if needed
|
|
164
|
+
4. BrainBank executes search + call tree + formatting
|
|
165
|
+
5. Workflow Trace returned as markdown to the agent
|
|
120
166
|
|
|
121
167
|
## License
|
|
122
168
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainbank/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for BrainBank",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"bin": "
|
|
6
|
+
"main": "src/mcp-server.ts",
|
|
7
|
+
"bin": "src/mcp-server.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"
|
|
9
|
+
"src/"
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsup"
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
17
17
|
"zod": "^4.3.6"
|
|
18
18
|
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@brainbank/code": ">=0.1.0",
|
|
21
|
+
"@brainbank/git": ">=0.1.0",
|
|
22
|
+
"@brainbank/docs": ">=0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"@brainbank/code": { "optional": true },
|
|
26
|
+
"@brainbank/git": { "optional": true },
|
|
27
|
+
"@brainbank/docs": { "optional": true }
|
|
28
|
+
},
|
|
19
29
|
"repository": {
|
|
20
30
|
"type": "git",
|
|
21
31
|
"url": "git+https://github.com/pinecall/brainbank.git",
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BrainBank — MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Exposes BrainBank as an MCP server via stdio transport.
|
|
7
|
+
* Works with Google Antigravity, Claude Desktop, and any MCP-compatible client.
|
|
8
|
+
*
|
|
9
|
+
* Usage in mcp_config.json:
|
|
10
|
+
* {
|
|
11
|
+
* "mcpServers": {
|
|
12
|
+
* "brainbank": {
|
|
13
|
+
* "command": "npx",
|
|
14
|
+
* "args": ["-y", "@brainbank/mcp"]
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Tools (2):
|
|
20
|
+
* brainbank_context — Workflow Trace: search + call tree + called-by annotations
|
|
21
|
+
* brainbank_index — Re-index (requires .brainbank/config.json)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
25
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
26
|
+
import { z } from 'zod/v3';
|
|
27
|
+
import { existsSync } from 'node:fs';
|
|
28
|
+
import { WorkspacePool } from './workspace-pool.js';
|
|
29
|
+
import { createWorkspaceBrain, resolveRepoPath } from './workspace-factory.js';
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// ── Multi-Workspace BrainBank Pool ─────────────────────
|
|
34
|
+
|
|
35
|
+
const pool = new WorkspacePool({
|
|
36
|
+
factory: createWorkspaceBrain,
|
|
37
|
+
maxMemoryMB: parseInt(process.env.BRAINBANK_MAX_MEMORY_MB ?? '2048', 10),
|
|
38
|
+
ttlMinutes: parseInt(process.env.BRAINBANK_TTL_MINUTES ?? '30', 10),
|
|
39
|
+
onError: (repo, err) => {
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
+
console.error(`BrainBank pool error [${repo}]: ${msg}`);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** Resolve repo and get a BrainBank from the pool. */
|
|
46
|
+
async function getBrainBank(targetRepo?: string) {
|
|
47
|
+
return pool.get(resolveRepoPath(targetRepo));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── MCP Server Setup ────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const server = new McpServer({
|
|
53
|
+
name: 'brainbank',
|
|
54
|
+
version: '0.4.0',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ── Tool: brainbank_context ─────────────────────────
|
|
58
|
+
|
|
59
|
+
server.registerTool(
|
|
60
|
+
'brainbank_context',
|
|
61
|
+
{
|
|
62
|
+
title: 'BrainBank Context',
|
|
63
|
+
description:
|
|
64
|
+
'Get a formatted knowledge context block for a task. Returns a Workflow Trace: ' +
|
|
65
|
+
'search hits + full call tree with `called by` annotations, topologically ordered. ' +
|
|
66
|
+
'All source code included — no trimming, no truncation.',
|
|
67
|
+
inputSchema: z.object({
|
|
68
|
+
task: z.string().describe('Description of the task you need context for'),
|
|
69
|
+
affectedFiles: z.array(z.string()).optional().default([]).describe('Files you plan to modify (improves co-edit suggestions)'),
|
|
70
|
+
codeResults: z.number().optional().default(20).describe('Max code results'),
|
|
71
|
+
gitResults: z.number().optional().default(5).describe('Max git commit results'),
|
|
72
|
+
docsResults: z.number().optional().describe('Max document results (omit to skip docs)'),
|
|
73
|
+
sources: z.record(z.number()).optional().describe('Per-source result limits, overrides codeResults/gitResults/docsResults (e.g. { code: 10, git: 0, docs: 5 })'),
|
|
74
|
+
path: z.string().optional().describe('Filter results to files under this path prefix (e.g. src/services/)'),
|
|
75
|
+
repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),
|
|
76
|
+
// BrainBankQL context fields
|
|
77
|
+
lines: z.boolean().optional().describe('Prefix each code line with its source line number (e.g. 127| code)'),
|
|
78
|
+
symbols: z.boolean().optional().describe('Append symbol index (all functions, classes, interfaces) for matched files'),
|
|
79
|
+
compact: z.boolean().optional().describe('Show only function/class signatures, skip bodies'),
|
|
80
|
+
callTree: z.union([z.boolean(), z.object({ depth: z.number() })]).optional().describe('Include call tree expansion. Pass { depth: N } to control depth'),
|
|
81
|
+
imports: z.boolean().optional().describe('Include dependency/import summary section'),
|
|
82
|
+
expander: z.boolean().optional().describe('Enable LLM-powered context expansion to discover related chunks not found by search'),
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
async ({ task, affectedFiles, codeResults, gitResults, docsResults, sources, path, repo, lines, symbols, compact, callTree, imports, expander }) => {
|
|
86
|
+
const repoPath = resolveRepoPath(repo);
|
|
87
|
+
const brainbank = await getBrainBank(repo);
|
|
88
|
+
|
|
89
|
+
// Build sources from explicit params, then let `sources` override
|
|
90
|
+
const base: Record<string, number> = { code: codeResults, git: gitResults };
|
|
91
|
+
if (docsResults !== undefined) base.docs = docsResults;
|
|
92
|
+
const resolvedSources = sources ? { ...base, ...sources } : base;
|
|
93
|
+
|
|
94
|
+
// Build fields from explicit params (only include defined values)
|
|
95
|
+
const fields: Record<string, unknown> = {};
|
|
96
|
+
if (lines !== undefined) fields.lines = lines;
|
|
97
|
+
if (symbols !== undefined) fields.symbols = symbols;
|
|
98
|
+
if (compact !== undefined) fields.compact = compact;
|
|
99
|
+
if (callTree !== undefined) fields.callTree = callTree;
|
|
100
|
+
if (imports !== undefined) fields.imports = imports;
|
|
101
|
+
if (expander !== undefined) fields.expander = expander;
|
|
102
|
+
|
|
103
|
+
const context = await brainbank.getContext(task, {
|
|
104
|
+
affectedFiles,
|
|
105
|
+
sources: resolvedSources,
|
|
106
|
+
pathPrefix: path,
|
|
107
|
+
source: 'mcp',
|
|
108
|
+
fields: Object.keys(fields).length > 0 ? fields : undefined,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return { content: [{ type: 'text' as const, text: context }] };
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// ── Tool: brainbank_index ───────────────────────────
|
|
116
|
+
|
|
117
|
+
server.registerTool(
|
|
118
|
+
'brainbank_index',
|
|
119
|
+
{
|
|
120
|
+
title: 'BrainBank Index',
|
|
121
|
+
description:
|
|
122
|
+
'Re-index code, git history, and docs. Requires .brainbank/config.json to exist. ' +
|
|
123
|
+
'Incremental — only changed files are processed.',
|
|
124
|
+
inputSchema: z.object({
|
|
125
|
+
forceReindex: z.boolean().optional().default(false).describe('Force re-index of all files'),
|
|
126
|
+
repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
async ({ forceReindex, repo }) => {
|
|
130
|
+
const repoPath = resolveRepoPath(repo);
|
|
131
|
+
|
|
132
|
+
// Require config.json — force users to set up structure first
|
|
133
|
+
if (!existsSync(`${repoPath}/.brainbank/config.json`)) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{
|
|
136
|
+
type: 'text' as const,
|
|
137
|
+
text:
|
|
138
|
+
`BrainBank: No .brainbank/config.json found at ${repoPath}.\n\n` +
|
|
139
|
+
`## How to set up\n\n` +
|
|
140
|
+
`Create \`${repoPath}/.brainbank/config.json\` with:\n\n` +
|
|
141
|
+
'```json\n' +
|
|
142
|
+
'{\n' +
|
|
143
|
+
' "plugins": ["code"],\n' +
|
|
144
|
+
' "code": {\n' +
|
|
145
|
+
' "embedding": "perplexity-context",\n' +
|
|
146
|
+
' "ignore": [\n' +
|
|
147
|
+
' "node_modules/**", "dist/**", "build/**",\n' +
|
|
148
|
+
' ".next/**", "coverage/**", "__pycache__/**",\n' +
|
|
149
|
+
' "**/*.min.js", "**/*.min.css",\n' +
|
|
150
|
+
' "tests/**", "test/**",\n' +
|
|
151
|
+
' "**/test_*.py", "**/*_test.py",\n' +
|
|
152
|
+
' "**/*.test.ts", "**/*.spec.ts"\n' +
|
|
153
|
+
' ]\n' +
|
|
154
|
+
' },\n' +
|
|
155
|
+
' "embedding": "perplexity-context"\n' +
|
|
156
|
+
'}\n' +
|
|
157
|
+
'```\n\n' +
|
|
158
|
+
`**Embedding options:** \`local\` (free, offline), \`openai\`, \`perplexity\`, \`perplexity-context\` (best quality)\n` +
|
|
159
|
+
`**Plugins available:** \`code\`, \`git\`, \`docs\`\n\n` +
|
|
160
|
+
`Then run:\n` +
|
|
161
|
+
'```bash\nbrainbank index . --force --yes\n```',
|
|
162
|
+
}],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const brainbank = await getBrainBank(repo);
|
|
167
|
+
const result = await brainbank.index({ forceReindex });
|
|
168
|
+
|
|
169
|
+
const lines = ['## Indexing Complete', ''];
|
|
170
|
+
|
|
171
|
+
const codeResult = result.code as { indexed?: number; skipped?: number; chunks?: number } | undefined;
|
|
172
|
+
const gitResult = result.git as { indexed?: number; skipped?: number } | undefined;
|
|
173
|
+
|
|
174
|
+
lines.push(`**Code**: ${codeResult?.indexed ?? 0} files indexed, ${codeResult?.skipped ?? 0} skipped, ${codeResult?.chunks ?? 0} chunks`);
|
|
175
|
+
lines.push(`**Git**: ${gitResult?.indexed ?? 0} commits indexed, ${gitResult?.skipped ?? 0} skipped`);
|
|
176
|
+
|
|
177
|
+
const docsResult = result.docs as Record<string, { indexed: number; skipped: number; chunks: number }> | undefined;
|
|
178
|
+
if (docsResult) {
|
|
179
|
+
for (const [name, stat] of Object.entries(docsResult)) {
|
|
180
|
+
lines.push(`**Docs [${name}]**: ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const stats = brainbank.stats();
|
|
185
|
+
const codeStats = stats.code as { chunks?: number } | undefined;
|
|
186
|
+
const gitStats = stats.git as { commits?: number } | undefined;
|
|
187
|
+
const docStats = stats.documents as { documents?: number } | undefined;
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(`**Totals**: ${codeStats?.chunks ?? 0} code chunks, ${gitStats?.commits ?? 0} commits, ${docStats?.documents ?? 0} docs`);
|
|
190
|
+
|
|
191
|
+
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// ── Tool: brainbank_files ───────────────────────────
|
|
196
|
+
|
|
197
|
+
server.registerTool(
|
|
198
|
+
'brainbank_files',
|
|
199
|
+
{
|
|
200
|
+
title: 'BrainBank Files',
|
|
201
|
+
description:
|
|
202
|
+
'Fetch full file contents from the index. Use AFTER brainbank_context ' +
|
|
203
|
+
'to view complete files identified by search. No semantic search runs — ' +
|
|
204
|
+
'this is a direct file viewer.\n\n' +
|
|
205
|
+
'Supports:\n' +
|
|
206
|
+
'- Exact paths: "src/auth/login.ts"\n' +
|
|
207
|
+
'- Directories: "src/graph/" (trailing / = all files under path)\n' +
|
|
208
|
+
'- Glob patterns: "src/**/*.service.ts"\n' +
|
|
209
|
+
'- Fuzzy basename: "plugin.ts" (matches src/plugin.ts when exact fails)',
|
|
210
|
+
inputSchema: z.object({
|
|
211
|
+
files: z.array(z.string()).describe(
|
|
212
|
+
'File paths to fetch. Exact paths, directories (trailing /), ' +
|
|
213
|
+
'glob patterns (e.g. src/**/*.ts), or fuzzy basenames.',
|
|
214
|
+
),
|
|
215
|
+
repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),
|
|
216
|
+
lines: z.boolean().optional().describe('Prefix each line with source line number'),
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
async ({ files, repo, lines }) => {
|
|
220
|
+
const brainbank = await getBrainBank(repo);
|
|
221
|
+
const results = brainbank.resolveFiles(files);
|
|
222
|
+
|
|
223
|
+
if (results.length === 0) {
|
|
224
|
+
return { content: [{ type: 'text' as const, text: 'No matching files found in the index.' }] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Format: markdown with file headers + fenced code blocks
|
|
228
|
+
const parts: string[] = [];
|
|
229
|
+
for (const r of results) {
|
|
230
|
+
const meta = r.metadata as Record<string, unknown>;
|
|
231
|
+
const lang = (meta.language as string) ?? '';
|
|
232
|
+
const startLine = (meta.startLine as number) ?? 1;
|
|
233
|
+
|
|
234
|
+
parts.push(`## ${r.filePath}\n`);
|
|
235
|
+
parts.push('```' + lang);
|
|
236
|
+
|
|
237
|
+
if (lines) {
|
|
238
|
+
const codeLines = r.content.split('\n');
|
|
239
|
+
const pad = String(startLine + codeLines.length - 1).length;
|
|
240
|
+
parts.push(codeLines.map((l, i) =>
|
|
241
|
+
`${String(startLine + i).padStart(pad)}| ${l}`,
|
|
242
|
+
).join('\n'));
|
|
243
|
+
} else {
|
|
244
|
+
parts.push(r.content);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
parts.push('```\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { content: [{ type: 'text' as const, text: parts.join('\n') }] };
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// ── Start Server ────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
async function main() {
|
|
257
|
+
const transport = new StdioServerTransport();
|
|
258
|
+
await server.connect(transport);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
main().catch(err => {
|
|
262
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
263
|
+
console.error(`BrainBank MCP Server Error: ${message}`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceFactory — creates BrainBank instances via the core factory.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to `createBrain()` from the `brainbank` package, passing
|
|
5
|
+
* a portable `BrainContext`. No plugin hardcoding — the factory handles
|
|
6
|
+
* plugin discovery from config and installed packages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { BrainBank, BrainContext } from 'brainbank';
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect repo root by walking up from startDir until we find `.git/`.
|
|
16
|
+
* Returns startDir itself if no `.git/` is found.
|
|
17
|
+
*/
|
|
18
|
+
export function findRepoRoot(startDir: string): string {
|
|
19
|
+
let dir = path.resolve(startDir);
|
|
20
|
+
while (true) {
|
|
21
|
+
if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) break;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
return path.resolve(startDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve the effective repo path from an optional target, env, or cwd. */
|
|
30
|
+
export function resolveRepoPath(targetRepo?: string): string {
|
|
31
|
+
const rp = targetRepo
|
|
32
|
+
?? process.env.BRAINBANK_REPO
|
|
33
|
+
?? findRepoRoot(process.cwd());
|
|
34
|
+
return rp.replace(/\/+$/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a BrainBank instance for a workspace.
|
|
39
|
+
* Uses the core factory which handles:
|
|
40
|
+
* - Config loading from .brainbank/config.json
|
|
41
|
+
* - Dynamic plugin discovery and registration
|
|
42
|
+
* - Embedding/reranker provider setup
|
|
43
|
+
* - Folder plugin auto-discovery
|
|
44
|
+
*/
|
|
45
|
+
export async function createWorkspaceBrain(repoPath: string): Promise<BrainBank> {
|
|
46
|
+
const { createBrain, resetFactoryCache } = await import('brainbank') as typeof import('brainbank');
|
|
47
|
+
resetFactoryCache();
|
|
48
|
+
|
|
49
|
+
const context: BrainContext = {
|
|
50
|
+
repoPath,
|
|
51
|
+
env: process.env as Record<string, string | undefined>,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Silence stdout during initialization — the core factory emits ANSI-colored
|
|
55
|
+
// console.log messages (plugin loading, multi-repo detection) that corrupt
|
|
56
|
+
// the MCP JSON-RPC stdio transport. Redirect console.log → stderr temporarily.
|
|
57
|
+
const origLog = console.log;
|
|
58
|
+
console.log = (...args: unknown[]) => console.error(...args);
|
|
59
|
+
try {
|
|
60
|
+
const brain = await createBrain(context);
|
|
61
|
+
await brain.initialize();
|
|
62
|
+
return brain;
|
|
63
|
+
} finally {
|
|
64
|
+
console.log = origLog;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspacePool — BrainBank instance lifecycle manager.
|
|
3
|
+
*
|
|
4
|
+
* Manages cached BrainBank instances per workspace with:
|
|
5
|
+
* - Memory-pressure eviction (oldest idle first)
|
|
6
|
+
* - TTL eviction for inactive workspaces
|
|
7
|
+
* - Active-operation tracking (prevents mid-query eviction)
|
|
8
|
+
* - Hot-reload of stale HNSW indices
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { BrainBank } from 'brainbank';
|
|
12
|
+
|
|
13
|
+
/** Pool configuration. */
|
|
14
|
+
export interface PoolOptions {
|
|
15
|
+
/** Max total estimated memory in MB. Default: 2048. */
|
|
16
|
+
maxMemoryMB?: number;
|
|
17
|
+
/** Minutes of inactivity before eviction. Default: 30. */
|
|
18
|
+
ttlMinutes?: number;
|
|
19
|
+
/** Factory function to create a BrainBank for a repo path. */
|
|
20
|
+
factory: (repoPath: string) => Promise<BrainBank>;
|
|
21
|
+
/** Called when a workspace is evicted. */
|
|
22
|
+
onEvict?: (repoPath: string) => void;
|
|
23
|
+
/** Called when an error occurs during pool operations. */
|
|
24
|
+
onError?: (repoPath: string, err: unknown) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Internal pool entry. */
|
|
28
|
+
interface PoolEntry {
|
|
29
|
+
brain: BrainBank;
|
|
30
|
+
repoPath: string;
|
|
31
|
+
lastAccess: number;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
activeOps: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Public pool statistics. */
|
|
37
|
+
export interface PoolStats {
|
|
38
|
+
size: number;
|
|
39
|
+
totalMemoryMB: number;
|
|
40
|
+
entries: PoolEntryStats[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Per-entry statistics. */
|
|
44
|
+
export interface PoolEntryStats {
|
|
45
|
+
repoPath: string;
|
|
46
|
+
lastAccessAgo: string;
|
|
47
|
+
memoryMB: number;
|
|
48
|
+
activeOps: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_MAX_MEMORY_MB = 2048;
|
|
52
|
+
const DEFAULT_TTL_MINUTES = 30;
|
|
53
|
+
const EVICTION_INTERVAL_MS = 60_000;
|
|
54
|
+
|
|
55
|
+
/** Format milliseconds as a human-readable "ago" string. */
|
|
56
|
+
function formatAgo(ms: number): string {
|
|
57
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s ago`;
|
|
58
|
+
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m ago`;
|
|
59
|
+
return `${Math.round(ms / 3_600_000)}h ago`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class WorkspacePool {
|
|
63
|
+
private _pool = new Map<string, PoolEntry>();
|
|
64
|
+
private _timer: ReturnType<typeof setInterval> | null = null;
|
|
65
|
+
private _maxMemoryBytes: number;
|
|
66
|
+
private _ttlMs: number;
|
|
67
|
+
private _factory: (repoPath: string) => Promise<BrainBank>;
|
|
68
|
+
private _onEvict?: (repoPath: string) => void;
|
|
69
|
+
private _onError?: (repoPath: string, err: unknown) => void;
|
|
70
|
+
|
|
71
|
+
constructor(options: PoolOptions) {
|
|
72
|
+
this._maxMemoryBytes = (options.maxMemoryMB ?? DEFAULT_MAX_MEMORY_MB) * 1024 * 1024;
|
|
73
|
+
this._ttlMs = (options.ttlMinutes ?? DEFAULT_TTL_MINUTES) * 60 * 1000;
|
|
74
|
+
this._factory = options.factory;
|
|
75
|
+
this._onEvict = options.onEvict;
|
|
76
|
+
this._onError = options.onError;
|
|
77
|
+
|
|
78
|
+
this._timer = setInterval(() => this._evictStale(), EVICTION_INTERVAL_MS);
|
|
79
|
+
// Don't hold the process open for the timer
|
|
80
|
+
if (this._timer.unref) this._timer.unref();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Number of cached workspaces. */
|
|
84
|
+
get size(): number {
|
|
85
|
+
return this._pool.size;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get a BrainBank for the given repo path.
|
|
90
|
+
* Returns a cached instance (with hot-reload) or creates a new one.
|
|
91
|
+
*/
|
|
92
|
+
async get(repoPath: string): Promise<BrainBank> {
|
|
93
|
+
const key = repoPath.replace(/\/+$/, '');
|
|
94
|
+
|
|
95
|
+
const existing = this._pool.get(key);
|
|
96
|
+
if (existing) {
|
|
97
|
+
existing.lastAccess = Date.now();
|
|
98
|
+
try { await existing.brain.ensureFresh(); } catch { /* stale is better than nothing */ }
|
|
99
|
+
return existing.brain;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._evictByMemoryPressure();
|
|
103
|
+
|
|
104
|
+
const brain = await this._factory(key);
|
|
105
|
+
this._pool.set(key, {
|
|
106
|
+
brain,
|
|
107
|
+
repoPath: key,
|
|
108
|
+
lastAccess: Date.now(),
|
|
109
|
+
createdAt: Date.now(),
|
|
110
|
+
activeOps: 0,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return brain;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Execute an operation with active-op tracking.
|
|
118
|
+
* Prevents the workspace from being evicted while the operation runs.
|
|
119
|
+
*/
|
|
120
|
+
async withBrain<T>(repoPath: string, fn: (brain: BrainBank) => Promise<T>): Promise<T> {
|
|
121
|
+
const brain = await this.get(repoPath);
|
|
122
|
+
const key = repoPath.replace(/\/+$/, '');
|
|
123
|
+
const entry = this._pool.get(key);
|
|
124
|
+
if (entry) entry.activeOps++;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
return await fn(brain);
|
|
128
|
+
} finally {
|
|
129
|
+
if (entry) {
|
|
130
|
+
entry.activeOps--;
|
|
131
|
+
entry.lastAccess = Date.now();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Manually evict a specific workspace. */
|
|
137
|
+
evict(repoPath: string): void {
|
|
138
|
+
const key = repoPath.replace(/\/+$/, '');
|
|
139
|
+
this._evictEntry(key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Get pool statistics. */
|
|
143
|
+
stats(): PoolStats {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
let totalMemory = 0;
|
|
146
|
+
const entries: PoolEntryStats[] = [];
|
|
147
|
+
|
|
148
|
+
for (const entry of this._pool.values()) {
|
|
149
|
+
const memBytes = entry.brain.memoryHint();
|
|
150
|
+
const memMB = Math.round(memBytes / 1024 / 1024 * 100) / 100;
|
|
151
|
+
totalMemory += memBytes;
|
|
152
|
+
|
|
153
|
+
entries.push({
|
|
154
|
+
repoPath: entry.repoPath,
|
|
155
|
+
lastAccessAgo: formatAgo(now - entry.lastAccess),
|
|
156
|
+
memoryMB: memMB,
|
|
157
|
+
activeOps: entry.activeOps,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
size: this._pool.size,
|
|
163
|
+
totalMemoryMB: Math.round(totalMemory / 1024 / 1024 * 100) / 100,
|
|
164
|
+
entries,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Close all entries and stop the eviction timer. */
|
|
169
|
+
close(): void {
|
|
170
|
+
if (this._timer) {
|
|
171
|
+
clearInterval(this._timer);
|
|
172
|
+
this._timer = null;
|
|
173
|
+
}
|
|
174
|
+
for (const key of [...this._pool.keys()]) {
|
|
175
|
+
this._evictEntry(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Evict workspaces that haven't been accessed within the TTL. */
|
|
180
|
+
private _evictStale(): void {
|
|
181
|
+
const cutoff = Date.now() - this._ttlMs;
|
|
182
|
+
for (const [key, entry] of this._pool) {
|
|
183
|
+
if (entry.lastAccess < cutoff && entry.activeOps === 0) {
|
|
184
|
+
this._evictEntry(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Evict oldest idle entries until total memory is under the limit. */
|
|
190
|
+
private _evictByMemoryPressure(): void {
|
|
191
|
+
let totalMemory = 0;
|
|
192
|
+
for (const entry of this._pool.values()) {
|
|
193
|
+
totalMemory += entry.brain.memoryHint();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (totalMemory < this._maxMemoryBytes) return;
|
|
197
|
+
|
|
198
|
+
// Sort by lastAccess ascending (oldest first), filter idle
|
|
199
|
+
const candidates = [...this._pool.entries()]
|
|
200
|
+
.filter(([, e]) => e.activeOps === 0)
|
|
201
|
+
.sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
|
|
202
|
+
|
|
203
|
+
for (const [key, entry] of candidates) {
|
|
204
|
+
if (totalMemory < this._maxMemoryBytes) break;
|
|
205
|
+
totalMemory -= entry.brain.memoryHint();
|
|
206
|
+
this._evictEntry(key);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Evict a single entry by key. */
|
|
211
|
+
private _evictEntry(key: string): void {
|
|
212
|
+
const entry = this._pool.get(key);
|
|
213
|
+
if (!entry) return;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
entry.brain.close();
|
|
217
|
+
} catch (err: unknown) {
|
|
218
|
+
this._onError?.(key, err);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this._pool.delete(key);
|
|
222
|
+
this._onEvict?.(key);
|
|
223
|
+
}
|
|
224
|
+
}
|
package/dist/mcp-server.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
|
-
|
|
5
|
-
// src/mcp-server.ts
|
|
6
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
-
import { z } from "zod/v3";
|
|
9
|
-
import * as fs from "fs";
|
|
10
|
-
import * as path from "path";
|
|
11
|
-
import { BrainBank } from "brainbank";
|
|
12
|
-
import { code } from "brainbank/code";
|
|
13
|
-
import { git } from "brainbank/git";
|
|
14
|
-
import { docs } from "brainbank/docs";
|
|
15
|
-
var defaultRepoPath = process.env.BRAINBANK_REPO || void 0;
|
|
16
|
-
async function createReranker() {
|
|
17
|
-
const rerankerEnv = process.env.BRAINBANK_RERANKER ?? "none";
|
|
18
|
-
if (rerankerEnv === "none") return void 0;
|
|
19
|
-
if (rerankerEnv === "qwen3") {
|
|
20
|
-
const { Qwen3Reranker } = await import("@brainbank/reranker");
|
|
21
|
-
return new Qwen3Reranker();
|
|
22
|
-
}
|
|
23
|
-
return void 0;
|
|
24
|
-
}
|
|
25
|
-
__name(createReranker, "createReranker");
|
|
26
|
-
async function createEmbeddingProvider() {
|
|
27
|
-
const embeddingEnv = process.env.BRAINBANK_EMBEDDING ?? "local";
|
|
28
|
-
if (embeddingEnv === "openai") {
|
|
29
|
-
const { OpenAIEmbedding } = await import("brainbank");
|
|
30
|
-
return new OpenAIEmbedding();
|
|
31
|
-
}
|
|
32
|
-
if (embeddingEnv === "perplexity") {
|
|
33
|
-
const { PerplexityEmbedding } = await import("brainbank");
|
|
34
|
-
return new PerplexityEmbedding();
|
|
35
|
-
}
|
|
36
|
-
if (embeddingEnv === "perplexity-context") {
|
|
37
|
-
const { PerplexityContextEmbedding } = await import("brainbank");
|
|
38
|
-
return new PerplexityContextEmbedding();
|
|
39
|
-
}
|
|
40
|
-
return void 0;
|
|
41
|
-
}
|
|
42
|
-
__name(createEmbeddingProvider, "createEmbeddingProvider");
|
|
43
|
-
var MAX_POOL_SIZE = 10;
|
|
44
|
-
var _pool = /* @__PURE__ */ new Map();
|
|
45
|
-
var _sharedReranker = void 0;
|
|
46
|
-
var _sharedEmbedding = void 0;
|
|
47
|
-
var _sharedReady = false;
|
|
48
|
-
async function ensureShared() {
|
|
49
|
-
if (_sharedReady) return;
|
|
50
|
-
_sharedReranker = await createReranker();
|
|
51
|
-
_sharedEmbedding = await createEmbeddingProvider();
|
|
52
|
-
_sharedReady = true;
|
|
53
|
-
}
|
|
54
|
-
__name(ensureShared, "ensureShared");
|
|
55
|
-
async function getBrainBank(targetRepo) {
|
|
56
|
-
const rp = targetRepo ?? defaultRepoPath;
|
|
57
|
-
if (!rp) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
"No repository specified. Pass the `repo` parameter with the workspace path, or set BRAINBANK_REPO environment variable."
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
const resolved = rp.replace(/\/+$/, "");
|
|
63
|
-
if (_pool.has(resolved)) {
|
|
64
|
-
const entry = _pool.get(resolved);
|
|
65
|
-
try {
|
|
66
|
-
const codeStats = entry.brain.indexer("code")?.stats?.();
|
|
67
|
-
if (codeStats && codeStats.hnswSize === 0) {
|
|
68
|
-
const dbPath = path.join(resolved, ".brainbank", "brainbank.db");
|
|
69
|
-
const dbSize = fs.existsSync(dbPath) ? fs.statSync(dbPath).size : 0;
|
|
70
|
-
if (dbSize > 1e5) {
|
|
71
|
-
evictPool(resolved);
|
|
72
|
-
} else {
|
|
73
|
-
entry.lastAccess = Date.now();
|
|
74
|
-
return entry.brain;
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
entry.lastAccess = Date.now();
|
|
78
|
-
return entry.brain;
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
entry.lastAccess = Date.now();
|
|
82
|
-
return entry.brain;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
await ensureShared();
|
|
86
|
-
if (_pool.size >= MAX_POOL_SIZE) {
|
|
87
|
-
let oldest;
|
|
88
|
-
let oldestTime = Infinity;
|
|
89
|
-
for (const [key, entry] of _pool) {
|
|
90
|
-
if (entry.lastAccess < oldestTime) {
|
|
91
|
-
oldestTime = entry.lastAccess;
|
|
92
|
-
oldest = key;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (oldest) evictPool(oldest);
|
|
96
|
-
}
|
|
97
|
-
const brain = await _createBrain(resolved);
|
|
98
|
-
_pool.set(resolved, { brain, lastAccess: Date.now() });
|
|
99
|
-
return brain;
|
|
100
|
-
}
|
|
101
|
-
__name(getBrainBank, "getBrainBank");
|
|
102
|
-
async function _createBrain(resolved) {
|
|
103
|
-
const opts = { repoPath: resolved, reranker: _sharedReranker };
|
|
104
|
-
if (_sharedEmbedding) {
|
|
105
|
-
opts.embeddingProvider = _sharedEmbedding;
|
|
106
|
-
opts.embeddingDims = _sharedEmbedding.dims;
|
|
107
|
-
}
|
|
108
|
-
const brain = new BrainBank(opts).use(code({ repoPath: resolved })).use(git({ repoPath: resolved })).use(docs());
|
|
109
|
-
try {
|
|
110
|
-
await brain.initialize();
|
|
111
|
-
} catch (err) {
|
|
112
|
-
if (err?.message?.includes("Invalid the given array length")) {
|
|
113
|
-
const dbPath = path.join(resolved, ".brainbank", "brainbank.db");
|
|
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 {
|
|
125
|
-
}
|
|
126
|
-
const fresh = new BrainBank(opts).use(code({ repoPath: resolved })).use(git({ repoPath: resolved })).use(docs());
|
|
127
|
-
await fresh.initialize();
|
|
128
|
-
return fresh;
|
|
129
|
-
}
|
|
130
|
-
throw err;
|
|
131
|
-
}
|
|
132
|
-
return brain;
|
|
133
|
-
}
|
|
134
|
-
__name(_createBrain, "_createBrain");
|
|
135
|
-
function evictPool(resolved) {
|
|
136
|
-
const entry = _pool.get(resolved);
|
|
137
|
-
if (entry) {
|
|
138
|
-
try {
|
|
139
|
-
entry.brain.close();
|
|
140
|
-
} catch {
|
|
141
|
-
}
|
|
142
|
-
_pool.delete(resolved);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
__name(evictPool, "evictPool");
|
|
146
|
-
var server = new McpServer({
|
|
147
|
-
name: "brainbank",
|
|
148
|
-
version: "0.2.0"
|
|
149
|
-
});
|
|
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
|
-
server.registerTool(
|
|
185
|
-
"brainbank_context",
|
|
186
|
-
{
|
|
187
|
-
title: "BrainBank Context",
|
|
188
|
-
description: "Get a formatted knowledge context block for a task. Returns relevant code, git history, and co-edit patterns as markdown.",
|
|
189
|
-
inputSchema: z.object({
|
|
190
|
-
task: z.string().describe("Description of the task you need context for"),
|
|
191
|
-
affectedFiles: z.array(z.string()).optional().default([]).describe("Files you plan to modify (improves co-edit suggestions)"),
|
|
192
|
-
codeResults: z.number().optional().default(6).describe("Max code results"),
|
|
193
|
-
gitResults: z.number().optional().default(5).describe("Max git commit results"),
|
|
194
|
-
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)")
|
|
195
|
-
})
|
|
196
|
-
},
|
|
197
|
-
async ({ task, affectedFiles, codeResults, gitResults, repo }) => {
|
|
198
|
-
const brainbank = await getBrainBank(repo);
|
|
199
|
-
const context = await brainbank.getContext(task, {
|
|
200
|
-
affectedFiles,
|
|
201
|
-
codeResults,
|
|
202
|
-
gitResults
|
|
203
|
-
});
|
|
204
|
-
return { content: [{ type: "text", text: context }] };
|
|
205
|
-
}
|
|
206
|
-
);
|
|
207
|
-
server.registerTool(
|
|
208
|
-
"brainbank_index",
|
|
209
|
-
{
|
|
210
|
-
title: "BrainBank Index",
|
|
211
|
-
description: "Index (or re-index) code, git history, and docs. Incremental \u2014 only changed files are processed.",
|
|
212
|
-
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
|
-
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
|
-
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)")
|
|
218
|
-
})
|
|
219
|
-
},
|
|
220
|
-
async ({ modules, docsPath, forceReindex, gitDepth, repo }) => {
|
|
221
|
-
const brainbank = await getBrainBank(repo);
|
|
222
|
-
if (docsPath) {
|
|
223
|
-
const absPath = path.resolve(docsPath);
|
|
224
|
-
const collName = path.basename(absPath);
|
|
225
|
-
try {
|
|
226
|
-
brainbank.addCollection({
|
|
227
|
-
name: collName,
|
|
228
|
-
path: absPath,
|
|
229
|
-
pattern: "**/*.md",
|
|
230
|
-
ignore: ["deprecated/**", "node_modules/**"]
|
|
231
|
-
});
|
|
232
|
-
} catch {
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const result = await brainbank.index({ modules, forceReindex, gitDepth });
|
|
236
|
-
const lines = [
|
|
237
|
-
"## Indexing Complete",
|
|
238
|
-
"",
|
|
239
|
-
`**Code**: ${result.code?.indexed ?? 0} files indexed, ${result.code?.skipped ?? 0} skipped, ${result.code?.chunks ?? 0} chunks`,
|
|
240
|
-
`**Git**: ${result.git?.indexed ?? 0} commits indexed, ${result.git?.skipped ?? 0} skipped`
|
|
241
|
-
];
|
|
242
|
-
if (result.docs) {
|
|
243
|
-
for (const [name, stat] of Object.entries(result.docs)) {
|
|
244
|
-
lines.push(`**Docs [${name}]**: ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const stats = brainbank.stats();
|
|
248
|
-
lines.push("");
|
|
249
|
-
lines.push(`**Totals**: ${stats.code?.chunks ?? 0} code chunks, ${stats.git?.commits ?? 0} commits, ${stats.documents?.documents ?? 0} docs`);
|
|
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
|
-
}
|
|
284
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
285
|
-
}
|
|
286
|
-
);
|
|
287
|
-
server.registerTool(
|
|
288
|
-
"brainbank_history",
|
|
289
|
-
{
|
|
290
|
-
title: "BrainBank File History",
|
|
291
|
-
description: "Get git commit history for a file. Shows changes, authors, and line counts.",
|
|
292
|
-
inputSchema: z.object({
|
|
293
|
-
filePath: z.string().describe('File path (relative or partial, e.g. "auth.ts")'),
|
|
294
|
-
limit: z.number().optional().default(20).describe("Max commits to return"),
|
|
295
|
-
repo: z.string().optional().describe("Repository path (default: BRAINBANK_REPO)")
|
|
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)")
|
|
325
|
-
})
|
|
326
|
-
},
|
|
327
|
-
async ({ action, collection, content, query, metadata, k, keep, repo }) => {
|
|
328
|
-
const brainbank = await getBrainBank(repo);
|
|
329
|
-
const coll = brainbank.collection(collection);
|
|
330
|
-
if (action === "add") {
|
|
331
|
-
if (!content) throw new Error("BrainBank: content is required for add action.");
|
|
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
|
-
};
|
|
336
|
-
}
|
|
337
|
-
if (action === "search") {
|
|
338
|
-
if (!query) throw new Error("BrainBank: query is required for search action.");
|
|
339
|
-
const results = await coll.search(query, { k });
|
|
340
|
-
if (results.length === 0) {
|
|
341
|
-
return { content: [{ type: "text", text: `No results in '${collection}' for "${query}"` }] };
|
|
342
|
-
}
|
|
343
|
-
const lines = [`## Collection: ${collection}`, ""];
|
|
344
|
-
for (const r of results) {
|
|
345
|
-
const score = Math.round((r.score ?? 0) * 100);
|
|
346
|
-
lines.push(`[${score}%] ${r.content}`);
|
|
347
|
-
if (Object.keys(r.metadata).length > 0) {
|
|
348
|
-
lines.push(` ${JSON.stringify(r.metadata)}`);
|
|
349
|
-
}
|
|
350
|
-
lines.push("");
|
|
351
|
-
}
|
|
352
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
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
|
-
};
|
|
360
|
-
}
|
|
361
|
-
throw new Error(`BrainBank: Unknown action "${action}".`);
|
|
362
|
-
}
|
|
363
|
-
);
|
|
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
|
-
async function main() {
|
|
394
|
-
const transport = new StdioServerTransport();
|
|
395
|
-
await server.connect(transport);
|
|
396
|
-
}
|
|
397
|
-
__name(main, "main");
|
|
398
|
-
main().catch((err) => {
|
|
399
|
-
console.error(`BrainBank MCP Server Error: ${err.message}`);
|
|
400
|
-
process.exit(1);
|
|
401
|
-
});
|
|
402
|
-
//# sourceMappingURL=mcp-server.js.map
|
package/dist/mcp-server.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * BrainBank — MCP Server\n * \n * Exposes BrainBank as an MCP server via stdio transport.\n * Works with Google Antigravity, Claude Desktop, and any MCP-compatible client.\n * \n * Usage in Antigravity mcp_config.json:\n * {\n * \"mcpServers\": {\n * \"brainbank\": {\n * \"command\": \"npx\",\n * \"args\": [\"tsx\", \"/path/to/brainbank/packages/mcp/src/mcp-server.ts\"],\n * \"env\": { \"BRAINBANK_REPO\": \"/path/to/your/repo\" }\n * }\n * }\n * }\n * \n * Tools (6):\n * brainbank_search — Unified search (hybrid, vector, or keyword mode)\n * brainbank_context — Formatted knowledge context for a task\n * brainbank_index — Trigger code/git/docs indexing\n * brainbank_stats — Index statistics\n * brainbank_history — Git history for a specific file\n * brainbank_collection — KV collection operations (add, search, trim)\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v3';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { BrainBank } from 'brainbank';\nimport { code } from 'brainbank/code';\nimport { git } from 'brainbank/git';\nimport { docs } from 'brainbank/docs';\n\n// ── Configuration from env ──────────────────────────\n\n/**\n * Detect repo root by walking up from startDir until we find `.git/`.\n * Returns startDir itself if no `.git/` is found (mono-repo or non-git project).\n */\nfunction findRepoRoot(startDir: string): string {\n let dir = path.resolve(startDir);\n while (true) {\n if (fs.existsSync(path.join(dir, '.git'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break; // filesystem root\n dir = parent;\n }\n return path.resolve(startDir); // fallback: use startDir as-is\n}\n\nconst defaultRepoPath = process.env.BRAINBANK_REPO || undefined;\n\n// ── Reranker (default: none, set BRAINBANK_RERANKER=qwen3 to enable) ──\n\nasync function createReranker() {\n const rerankerEnv = process.env.BRAINBANK_RERANKER ?? 'none';\n if (rerankerEnv === 'none') return undefined;\n if (rerankerEnv === 'qwen3') {\n const { Qwen3Reranker } = await import('@brainbank/reranker');\n return new Qwen3Reranker();\n }\n return undefined;\n}\n\n// ── Embedding Provider (default: local) ──\n\nasync function createEmbeddingProvider() {\n const embeddingEnv = process.env.BRAINBANK_EMBEDDING ?? 'local';\n if (embeddingEnv === 'openai') {\n const { OpenAIEmbedding } = await import('brainbank');\n return new OpenAIEmbedding();\n }\n if (embeddingEnv === 'perplexity') {\n const { PerplexityEmbedding } = await import('brainbank');\n return new PerplexityEmbedding();\n }\n if (embeddingEnv === 'perplexity-context') {\n const { PerplexityContextEmbedding } = await import('brainbank');\n return new PerplexityContextEmbedding();\n }\n return undefined; // BrainBank defaults to local WASM\n}\n\n// ── Multi-Workspace BrainBank Pool ─────────────────────\n\nconst MAX_POOL_SIZE = 10;\n\ninterface PoolEntry {\n brain: BrainBank;\n lastAccess: number;\n}\n\nconst _pool = new Map<string, PoolEntry>();\nlet _sharedReranker: any = undefined;\nlet _sharedEmbedding: any = undefined;\nlet _sharedReady = false;\n\nasync function ensureShared() {\n if (_sharedReady) return;\n _sharedReranker = await createReranker();\n _sharedEmbedding = await createEmbeddingProvider();\n _sharedReady = true;\n}\n\nasync function getBrainBank(targetRepo?: string): Promise<BrainBank> {\n const rp = targetRepo ?? defaultRepoPath;\n if (!rp) {\n throw new Error(\n 'No repository specified. Pass the `repo` parameter with the workspace path, ' +\n 'or set BRAINBANK_REPO environment variable.'\n );\n }\n const resolved = rp.replace(/\\/+$/, '');\n\n if (_pool.has(resolved)) {\n const entry = _pool.get(resolved)!;\n try {\n const codeStats = entry.brain.indexer('code')?.stats?.();\n if (codeStats && codeStats.hnswSize === 0) {\n const dbPath = path.join(resolved, '.brainbank', 'brainbank.db');\n const dbSize = fs.existsSync(dbPath) ? fs.statSync(dbPath).size : 0;\n if (dbSize > 100_000) {\n evictPool(resolved);\n } else {\n entry.lastAccess = Date.now();\n return entry.brain;\n }\n } else {\n entry.lastAccess = Date.now();\n return entry.brain;\n }\n } catch {\n entry.lastAccess = Date.now();\n return entry.brain;\n }\n }\n\n await ensureShared();\n\n if (_pool.size >= MAX_POOL_SIZE) {\n let oldest: string | undefined;\n let oldestTime = Infinity;\n for (const [key, entry] of _pool) {\n if (entry.lastAccess < oldestTime) {\n oldestTime = entry.lastAccess;\n oldest = key;\n }\n }\n if (oldest) evictPool(oldest);\n }\n\n const brain = await _createBrain(resolved);\n _pool.set(resolved, { brain, lastAccess: Date.now() });\n return brain;\n}\n\nasync function _createBrain(resolved: string): Promise<BrainBank> {\n const opts: Record<string, any> = { repoPath: resolved, reranker: _sharedReranker };\n if (_sharedEmbedding) {\n opts.embeddingProvider = _sharedEmbedding;\n opts.embeddingDims = _sharedEmbedding.dims;\n }\n const brain = new BrainBank(opts)\n .use(code({ repoPath: resolved }))\n .use(git({ repoPath: resolved }))\n .use(docs());\n\n try {\n await brain.initialize();\n } catch (err: any) {\n if (err?.message?.includes('Invalid the given array length')) {\n const dbPath = path.join(resolved, '.brainbank', 'brainbank.db');\n try { fs.unlinkSync(dbPath); } catch {}\n try { fs.unlinkSync(dbPath + '-wal'); } catch {}\n try { fs.unlinkSync(dbPath + '-shm'); } catch {}\n\n const fresh = new BrainBank(opts)\n .use(code({ repoPath: resolved }))\n .use(git({ repoPath: resolved }))\n .use(docs());\n await fresh.initialize();\n return fresh;\n }\n throw err;\n }\n\n return brain;\n}\n\nfunction evictPool(resolved: string) {\n const entry = _pool.get(resolved);\n if (entry) {\n try { entry.brain.close(); } catch {}\n _pool.delete(resolved);\n }\n}\n\n// ── MCP Server Setup ────────────────────────────────\n\nconst server = new McpServer({\n name: 'brainbank',\n version: '0.2.0',\n});\n\n// ── Tool: brainbank_search ──────────────────────────\n// Replaces: brainbank_search, brainbank_hybrid_search, brainbank_keyword_search\n\nserver.registerTool(\n 'brainbank_search',\n {\n title: 'BrainBank Search',\n description:\n 'Search indexed code and git commits. Supports three modes:\\n' +\n '- hybrid (default): vector + BM25 fused with RRF — best quality\\n' +\n '- vector: semantic similarity only\\n' +\n '- keyword: instant BM25 for exact terms, function names, error messages',\n inputSchema: z.object({\n query: z.string().describe('Search query — works with both keywords and natural language'),\n mode: z.enum(['hybrid', 'vector', 'keyword']).optional().default('hybrid').describe('Search strategy'),\n codeK: z.number().optional().default(8).describe('Max code results'),\n gitK: z.number().optional().default(5).describe('Max git results'),\n minScore: z.number().optional().default(0.25).describe('Minimum similarity score (0-1), only for vector mode'),\n collections: z.record(z.string(), z.number()).optional().describe(\n 'Max results per source. Reserved: \"code\", \"git\", \"docs\". Any other key = KV collection.'\n ),\n repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),\n }),\n },\n async ({ query, mode, codeK, gitK, minScore, collections, repo }) => {\n const brainbank = await getBrainBank(repo);\n\n let results;\n if (mode === 'keyword') {\n results = await brainbank.searchBM25(query, { codeK, gitK });\n } else if (mode === 'vector') {\n results = await brainbank.search(query, { codeK, gitK, minScore });\n } else {\n results = await brainbank.hybridSearch(query, { codeK, gitK, collections });\n }\n\n if (results.length === 0) {\n return { content: [{ type: 'text', text: 'No results found.' }] };\n }\n\n const modeLabel = mode === 'keyword' ? 'Keyword (BM25)' : mode === 'vector' ? 'Vector' : 'Hybrid (Vector + BM25 → RRF)';\n return { content: [{ type: 'text', text: formatResults(results, modeLabel) }] };\n },\n);\n\n// ── Tool: brainbank_context ─────────────────────────\n\nserver.registerTool(\n 'brainbank_context',\n {\n title: 'BrainBank Context',\n description: 'Get a formatted knowledge context block for a task. Returns relevant code, git history, and co-edit patterns as markdown.',\n inputSchema: z.object({\n task: z.string().describe('Description of the task you need context for'),\n affectedFiles: z.array(z.string()).optional().default([]).describe('Files you plan to modify (improves co-edit suggestions)'),\n codeResults: z.number().optional().default(6).describe('Max code results'),\n gitResults: z.number().optional().default(5).describe('Max git commit results'),\n repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),\n }),\n },\n async ({ task, affectedFiles, codeResults, gitResults, repo }) => {\n const brainbank = await getBrainBank(repo);\n const context = await brainbank.getContext(task, {\n affectedFiles,\n codeResults,\n gitResults,\n });\n\n return { content: [{ type: 'text', text: context }] };\n },\n);\n\n// ── Tool: brainbank_index ───────────────────────────\n\nserver.registerTool(\n 'brainbank_index',\n {\n title: 'BrainBank Index',\n description: 'Index (or re-index) code, git history, and docs. Incremental — only changed files are processed.',\n inputSchema: z.object({\n modules: z.array(z.enum(['code', 'git', 'docs'])).optional().describe('Which modules to index (default: all)'),\n docsPath: z.string().optional().describe('Path to a docs folder to register and index'),\n forceReindex: z.boolean().optional().default(false).describe('Force re-index of all files'),\n gitDepth: z.number().optional().default(500).describe('Number of git commits to index'),\n repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),\n }),\n },\n async ({ modules, docsPath, forceReindex, gitDepth, repo }) => {\n const brainbank = await getBrainBank(repo);\n\n if (docsPath) {\n const absPath = path.resolve(docsPath);\n const collName = path.basename(absPath);\n try {\n brainbank.addCollection({\n name: collName,\n path: absPath,\n pattern: '**/*.md',\n ignore: ['deprecated/**', 'node_modules/**'],\n });\n } catch {\n // docs module not loaded\n }\n }\n\n const result = await brainbank.index({ modules, forceReindex, gitDepth });\n\n const lines = [\n '## Indexing Complete',\n '',\n `**Code**: ${result.code?.indexed ?? 0} files indexed, ${result.code?.skipped ?? 0} skipped, ${result.code?.chunks ?? 0} chunks`,\n `**Git**: ${result.git?.indexed ?? 0} commits indexed, ${result.git?.skipped ?? 0} skipped`,\n ];\n\n if (result.docs) {\n for (const [name, stat] of Object.entries(result.docs)) {\n lines.push(`**Docs [${name}]**: ${stat.indexed} indexed, ${stat.skipped} skipped, ${stat.chunks} chunks`);\n }\n }\n\n const stats = brainbank.stats();\n lines.push('');\n lines.push(`**Totals**: ${stats.code?.chunks ?? 0} code chunks, ${stats.git?.commits ?? 0} commits, ${stats.documents?.documents ?? 0} docs`);\n\n return { content: [{ type: 'text', text: lines.join('\\n') }] };\n },\n);\n\n// ── Tool: brainbank_stats ───────────────────────────\n\nserver.registerTool(\n 'brainbank_stats',\n {\n title: 'BrainBank Stats',\n description: 'Get index statistics: file count, code chunks, git commits, HNSW sizes, KV collections.',\n inputSchema: z.object({\n repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),\n }),\n },\n async ({ repo }) => {\n const brainbank = await getBrainBank(repo);\n const s = brainbank.stats();\n\n const lines = ['## BrainBank Stats', ''];\n\n if (s.code) {\n lines.push(`**Code**: ${s.code.files} files, ${s.code.chunks} chunks, ${s.code.hnswSize} vectors`);\n }\n if (s.git) {\n lines.push(`**Git**: ${s.git.commits} commits, ${s.git.filesTracked} files, ${s.git.coEdits} co-edit pairs`);\n }\n if (s.documents) {\n lines.push(`**Docs**: ${s.documents.collections} collections, ${s.documents.documents} documents`);\n }\n\n const kvNames = brainbank.listCollectionNames();\n if (kvNames.length > 0) {\n lines.push('');\n lines.push('**KV Collections**:');\n for (const name of kvNames) {\n const coll = brainbank.collection(name);\n lines.push(`- ${name}: ${coll.count()} items`);\n }\n }\n\n return { content: [{ type: 'text', text: lines.join('\\n') }] };\n },\n);\n\n// ── Tool: brainbank_history ─────────────────────────\n\nserver.registerTool(\n 'brainbank_history',\n {\n title: 'BrainBank File History',\n description: 'Get git commit history for a file. Shows changes, authors, and line counts.',\n inputSchema: z.object({\n filePath: z.string().describe('File path (relative or partial, e.g. \"auth.ts\")'),\n limit: z.number().optional().default(20).describe('Max commits to return'),\n repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),\n }),\n },\n async ({ filePath, limit, repo }) => {\n const brainbank = await getBrainBank(repo);\n const history = await brainbank.fileHistory(filePath, limit);\n\n if (history.length === 0) {\n return { content: [{ type: 'text', text: `No git history found for \"${filePath}\"` }] };\n }\n\n const lines = [`## Git History: ${filePath}`, ''];\n for (const h of history as any[]) {\n lines.push(`**[${h.short_hash}]** ${h.message} *(${h.author}, +${h.additions}/-${h.deletions})*`);\n }\n\n return { content: [{ type: 'text', text: lines.join('\\n') }] };\n },\n);\n\n// ── Tool: brainbank_collection ──────────────────────\n// Replaces: brainbank_collection_add, brainbank_collection_search, brainbank_collection_trim\n\nserver.registerTool(\n 'brainbank_collection',\n {\n title: 'BrainBank Collection',\n description:\n 'Operate on KV collections (auto-created). Actions:\\n' +\n '- add: store content with optional metadata\\n' +\n '- search: hybrid vector + keyword search\\n' +\n '- trim: keep only N most recent items',\n inputSchema: z.object({\n action: z.enum(['add', 'search', 'trim']).describe('Operation to perform'),\n collection: z.string().describe('Collection name (e.g. \"errors\", \"decisions\")'),\n content: z.string().optional().describe('Content to store (required for add)'),\n query: z.string().optional().describe('Search query (required for search)'),\n metadata: z.record(z.any()).optional().default({}).describe('Metadata for add'),\n k: z.number().optional().default(5).describe('Max results for search'),\n keep: z.number().optional().describe('Items to keep for trim'),\n repo: z.string().optional().describe('Repository path (default: BRAINBANK_REPO)'),\n }),\n },\n async ({ action, collection, content, query, metadata, k, keep, repo }) => {\n const brainbank = await getBrainBank(repo);\n const coll = brainbank.collection(collection);\n\n if (action === 'add') {\n if (!content) throw new Error('BrainBank: content is required for add action.');\n const id = await coll.add(content, metadata);\n return {\n content: [{ type: 'text', text: `✓ Item #${id} added to '${collection}' (${coll.count()} total)` }],\n };\n }\n\n if (action === 'search') {\n if (!query) throw new Error('BrainBank: query is required for search action.');\n const results = await coll.search(query, { k });\n\n if (results.length === 0) {\n return { content: [{ type: 'text', text: `No results in '${collection}' for \"${query}\"` }] };\n }\n\n const lines = [`## Collection: ${collection}`, ''];\n for (const r of results) {\n const score = Math.round((r.score ?? 0) * 100);\n lines.push(`[${score}%] ${r.content}`);\n if (Object.keys(r.metadata).length > 0) {\n lines.push(` ${JSON.stringify(r.metadata)}`);\n }\n lines.push('');\n }\n return { content: [{ type: 'text', text: lines.join('\\n') }] };\n }\n\n if (action === 'trim') {\n if (keep == null) throw new Error('BrainBank: keep is required for trim action.');\n const result = await coll.trim({ keep });\n return {\n content: [{ type: 'text', text: `✓ Trimmed ${result.removed} items from '${collection}' (kept ${keep})` }],\n };\n }\n\n throw new Error(`BrainBank: Unknown action \"${action}\".`);\n },\n);\n\n// ── Shared result formatter ─────────────────────────\n\nfunction formatResults(results: any[], mode: string): string {\n const lines: string[] = [`## ${mode}`, ''];\n for (const r of results) {\n const score = Math.round(r.score * 100);\n if (r.type === 'code') {\n const m = r.metadata;\n lines.push(`[CODE ${score}%] ${r.filePath} — ${m.name || m.chunkType} (L${m.startLine}-${m.endLine})`);\n lines.push(r.content);\n lines.push('');\n } else if (r.type === 'commit') {\n const m = r.metadata;\n lines.push(`[COMMIT ${score}%] ${m.shortHash} — ${r.content} (${m.author})`);\n if (m.files?.length) lines.push(` Files: ${m.files.join(', ')}`);\n lines.push('');\n } else if (r.type === 'document') {\n const ctx = r.context ? ` — ${r.context}` : '';\n lines.push(`[DOC ${score}%] ${r.filePath} [${r.metadata.collection}]${ctx}`);\n lines.push(r.content);\n lines.push('');\n } else if (r.type === 'collection') {\n const col = r.metadata?.collection ?? 'unknown';\n lines.push(`[COLLECTION ${score}%] [${col}]`);\n lines.push(r.content);\n lines.push('');\n }\n }\n return lines.join('\\n');\n}\n\n// ── Start Server ────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch(err => {\n console.error(`BrainBank MCP Server Error: ${err.message}`);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|