graph-indexer 1.1.0 → 2.0.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/CHANGELOG.md +412 -0
- package/README.md +228 -350
- package/SECURITY.md +68 -0
- package/TRADEMARK.md +31 -0
- package/cli-ui.mjs +78 -0
- package/config.mjs +269 -0
- package/daemon-ctl.mjs +175 -0
- package/daemon-lock.mjs +69 -0
- package/embedders/python/mlx_embed_server.py +61 -0
- package/embedders/python/requirements-mlx.txt +5 -0
- package/embedders/setup-mlx.mjs +110 -0
- package/embeddings.mjs +489 -0
- package/engine/binary.mjs +399 -0
- package/{core-engine.mjs → engine/memory.mjs} +359 -249
- package/engine/sqlite.mjs +863 -0
- package/enrichment.mjs +426 -0
- package/git-signals.mjs +248 -0
- package/indexer.mjs +216 -77
- package/init.mjs +1604 -170
- package/layout.mjs +123 -0
- package/mcp/format.mjs +61 -0
- package/mcp/tools.mjs +1157 -0
- package/mcp/topology.mjs +551 -0
- package/mcp-server.mjs +118 -573
- package/package.json +56 -16
- package/parse/extractor.mjs +617 -0
- package/parse/imports.mjs +365 -0
- package/parse/languages.mjs +89 -0
- package/parse/metadata.mjs +655 -0
- package/parse/routes.mjs +327 -0
- package/prompts/CORE.md +86 -0
- package/prompts/DOMAIN_TEMPLATE.md +46 -0
- package/prompts/INTEGRATION.md +123 -0
- package/prompts/frameworks/ANDROID.md +17 -0
- package/prompts/frameworks/ASPNET_CORE.md +19 -0
- package/prompts/frameworks/FASTAPI_DJANGO.md +19 -0
- package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
- package/prompts/frameworks/RAILS.md +20 -0
- package/prompts/frameworks/REACT.md +20 -0
- package/prompts/frameworks/SPRING_BOOT.md +19 -0
- package/prompts/languages/BASH.md +22 -0
- package/prompts/languages/C.md +21 -0
- package/prompts/languages/CSHARP.md +24 -0
- package/prompts/languages/CSS_SCSS.md +22 -0
- package/prompts/languages/GO.md +23 -0
- package/prompts/languages/JAVA.md +23 -0
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
- package/prompts/languages/KOTLIN.md +24 -0
- package/prompts/languages/PHP.md +23 -0
- package/prompts/languages/PYTHON.md +23 -0
- package/prompts/languages/RUBY.md +23 -0
- package/prompts/languages/RUST.md +21 -0
- package/prompts/languages/SWIFT.md +21 -0
- package/search-core.mjs +627 -0
- package/storage.mjs +68 -0
- package/watch-daemon.mjs +204 -69
- package/PROMPT.md +0 -143
- package/parser-utils.mjs +0 -861
package/mcp-server.mjs
CHANGED
|
@@ -1,142 +1,130 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* @file mcp-server.mjs
|
|
4
|
-
* @description MCP
|
|
5
|
-
*
|
|
4
|
+
* @description MCP server bootstrap. Resolves configuration, selects the storage
|
|
5
|
+
* backend (in-memory by default, SQLite when configured), registers
|
|
6
|
+
* the tool surface (mcp/tools.mjs) and connects over stdio. All
|
|
7
|
+
* retrieval logic lives in the store + search-core; all tool logic in
|
|
8
|
+
* mcp/tools.mjs — this file is wiring only.
|
|
6
9
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
7
10
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
8
11
|
* @license MIT
|
|
9
12
|
*/
|
|
10
|
-
import { McpServer, ResourceTemplate } from
|
|
11
|
-
import { StdioServerTransport } from
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
13
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { spawn } from 'child_process';
|
|
19
|
+
import { resolveConfig, describeConfig, configNotices } from './config.mjs';
|
|
20
|
+
import { ensureDataDir, migrateLegacyLayout } from './layout.mjs';
|
|
21
|
+
import { daemonStatus } from './daemon-lock.mjs';
|
|
22
|
+
import { createStore } from './storage.mjs';
|
|
23
|
+
import { registerTools } from './mcp/tools.mjs';
|
|
24
|
+
import { createEmbedder, readEmbedMeta } from './embeddings.mjs';
|
|
25
|
+
import { loadGitSignals } from './git-signals.mjs';
|
|
26
|
+
|
|
27
|
+
const config = resolveConfig();
|
|
28
|
+
const PROJECT_ROOT = config.projectRoot;
|
|
29
|
+
const PID_FILE = config.pidFile;
|
|
30
|
+
const PACKAGE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const PID_FILE = resolve(PROJECT_ROOT, ".idx-daemon.pid");
|
|
32
|
+
ensureDataDir(PROJECT_ROOT);
|
|
33
|
+
migrateLegacyLayout(PROJECT_ROOT);
|
|
23
34
|
|
|
24
|
-
// Single source of truth for the version: graph-indexer's own package.json
|
|
25
|
-
// (next to this file), never the consumer project's package.json.
|
|
26
|
-
const PACKAGE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
27
35
|
function readPackageVersion() {
|
|
28
36
|
try {
|
|
29
|
-
return JSON.parse(fs.readFileSync(path.join(PACKAGE_DIR,
|
|
30
|
-
} catch { return
|
|
37
|
+
return JSON.parse(fs.readFileSync(path.join(PACKAGE_DIR, 'package.json'), 'utf-8')).version || '0.0.0';
|
|
38
|
+
} catch { return '0.0.0'; }
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
// ───
|
|
34
|
-
|
|
41
|
+
// ─── Watch-daemon orchestration (both backends) ─────────────────────────────────
|
|
42
|
+
// The incremental watcher keeps the configured backend fresh: it rewrites the
|
|
43
|
+
// JSON snapshot for the in-memory engine (picked up below via an fs watch), and
|
|
44
|
+
// applies per-file WAL transactions for SQLite (picked up by the store via
|
|
45
|
+
// PRAGMA data_version). Either way, a long-running MCP server stays consistent
|
|
46
|
+
// with the working tree without restarts or full re-indexes.
|
|
35
47
|
function ensureDaemonRunning() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
47
|
-
const daemonPath = path.join(__dirname, "watch-daemon.mjs");
|
|
48
|
-
const logPath = path.join(PROJECT_ROOT, ".idx-daemon.log");
|
|
49
|
-
process.stderr.write(`🚀 Starting Watcher Daemon...\n Log: ${logPath}\n`);
|
|
48
|
+
// The daemon owns the PID lock exclusively (it acquires it atomically on
|
|
49
|
+
// startup — see daemon-lock.mjs). We only check whether one is already live;
|
|
50
|
+
// if not, we spawn it detached. Even if two servers race here, the losing
|
|
51
|
+
// daemon fails to acquire the lock and exits, so never more than one runs.
|
|
52
|
+
const { running, pid } = daemonStatus(PID_FILE);
|
|
53
|
+
if (running) { process.stderr.write(`✅ Daemon already active (PID: ${pid}).\n`); return; }
|
|
54
|
+
|
|
55
|
+
const daemonPath = path.join(PACKAGE_DIR, 'watch-daemon.mjs');
|
|
56
|
+
process.stderr.write(`🚀 Starting Watcher Daemon...\n Log: ${config.logFile}\n`);
|
|
50
57
|
let logFd;
|
|
51
|
-
try { logFd = fs.openSync(
|
|
58
|
+
try { logFd = fs.openSync(config.logFile, 'a'); } catch { logFd = null; }
|
|
52
59
|
const child = spawn(process.execPath, [daemonPath], {
|
|
53
60
|
detached: true,
|
|
54
61
|
stdio: logFd !== null ? ['ignore', logFd, logFd] : 'ignore',
|
|
55
|
-
env: { ...process.env, MCP_PROJECT_ROOT: PROJECT_ROOT }
|
|
62
|
+
env: { ...process.env, MCP_PROJECT_ROOT: PROJECT_ROOT },
|
|
56
63
|
});
|
|
57
64
|
child.unref();
|
|
58
65
|
if (logFd !== null) fs.closeSync(logFd);
|
|
59
|
-
fs.writeFileSync(PID_FILE, child.pid.toString());
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ─── Frontier 3: Query-Driven Body Pruning ───────────────────────────────────
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Extracts just the function signature (first N lines up to opening brace).
|
|
66
|
-
*/
|
|
67
|
-
function extractSignatureLine(codeSnippet) {
|
|
68
|
-
if (!codeSnippet) return '';
|
|
69
|
-
const lines = codeSnippet.split('\n');
|
|
70
|
-
const sigLines = [];
|
|
71
|
-
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
72
|
-
sigLines.push(lines[i]);
|
|
73
|
-
const l = lines[i];
|
|
74
|
-
// Stop at opening brace or arrow that ends the signature
|
|
75
|
-
if (i > 0 && (l.trimEnd().endsWith('{') || l.includes('=>') || l.trimEnd().endsWith(':'))) break;
|
|
76
|
-
}
|
|
77
|
-
return sigLines.join('\n');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Prune a function body: keep signature + lines containing any query token + return/throw lines.
|
|
82
|
-
* Falls back to first maxLines lines when no match found.
|
|
83
|
-
*/
|
|
84
|
-
function pruneBodyByQuery(codeSnippet, queryTokens, maxLines = 40) {
|
|
85
|
-
if (!codeSnippet) return '';
|
|
86
|
-
const lines = codeSnippet.split('\n');
|
|
87
|
-
if (lines.length <= maxLines) return codeSnippet;
|
|
88
|
-
|
|
89
|
-
const querySet = new Set(queryTokens.filter(t => t.length >= 3).map(t => t.toLowerCase()));
|
|
90
|
-
if (querySet.size === 0) return lines.slice(0, maxLines).join('\n') + '\n// …';
|
|
91
|
-
|
|
92
|
-
// Always include: opening lines (signature + brace), closing lines, return/throw
|
|
93
|
-
const SIG_LINES = Math.min(5, lines.length);
|
|
94
|
-
const TAIL_LINES = Math.min(3, lines.length);
|
|
95
|
-
const sigBlock = lines.slice(0, SIG_LINES);
|
|
96
|
-
const tailBlock = lines.slice(Math.max(lines.length - TAIL_LINES, SIG_LINES));
|
|
97
|
-
|
|
98
|
-
const bodyLines = lines.slice(SIG_LINES, lines.length - TAIL_LINES);
|
|
99
|
-
const relevant = bodyLines.filter(line => {
|
|
100
|
-
const ll = line.toLowerCase();
|
|
101
|
-
if (/^\s*(return|throw|raise|yield)\b/.test(ll)) return true;
|
|
102
|
-
return [...querySet].some(token => ll.includes(token));
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
if (relevant.length === 0) return lines.slice(0, maxLines).join('\n') + '\n// …';
|
|
106
|
-
return [...sigBlock, ...relevant, ...tailBlock].join('\n');
|
|
107
66
|
}
|
|
108
67
|
|
|
109
|
-
// ───
|
|
68
|
+
// ─── Boot ──────────────────────────────────────────────────────────────────────
|
|
110
69
|
|
|
111
70
|
ensureDaemonRunning();
|
|
112
|
-
const version = readPackageVersion();
|
|
113
|
-
const server = new McpServer({ name: "graph-indexer", version });
|
|
114
71
|
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
72
|
+
const version = readPackageVersion();
|
|
73
|
+
const server = new McpServer({ name: 'graph-indexer', version });
|
|
74
|
+
|
|
75
|
+
const db = await createStore(config, { cacheEmbeddings: false });
|
|
76
|
+
const backend = db.backend;
|
|
77
|
+
|
|
78
|
+
process.stderr.write('⚙️ Effective configuration:\n');
|
|
79
|
+
for (const line of describeConfig(config, { backend })) process.stderr.write(` ${line}\n`);
|
|
80
|
+
for (const notice of configNotices(config)) process.stderr.write(`⚠️ ${notice}\n`);
|
|
81
|
+
|
|
82
|
+
try { db.load(); } catch (err) { process.stderr.write(`⏳ Waiting for initial indexing… (${err.message})\n`); }
|
|
83
|
+
|
|
84
|
+
// In-memory backend: the daemon is a separate process that rewrites
|
|
85
|
+
// code-index.json — without reloading, this server would answer from a stale
|
|
86
|
+
// snapshot until restart. (The SQLite store refreshes itself per query via
|
|
87
|
+
// PRAGMA data_version, so no watcher is needed there.)
|
|
88
|
+
if (backend !== 'sqlite' && typeof db.reload === 'function') {
|
|
89
|
+
let reloadTimer = null;
|
|
90
|
+
const scheduleReload = () => {
|
|
91
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
92
|
+
reloadTimer = setTimeout(() => {
|
|
93
|
+
reloadTimer = null;
|
|
94
|
+
try {
|
|
95
|
+
db.reload();
|
|
96
|
+
process.stderr.write('🔄 Index reloaded from disk (daemon update).\n');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
process.stderr.write(`⚠️ Index reload failed: ${err.message}\n`);
|
|
99
|
+
}
|
|
100
|
+
}, 1000);
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
fs.watch(path.dirname(config.indexPath), (event, name) => {
|
|
104
|
+
if (name === path.basename(config.indexPath)) scheduleReload();
|
|
105
|
+
});
|
|
106
|
+
} catch { /* fs.watch unavailable — index stays load-time static */ }
|
|
107
|
+
}
|
|
118
108
|
|
|
119
|
-
// Graceful shutdown
|
|
120
109
|
process.on('SIGTERM', () => { db.close(); process.exit(0); });
|
|
121
110
|
process.on('SIGINT', () => { db.close(); process.exit(0); });
|
|
122
111
|
|
|
123
|
-
// ─── Graph
|
|
124
|
-
|
|
112
|
+
// ─── Graph dependency resource ─────────────────────────────────────────────────
|
|
125
113
|
server.resource(
|
|
126
|
-
|
|
127
|
-
new ResourceTemplate(
|
|
114
|
+
'graph-dependencies',
|
|
115
|
+
new ResourceTemplate('graph://dependencies/{file_path}', {
|
|
128
116
|
list: async () => ({
|
|
129
117
|
resources: Object.keys(db.graph.dependencies).map(fp => ({
|
|
130
118
|
uri: `graph://dependencies/${encodeURIComponent(fp)}`,
|
|
131
|
-
name: fp, mimeType:
|
|
119
|
+
name: fp, mimeType: 'text/markdown',
|
|
132
120
|
description: `Dependency topology for ${fp}`,
|
|
133
121
|
}))
|
|
134
122
|
})
|
|
135
123
|
}),
|
|
136
124
|
async (uri, { file_path }) => {
|
|
137
125
|
const p = decodeURIComponent(String(file_path));
|
|
138
|
-
const deps = db.
|
|
139
|
-
const usedBy = db.
|
|
126
|
+
const deps = db.getDependencies(p);
|
|
127
|
+
const usedBy = db.getImportedBy(p);
|
|
140
128
|
const md = [
|
|
141
129
|
`# Dependency Topology: \`${p}\``, '',
|
|
142
130
|
`## Imports (${deps.length})`,
|
|
@@ -144,485 +132,42 @@ server.resource(
|
|
|
144
132
|
`## Imported By (${usedBy.length})`,
|
|
145
133
|
usedBy.length ? usedBy.map(d => `- \`${d}\``).join('\n') : '_No files import this_',
|
|
146
134
|
].join('\n');
|
|
147
|
-
return { contents: [{ uri: uri.href, mimeType:
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
// ─── search_code ──────────────────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
server.tool(
|
|
154
|
-
"search_code",
|
|
155
|
-
"CRITICAL: ALWAYS USE THIS TOOL FIRST to find code. High-precision AST hybrid search returning exact chunks and cross-file topology.",
|
|
156
|
-
{
|
|
157
|
-
query: z.string().describe("Natural language description of the logic to find."),
|
|
158
|
-
exact_tokens: z.string().optional().describe("Exact symbol name for guaranteed rank-1 placement."),
|
|
159
|
-
include_topology: z.boolean().default(true),
|
|
160
|
-
min_score: z.number().min(0).max(1).default(0.3),
|
|
161
|
-
top_k: z.number().int().min(1).max(20).default(5),
|
|
162
|
-
token_budget: z.number().int().min(100).optional().describe(
|
|
163
|
-
"Token budget for code bodies (1 token ≈ 4 chars). Omit to use smart default."
|
|
164
|
-
),
|
|
165
|
-
detail: z.enum(['signatures', 'smart', 'full']).default('smart').describe(
|
|
166
|
-
"'signatures': compact cards only (~20 tok each, no bodies) — fastest. " +
|
|
167
|
-
"'smart' (default): signatures + query-relevant body snippets. " +
|
|
168
|
-
"'full': signatures + complete bodies."
|
|
169
|
-
),
|
|
170
|
-
},
|
|
171
|
-
async ({ query, exact_tokens, include_topology, min_score, top_k, token_budget, detail }) => {
|
|
172
|
-
try {
|
|
173
|
-
const fullQuery = exact_tokens ? `${query} ${exact_tokens}` : query;
|
|
174
|
-
let queryVector = null;
|
|
175
|
-
try { queryVector = await getLocalEmbedding(fullQuery); } catch { /* lexical fallback */ }
|
|
176
|
-
|
|
177
|
-
const matches = db.searchHybrid(fullQuery, queryVector, top_k, min_score, exact_tokens || null);
|
|
178
|
-
if (matches.length === 0) return { content: [{ type: "text", text: "No results found." }] };
|
|
179
|
-
|
|
180
|
-
const depSignature = (depPath) => {
|
|
181
|
-
const syms = [];
|
|
182
|
-
for (const c of db.chunks.values()) {
|
|
183
|
-
if (c.file_path === depPath && c.name && c.node_type !== 'expression_statement') {
|
|
184
|
-
syms.push(c.name);
|
|
185
|
-
if (syms.length >= 4) break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return syms.length ? `${depPath} [${syms.join(', ')}]` : depPath;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const lines = [`🔍 QUERY: "${fullQuery}" — ${matches.length} result(s)\n`];
|
|
192
|
-
|
|
193
|
-
// Phase 1: Signature cards (always emitted)
|
|
194
|
-
for (let i = 0; i < matches.length; i++) {
|
|
195
|
-
const { score, chunk } = matches[i];
|
|
196
|
-
lines.push(`${'─'.repeat(50)}`);
|
|
197
|
-
lines.push(`#${i + 1} · **${chunk.name}** [${chunk.node_type}]`);
|
|
198
|
-
lines.push(`📄 ${chunk.file_path}:${chunk.start_line}–${chunk.end_line} · ID: \`${chunk.id}\` · RRF: ${score.toFixed(4)}`);
|
|
199
|
-
|
|
200
|
-
// Frontier 3: show return type and key params in card
|
|
201
|
-
const sig = [];
|
|
202
|
-
if (chunk.params?.length) sig.push(`(${chunk.params.slice(0, 4).join(', ')})`);
|
|
203
|
-
if (chunk.return_type) sig.push(`→ ${chunk.return_type}`);
|
|
204
|
-
if (chunk.type_refs?.length) sig.push(`types: ${chunk.type_refs.slice(0, 4).join(', ')}`);
|
|
205
|
-
if (sig.length) lines.push(`🔤 ${sig.join(' ')}`);
|
|
206
|
-
|
|
207
|
-
if (chunk.docstring) lines.push(`💬 ${chunk.docstring.slice(0, 140).replace(/\n/g, ' ')}`);
|
|
208
|
-
|
|
209
|
-
if (include_topology) {
|
|
210
|
-
const deps = (db.graph.dependencies[chunk.file_path] || []).slice(0, 3);
|
|
211
|
-
const usedBy = (db.graph.importedBy[chunk.file_path] || []).slice(0, 3);
|
|
212
|
-
if (deps.length) lines.push(`⬇️ Deps: ${deps.map(depSignature).join(' | ')}`);
|
|
213
|
-
if (usedBy.length) lines.push(`⬆️ Used by: ${usedBy.join(', ')}`);
|
|
214
|
-
if (chunk.calls?.length) lines.push(`🔗 Calls: ${chunk.calls.slice(0, 6).join(', ')}`);
|
|
215
|
-
}
|
|
216
|
-
lines.push(`↩️ Expand: get_chunk("${chunk.id}")`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Phase 2: Code bodies — controlled by `detail`
|
|
220
|
-
if (detail !== 'signatures') {
|
|
221
|
-
const CHARS_PER_TOKEN = 4;
|
|
222
|
-
const defaultBudget = detail === 'full' ? 6000 : 2000;
|
|
223
|
-
let remainingChars = token_budget != null ? token_budget * CHARS_PER_TOKEN : defaultBudget;
|
|
224
|
-
|
|
225
|
-
lines.push(`\n${'═'.repeat(50)}`);
|
|
226
|
-
lines.push(`CODE BODIES (detail: ${detail}, budget: ~${Math.round(remainingChars / CHARS_PER_TOKEN)} tok)\n`);
|
|
227
|
-
|
|
228
|
-
const queryTokens = fullQuery.toLowerCase().split(/[\s\W_]+/).filter(t => t.length >= 3);
|
|
229
|
-
|
|
230
|
-
for (const { chunk } of matches) {
|
|
231
|
-
if (remainingChars <= 0) break;
|
|
232
|
-
const raw = chunk.code_snippet || '';
|
|
233
|
-
if (!raw) continue;
|
|
234
|
-
|
|
235
|
-
let snippet;
|
|
236
|
-
if (detail === 'full') {
|
|
237
|
-
snippet = raw.slice(0, remainingChars);
|
|
238
|
-
} else {
|
|
239
|
-
// 'smart': query-driven pruning for long functions
|
|
240
|
-
snippet = pruneBodyByQuery(raw, queryTokens).slice(0, remainingChars);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
lines.push(`### ${chunk.name} — ${chunk.file_path}`);
|
|
244
|
-
lines.push('```\n' + snippet + '\n```\n');
|
|
245
|
-
remainingChars -= snippet.length;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
250
|
-
} catch (err) {
|
|
251
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
// ─── get_chunk ────────────────────────────────────────────────────────────────
|
|
257
|
-
|
|
258
|
-
server.tool(
|
|
259
|
-
"get_chunk",
|
|
260
|
-
"CRITICAL: Use this INSTEAD of reading full files. Returns the complete body of a function/class by its chunk_id from search_code results.",
|
|
261
|
-
{
|
|
262
|
-
chunk_id: z.string().describe("The chunk ID shown in search_code results."),
|
|
263
|
-
view: z.enum(['full', 'signature']).default('full').describe(
|
|
264
|
-
"'full': complete source body. 'signature': just the function signature line (~5 tokens)."
|
|
265
|
-
),
|
|
266
|
-
},
|
|
267
|
-
async ({ chunk_id, view }) => {
|
|
268
|
-
try {
|
|
269
|
-
const chunk = db.chunks.get(chunk_id);
|
|
270
|
-
if (!chunk) return { content: [{ type: "text", text: `Chunk '${chunk_id}' not found. Run search_code to get valid IDs.` }] };
|
|
271
|
-
|
|
272
|
-
const parts = [
|
|
273
|
-
`# ${chunk.name}`,
|
|
274
|
-
`**File:** \`${chunk.file_path}\` · **Lines:** ${chunk.start_line}–${chunk.end_line} · **Type:** ${chunk.node_type}`,
|
|
275
|
-
];
|
|
276
|
-
if (chunk.params?.length) parts.push(`**Params:** ${chunk.params.join(', ')}`);
|
|
277
|
-
if (chunk.return_type) parts.push(`**Returns:** ${chunk.return_type}`);
|
|
278
|
-
if (chunk.type_refs?.length) parts.push(`**Type refs:** ${chunk.type_refs.join(', ')}`);
|
|
279
|
-
if (chunk.docstring) parts.push(`**Doc:** ${chunk.docstring}`);
|
|
280
|
-
|
|
281
|
-
const deps = db.graph.dependencies[chunk.file_path] || [];
|
|
282
|
-
const usedBy = db.graph.importedBy[chunk.file_path] || [];
|
|
283
|
-
if (deps.length) parts.push(`⬇️ Imports: ${deps.join(', ')}`);
|
|
284
|
-
if (usedBy.length) parts.push(`⬆️ Used by: ${usedBy.join(', ')}`);
|
|
285
|
-
if (chunk.calls?.length) parts.push(`🔗 Calls: ${chunk.calls.join(', ')}`);
|
|
286
|
-
|
|
287
|
-
if (view === 'signature') {
|
|
288
|
-
parts.push('', '```', extractSignatureLine(chunk.code_snippet), '```');
|
|
289
|
-
} else {
|
|
290
|
-
parts.push('', '```', chunk.code_snippet, '```');
|
|
291
|
-
}
|
|
292
|
-
return { content: [{ type: "text", text: parts.join('\n') }] };
|
|
293
|
-
} catch (err) {
|
|
294
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
295
|
-
}
|
|
135
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text: md }] };
|
|
296
136
|
}
|
|
297
137
|
);
|
|
298
138
|
|
|
299
|
-
// ───
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const chunkIds = db.symbolTable.get(key);
|
|
311
|
-
|
|
312
|
-
if (!chunkIds || chunkIds.size === 0) {
|
|
313
|
-
return { content: [{ type: "text", text: `Symbol '${symbol}' not in index. Try search_code(query="${symbol}") for fuzzy search.` }] };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const lines = [`# Symbol: \`${symbol}\` — ${chunkIds.size} definition(s)\n`];
|
|
317
|
-
for (const chunkId of chunkIds) {
|
|
318
|
-
const chunk = db.chunks.get(chunkId);
|
|
319
|
-
if (!chunk) continue;
|
|
320
|
-
lines.push(`${'─'.repeat(50)}`);
|
|
321
|
-
lines.push(`**${chunk.name}** [${chunk.node_type}]`);
|
|
322
|
-
lines.push(`📄 ${chunk.file_path}:${chunk.start_line}–${chunk.end_line} · ID: \`${chunk.id}\``);
|
|
323
|
-
if (chunk.params?.length) lines.push(`🔤 Params: ${chunk.params.join(', ')}`);
|
|
324
|
-
if (chunk.return_type) lines.push(`🔤 Returns: ${chunk.return_type}`);
|
|
325
|
-
if (chunk.type_refs?.length) lines.push(`🔗 Type refs: ${chunk.type_refs.join(', ')}`);
|
|
326
|
-
if (chunk.docstring) lines.push(`💬 ${chunk.docstring.slice(0, 160).replace(/\n/g, ' ')}`);
|
|
327
|
-
const deps = (db.graph.dependencies[chunk.file_path] || []).slice(0, 4);
|
|
328
|
-
const usedBy = (db.graph.importedBy[chunk.file_path] || []).slice(0, 4);
|
|
329
|
-
if (deps.length) lines.push(`⬇️ Imports: ${deps.join(', ')}`);
|
|
330
|
-
if (usedBy.length) lines.push(`⬆️ Used by: ${usedBy.join(', ')}`);
|
|
331
|
-
if (chunk.calls?.length) lines.push(`🔗 Calls: ${chunk.calls.slice(0, 8).join(', ')}`);
|
|
332
|
-
lines.push(`\n\`\`\`\n${extractSignatureLine(chunk.code_snippet)}\n\`\`\``);
|
|
333
|
-
lines.push(`↩️ Full body: get_chunk("${chunk.id}")`);
|
|
334
|
-
}
|
|
335
|
-
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
336
|
-
} catch (err) {
|
|
337
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
// ─── get_chunk_summary (Frontier 3) ──────────────────────────────────────────
|
|
343
|
-
|
|
344
|
-
server.tool(
|
|
345
|
-
"get_chunk_summary",
|
|
346
|
-
"Returns the function/class signature + docstring + called functions — no full body. ~50 tokens vs ~300 for full body. Use when you only need to understand the interface, not the implementation.",
|
|
347
|
-
{
|
|
348
|
-
chunk_id: z.string().describe("The chunk ID from search_code results."),
|
|
349
|
-
},
|
|
350
|
-
async ({ chunk_id }) => {
|
|
351
|
-
try {
|
|
352
|
-
const chunk = db.chunks.get(chunk_id);
|
|
353
|
-
if (!chunk) return { content: [{ type: "text", text: `Chunk '${chunk_id}' not found.` }] };
|
|
354
|
-
|
|
355
|
-
const lines = [
|
|
356
|
-
`# ${chunk.name} · ${chunk.file_path}:${chunk.start_line}–${chunk.end_line}`,
|
|
357
|
-
`**Type:** ${chunk.node_type}`,
|
|
358
|
-
];
|
|
359
|
-
if (chunk.params?.length) lines.push(`**Params:** ${chunk.params.join(', ')}`);
|
|
360
|
-
if (chunk.return_type) lines.push(`**Returns:** ${chunk.return_type}`);
|
|
361
|
-
if (chunk.type_refs?.length) lines.push(`**Type refs:** ${chunk.type_refs.join(', ')}`);
|
|
362
|
-
if (chunk.docstring) lines.push(`\n**Doc:** ${chunk.docstring.slice(0, 300)}`);
|
|
363
|
-
if (chunk.calls?.length) lines.push(`**Calls:** ${chunk.calls.join(', ')}`);
|
|
364
|
-
lines.push('', '```', extractSignatureLine(chunk.code_snippet), '```');
|
|
365
|
-
lines.push(`\n↩️ Full body: get_chunk("${chunk.id}")`);
|
|
366
|
-
|
|
367
|
-
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
368
|
-
} catch (err) {
|
|
369
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
// ─── get_file_skeleton ────────────────────────────────────────────────────────
|
|
375
|
-
|
|
376
|
-
server.tool(
|
|
377
|
-
"get_file_skeleton",
|
|
378
|
-
"Returns all top-level exports and definitions in a file with line numbers — no code bodies (~50 tokens vs 5000).",
|
|
379
|
-
{ file_path: z.string().describe("Relative path (e.g. 'src/app.ts').") },
|
|
380
|
-
async ({ file_path }) => {
|
|
381
|
-
try {
|
|
382
|
-
const absolutePath = resolve(PROJECT_ROOT, file_path);
|
|
383
|
-
const safeRoot = path.normalize(PROJECT_ROOT);
|
|
384
|
-
if (!path.normalize(absolutePath).startsWith(safeRoot + path.sep) &&
|
|
385
|
-
path.normalize(absolutePath) !== safeRoot) {
|
|
386
|
-
throw new Error("Access denied: path is outside the project root.");
|
|
387
|
-
}
|
|
388
|
-
if (!fs.existsSync(absolutePath)) throw new Error("File not found.");
|
|
389
|
-
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
390
|
-
const ext = path.extname(absolutePath);
|
|
391
|
-
const parser = getParserForFile(ext);
|
|
392
|
-
if (!parser) return { content: [{ type: "text", text: "Language not supported." }] };
|
|
393
|
-
const tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null);
|
|
394
|
-
const skeleton = extractFileSkeleton(tree.rootNode, content);
|
|
395
|
-
return { content: [{ type: "text", text: `# Skeleton: ${file_path}\n\n${skeleton || "_No semantic signatures found_"}` }] };
|
|
396
|
-
} catch (err) {
|
|
397
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
// ─── get_call_graph ───────────────────────────────────────────────────────────
|
|
403
|
-
|
|
404
|
-
server.tool(
|
|
405
|
-
"get_call_graph",
|
|
406
|
-
"Finds all chunks that call a specific function. CRITICAL for safe refactoring.",
|
|
407
|
-
{ target_function: z.string().describe("Exact function name (e.g. 'validateToken').") },
|
|
408
|
-
async ({ target_function }) => {
|
|
409
|
-
try {
|
|
410
|
-
const callers = [];
|
|
411
|
-
for (const chunk of db.chunks.values()) {
|
|
412
|
-
if (chunk.calls?.includes(target_function)) {
|
|
413
|
-
callers.push(`- [${chunk.node_type}] \`${chunk.name}\` in \`${chunk.file_path}\` (lines ${chunk.start_line}–${chunk.end_line})`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if (callers.length === 0) {
|
|
417
|
-
return { content: [{ type: "text", text: `✅ Safe to modify: no callers of '${target_function}' found.` }] };
|
|
418
|
-
}
|
|
419
|
-
return {
|
|
420
|
-
content: [{
|
|
421
|
-
type: "text", text: [
|
|
422
|
-
`# ⚠️ Call Graph: \`${target_function}\``,
|
|
423
|
-
`${callers.length} caller(s) depend on this — review before changing signature:`,
|
|
424
|
-
...callers
|
|
425
|
-
].join('\n')
|
|
426
|
-
}]
|
|
427
|
-
};
|
|
428
|
-
} catch (err) {
|
|
429
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
// ─── get_repo_map ─────────────────────────────────────────────────────────────
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Compute simplified PageRank over the file dependency graph.
|
|
438
|
-
* Files imported by many others receive higher rank (= more important).
|
|
439
|
-
*/
|
|
440
|
-
function computePageRank(graph, iters = 30, damping = 0.85) {
|
|
441
|
-
const files = Object.keys(graph.dependencies || {});
|
|
442
|
-
const N = files.length;
|
|
443
|
-
if (N === 0) return new Map();
|
|
444
|
-
|
|
445
|
-
const idx = new Map(files.map((f, i) => [f, i]));
|
|
446
|
-
const ranks = new Float64Array(N).fill(1.0 / N);
|
|
447
|
-
const outDeg = files.map(f => Math.max((graph.dependencies[f] || []).length, 1));
|
|
448
|
-
|
|
449
|
-
for (let iter = 0; iter < iters; iter++) {
|
|
450
|
-
const next = new Float64Array(N).fill((1 - damping) / N);
|
|
451
|
-
for (let i = 0; i < N; i++) {
|
|
452
|
-
const contrib = damping * ranks[i] / outDeg[i];
|
|
453
|
-
for (const dep of (graph.dependencies[files[i]] || [])) {
|
|
454
|
-
const j = idx.get(dep);
|
|
455
|
-
if (j !== undefined) next[j] += contrib;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
ranks.set(next);
|
|
459
|
-
}
|
|
460
|
-
return new Map(files.map((f, i) => [f, ranks[i]]));
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
server.tool(
|
|
464
|
-
"get_repo_map",
|
|
465
|
-
"Returns a compact symbol map of the entire codebase grouped by file, ordered by importance (most-imported files first via PageRank). Use this FIRST to orient yourself in an unfamiliar codebase — ~1-2k tokens vs reading every file. Combine with path_filter to focus on a subsystem.",
|
|
466
|
-
{
|
|
467
|
-
path_filter: z.string().optional().describe(
|
|
468
|
-
"Only include files whose path contains this string (e.g. 'auth', 'api/v2', 'src/core')."
|
|
469
|
-
),
|
|
470
|
-
max_files: z.number().int().min(1).max(300).default(80).describe(
|
|
471
|
-
"Max files to include in the map."
|
|
472
|
-
),
|
|
473
|
-
sort_by: z.enum(['importance', 'path']).default('importance').describe(
|
|
474
|
-
"'importance' (default): most-imported files first (PageRank). 'path': alphabetical."
|
|
475
|
-
),
|
|
476
|
-
},
|
|
477
|
-
async ({ path_filter, max_files, sort_by }) => {
|
|
478
|
-
try {
|
|
479
|
-
// Group chunks by file
|
|
480
|
-
const fileChunks = new Map();
|
|
481
|
-
for (const chunk of db.chunks.values()) {
|
|
482
|
-
if (path_filter && !chunk.file_path.toLowerCase().includes(path_filter.toLowerCase())) continue;
|
|
483
|
-
if (chunk.name === 'anonymous' || chunk.name === 'default_export') continue;
|
|
484
|
-
if (!fileChunks.has(chunk.file_path)) fileChunks.set(chunk.file_path, []);
|
|
485
|
-
fileChunks.get(chunk.file_path).push(chunk);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (fileChunks.size === 0) {
|
|
489
|
-
return {
|
|
490
|
-
content: [{
|
|
491
|
-
type: 'text', text: path_filter
|
|
492
|
-
? `No files found matching '${path_filter}'. Try a broader filter.`
|
|
493
|
-
: 'Index is empty. Run `npm run mcp:index` first.'
|
|
494
|
-
}]
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Sort files
|
|
499
|
-
let sortedFiles = Array.from(fileChunks.keys());
|
|
500
|
-
if (sort_by === 'importance') {
|
|
501
|
-
const pr = computePageRank(db.graph);
|
|
502
|
-
sortedFiles.sort((a, b) => (pr.get(b) ?? 0) - (pr.get(a) ?? 0));
|
|
503
|
-
} else {
|
|
504
|
-
sortedFiles.sort();
|
|
505
|
-
}
|
|
506
|
-
sortedFiles = sortedFiles.slice(0, max_files);
|
|
507
|
-
|
|
508
|
-
// Render compact map
|
|
509
|
-
const totalFiles = fileChunks.size;
|
|
510
|
-
const totalSymbols = Array.from(fileChunks.values()).reduce((s, a) => s + a.length, 0);
|
|
511
|
-
const lines = [
|
|
512
|
-
`# Repo Map — ${totalSymbols} symbols across ${totalFiles} files`,
|
|
513
|
-
path_filter ? `(filtered to '${path_filter}')` : '',
|
|
514
|
-
sortedFiles.length < totalFiles
|
|
515
|
-
? `(showing top ${sortedFiles.length} by ${sort_by}; use path_filter to narrow)\n`
|
|
516
|
-
: '',
|
|
517
|
-
].filter(Boolean);
|
|
518
|
-
|
|
519
|
-
for (const filePath of sortedFiles) {
|
|
520
|
-
const chunks = fileChunks.get(filePath);
|
|
521
|
-
lines.push(`\n${filePath}`);
|
|
522
|
-
// Deduplicate by name (prefer class/function over expression_statement)
|
|
523
|
-
const seen = new Set();
|
|
524
|
-
const deduped = [];
|
|
525
|
-
for (const c of chunks) {
|
|
526
|
-
const key = c.name.toLowerCase();
|
|
527
|
-
if (seen.has(key)) continue;
|
|
528
|
-
seen.add(key);
|
|
529
|
-
deduped.push(c);
|
|
530
|
-
}
|
|
531
|
-
for (const c of deduped.slice(0, 8)) {
|
|
532
|
-
const kind = c.node_type.includes('class') ? 'class'
|
|
533
|
-
: c.node_type.includes('interface') ? 'interface'
|
|
534
|
-
: c.node_type.includes('enum') ? 'enum'
|
|
535
|
-
: c.node_type.includes('struct') ? 'struct'
|
|
536
|
-
: c.node_type.includes('trait') ? 'trait'
|
|
537
|
-
: 'fn';
|
|
538
|
-
const params = c.params?.length
|
|
539
|
-
? `(${c.params.slice(0, 3).join(', ')}${c.params.length > 3 ? ', …' : ''})`
|
|
540
|
-
: '';
|
|
541
|
-
const ret = c.return_type ? ` → ${c.return_type.slice(0, 40)}` : '';
|
|
542
|
-
lines.push(` ${kind} ${c.name}${params}${ret}`);
|
|
543
|
-
}
|
|
544
|
-
if (deduped.length > 8) lines.push(` … (${deduped.length - 8} more)`);
|
|
545
|
-
}
|
|
546
|
-
if (totalFiles > max_files) {
|
|
547
|
-
lines.push(`\n… ${totalFiles - max_files} more files not shown. Use path_filter or increase max_files.`);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
551
|
-
} catch (err) {
|
|
552
|
-
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
// ─── list_index_stats ─────────────────────────────────────────────────────────
|
|
558
|
-
|
|
559
|
-
server.tool(
|
|
560
|
-
"list_index_stats",
|
|
561
|
-
"Returns index health: chunk count, embedding status, daemon status, search mode, and index freshness.",
|
|
562
|
-
{},
|
|
563
|
-
async () => {
|
|
564
|
-
try {
|
|
565
|
-
const chunkCount = db.chunks.size;
|
|
566
|
-
const fileSet = new Set([...db.chunks.values()].map(c => c.file_path));
|
|
567
|
-
const hasVectors = db.vectors.size > 0 || db._vecOffsets.size > 0;
|
|
568
|
-
const embeddingsEnabled = process.env.INDEXER_EMBEDDINGS !== 'off';
|
|
569
|
-
|
|
570
|
-
let indexAge = 'unknown';
|
|
571
|
-
try {
|
|
572
|
-
const ageMs = Date.now() - fs.statSync(INDEX_PATH).mtimeMs;
|
|
573
|
-
const ageSec = Math.floor(ageMs / 1000);
|
|
574
|
-
indexAge = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : `${Math.floor(ageSec / 3600)}h ago`;
|
|
575
|
-
} catch { }
|
|
576
|
-
|
|
577
|
-
let daemonStatus = 'unknown';
|
|
578
|
-
try {
|
|
579
|
-
if (fs.existsSync(PID_FILE)) {
|
|
580
|
-
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
581
|
-
process.kill(pid, 0);
|
|
582
|
-
daemonStatus = `running (PID: ${pid})`;
|
|
583
|
-
} else { daemonStatus = 'not running'; }
|
|
584
|
-
} catch { daemonStatus = 'not running (stale PID)'; }
|
|
585
|
-
|
|
586
|
-
const extCounts = new Map();
|
|
587
|
-
for (const chunk of db.chunks.values()) {
|
|
588
|
-
const ext = chunk.file_path.split('.').pop() || 'unknown';
|
|
589
|
-
extCounts.set(ext, (extCounts.get(ext) || 0) + 1);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const lazyModeActive = db._lazyMode;
|
|
593
|
-
const vectorSource = lazyModeActive
|
|
594
|
-
? (db._vecFd >= 0 ? 'disk-backed fd' : 'buffer-lazy')
|
|
595
|
-
: 'eager (in-memory matrix)';
|
|
596
|
-
const searchMode = !embeddingsEnabled
|
|
597
|
-
? '🔤 Lexical only (INDEXER_EMBEDDINGS=off)'
|
|
598
|
-
: hasVectors
|
|
599
|
-
? `🧠 Hybrid (semantic + lexical RRF) — vectors: ${vectorSource}`
|
|
600
|
-
: '🔤 Lexical only (Ollama unavailable or not yet indexed)';
|
|
601
|
-
|
|
602
|
-
const lines = [
|
|
603
|
-
`# 📊 graph-indexer Index Stats`, '',
|
|
604
|
-
`| Metric | Value |`, `| :--- | :--- |`,
|
|
605
|
-
`| **Chunks** | ${chunkCount} |`,
|
|
606
|
-
`| **Files indexed** | ${fileSet.size} |`,
|
|
607
|
-
`| **Symbols in table** | ${db.symbolTable.size} |`,
|
|
608
|
-
`| **Vector entries** | ${lazyModeActive ? db._vecOffsets.size : db.vectors.size} |`,
|
|
609
|
-
`| **Search mode** | ${searchMode} |`,
|
|
610
|
-
`| **Lazy vec mode** | ${lazyModeActive ? '✅ Yes (enterprise scale)' : '❌ No (small corpus)'} |`,
|
|
611
|
-
`| **Daemon** | ${daemonStatus} |`,
|
|
612
|
-
`| **Index age** | ${indexAge} |`,
|
|
613
|
-
'', `## Extension Breakdown`,
|
|
614
|
-
...Array.from(extCounts.entries()).sort((a, b) => b[1] - a[1]).map(([e, n]) => `- .${e}: ${n} chunks`),
|
|
615
|
-
];
|
|
616
|
-
if (chunkCount === 0) lines.push('', `⚠️ Index empty. Run \`npm run mcp:index\`.`);
|
|
617
|
-
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
618
|
-
} catch (err) {
|
|
619
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
620
|
-
}
|
|
621
|
-
}
|
|
139
|
+
// ─── Query embedder ────────────────────────────────────────────────────────────
|
|
140
|
+
// Query with the SAME provider/model the index was built with (stamped in the
|
|
141
|
+
// embeddings-bin sidecar). No stamp → an index from an older version: keep the
|
|
142
|
+
// legacy behaviour (try Ollama, fall back to lexical). The local pipeline loads
|
|
143
|
+
// lazily on first query, so an unused provider costs nothing at startup.
|
|
144
|
+
const embedMeta = readEmbedMeta(config.embeddingPath);
|
|
145
|
+
const embedder = await createEmbedder(
|
|
146
|
+
config,
|
|
147
|
+
embedMeta?.provider
|
|
148
|
+
? { provider: embedMeta.provider, model: embedMeta.model }
|
|
149
|
+
: { provider: config.embeddingsEnabled ? 'ollama' : 'off', model: config.embedModel }
|
|
622
150
|
);
|
|
623
151
|
|
|
624
|
-
// ───
|
|
152
|
+
// ─── Git signals (air-gapped sidecar; blast-radius hint + opt-in rank boost) ────
|
|
153
|
+
const gitSignals = config.gitSignals ? loadGitSignals(config.gitSignalsPath) : null;
|
|
154
|
+
|
|
155
|
+
// ─── Tools ───────────────────────────────────────────────────────────────────
|
|
156
|
+
registerTools(server, db, {
|
|
157
|
+
projectRoot: PROJECT_ROOT,
|
|
158
|
+
artifactPath: backend === 'sqlite' ? config.sqlitePath : config.indexPath,
|
|
159
|
+
pidFile: PID_FILE,
|
|
160
|
+
embeddingsEnabled: config.embeddingsEnabled && embedder.provider !== 'off',
|
|
161
|
+
embedder,
|
|
162
|
+
rerank: config.rerank,
|
|
163
|
+
hyde: config.hyde,
|
|
164
|
+
ollamaHost: config.ollamaHost,
|
|
165
|
+
llmProvider: config.llmProvider,
|
|
166
|
+
mlxLmHost: config.mlxLmHost,
|
|
167
|
+
gitSignals,
|
|
168
|
+
gitRankBoost: config.gitRankBoost,
|
|
169
|
+
});
|
|
625
170
|
|
|
626
171
|
const transport = new StdioServerTransport();
|
|
627
172
|
await server.connect(transport);
|
|
628
|
-
process.stderr.write(`✅ graph-indexer MCP server running (v${version}).\n`);
|
|
173
|
+
process.stderr.write(`✅ graph-indexer MCP server running (v${version}, ${backend} backend).\n`);
|