@softerist/heuristic-mcp 2.1.2 → 2.1.4
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 +37 -9
- package/features/index-codebase.js +34 -6
- package/features/register.js +121 -0
- package/index.js +14 -1
- package/lib/cache.js +47 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,14 +86,33 @@ Since it runs the **Local LLM** (Xenova) directly on your machine:
|
|
|
86
86
|
|
|
87
87
|
For a developer (or an AI agent) working on a confusing or large project, this tool is a massive productivity booster. It essentially turns the entire codebase into a searchable database of knowledge.
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
## How This is Different
|
|
90
|
+
|
|
91
|
+
Most MCP servers and RAG tools are "naive"—they just embed code chunks and run a vector search. **Heuristic MCP** is different because it adds **deterministic intelligence** on top of AI:
|
|
92
|
+
|
|
93
|
+
| Feature | Generic MCP / RAG Tool | Heuristic MCP |
|
|
94
|
+
| :- | :- | :- |
|
|
95
|
+
| **Ranking** | Pure similarity score | Similarity + **Call Graph Proximity** + **Recency Boost** |
|
|
96
|
+
| **Logic** | "Is this text similar?" | "Is this similar, AND used by this function, AND active?" |
|
|
97
|
+
| **Refactoring** | N/A | **`find_similar_code`** tool to detect duplicates |
|
|
98
|
+
| **Tuning** | Static (hardcoded) | **Runtime Config** (adjust ANN parameters on the fly) |
|
|
99
|
+
|
|
100
|
+
### Comparison to Cursor
|
|
101
|
+
|
|
102
|
+
[Cursor](https://cursor.sh) is an excellent AI editor with built-in codebase indexing.
|
|
103
|
+
|
|
104
|
+
- **Cursor** is an *Editor*: You must use their IDE to get the features.
|
|
105
|
+
- **Heuristic MCP** is a *Protocol*: It brings Cursor-like intelligence to **any** tool (Claude Desktop, multiple IDEs, agentic workflows) without locking you into a specific editor.
|
|
106
|
+
- **Transparency**: This is open-source. You know exactly how your code is indexed and where the data lives (locally).
|
|
107
|
+
|
|
108
|
+
## Performance
|
|
90
109
|
|
|
91
110
|
- Pre-indexed embeddings are faster than scanning files at runtime
|
|
92
111
|
- Smart project detection skips dependencies automatically (node_modules, vendor, etc.)
|
|
93
112
|
- Incremental updates - only re-processes changed files
|
|
94
113
|
- Optional ANN search (HNSW) for faster queries on large codebases
|
|
95
114
|
|
|
96
|
-
|
|
115
|
+
## Privacy
|
|
97
116
|
|
|
98
117
|
- Everything runs locally on your machine
|
|
99
118
|
- Your code never leaves your system
|
|
@@ -157,6 +176,22 @@ Add the server configuration to the `mcpServers` object in your config file:
|
|
|
157
176
|
}
|
|
158
177
|
```
|
|
159
178
|
|
|
179
|
+
### Auto-Fix Configuration (New!)
|
|
180
|
+
|
|
181
|
+
To automatically configure your IDEs (Antigravity, Claude, Cursor) with the correct path:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
heuristic-mcp --register
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This will automatically find your IDE config files and inject the correct absolute path to the server. You can also target a specific IDE:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
heuristic-mcp --register antigravity
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
160
195
|
## Environment Variables
|
|
161
196
|
|
|
162
197
|
Override configuration settings via environment variables in your MCP config:
|
|
@@ -269,13 +304,6 @@ Query: "error handling and exceptions"
|
|
|
269
304
|
|
|
270
305
|
Finds all try/catch blocks and error handling patterns.
|
|
271
306
|
|
|
272
|
-
## Privacy
|
|
273
|
-
|
|
274
|
-
- AI model runs entirely on your machine
|
|
275
|
-
- No network requests to external services
|
|
276
|
-
- No telemetry or analytics
|
|
277
|
-
- Cache stored locally in `.smart-coding-cache/`
|
|
278
|
-
|
|
279
307
|
## Technical Details
|
|
280
308
|
|
|
281
309
|
**Embedding Model**: all-MiniLM-L6-v2 via transformers.js
|
|
@@ -503,6 +503,7 @@ export class CodebaseIndexer {
|
|
|
503
503
|
console.error("[Indexer] Force reindex requested: clearing cache");
|
|
504
504
|
this.cache.setVectorStore([]);
|
|
505
505
|
this.cache.fileHashes = new Map();
|
|
506
|
+
await this.cache.clearCallGraphData({ removeFile: true });
|
|
506
507
|
}
|
|
507
508
|
|
|
508
509
|
const totalStartTime = Date.now();
|
|
@@ -520,9 +521,10 @@ export class CodebaseIndexer {
|
|
|
520
521
|
// Send progress: discovery complete
|
|
521
522
|
this.sendProgress(5, 100, `Discovered ${files.length} files`);
|
|
522
523
|
|
|
524
|
+
const currentFilesSet = new Set(files);
|
|
525
|
+
|
|
523
526
|
// Step 1.5: Prune deleted or excluded files from cache
|
|
524
527
|
if (!force) {
|
|
525
|
-
const currentFilesSet = new Set(files);
|
|
526
528
|
const cachedFiles = Array.from(this.cache.fileHashes.keys());
|
|
527
529
|
let prunedCount = 0;
|
|
528
530
|
|
|
@@ -540,10 +542,16 @@ export class CodebaseIndexer {
|
|
|
540
542
|
}
|
|
541
543
|
// If we pruned files, we should save these changes even if no other files changed
|
|
542
544
|
}
|
|
545
|
+
|
|
546
|
+
const prunedCallGraph = this.cache.pruneCallGraphData(currentFilesSet);
|
|
547
|
+
if (prunedCallGraph > 0 && this.config.verbose) {
|
|
548
|
+
console.error(`[Indexer] Pruned ${prunedCallGraph} call-graph entries`);
|
|
549
|
+
}
|
|
543
550
|
}
|
|
544
551
|
|
|
545
552
|
// Step 2: Pre-filter unchanged files (early hash check)
|
|
546
553
|
const filesToProcess = await this.preFilterFiles(files);
|
|
554
|
+
const filesToProcessSet = new Set(filesToProcess.map(entry => entry.file));
|
|
547
555
|
|
|
548
556
|
if (filesToProcess.length === 0) {
|
|
549
557
|
console.error("[Indexer] All files unchanged, nothing to index");
|
|
@@ -556,17 +564,37 @@ export class CodebaseIndexer {
|
|
|
556
564
|
|
|
557
565
|
const missingCallData = [];
|
|
558
566
|
for (const file of cachedFiles) {
|
|
559
|
-
if (!callDataFiles.has(file)) {
|
|
567
|
+
if (!callDataFiles.has(file) && currentFilesSet.has(file)) {
|
|
560
568
|
missingCallData.push(file);
|
|
561
569
|
}
|
|
562
570
|
}
|
|
563
571
|
|
|
564
572
|
if (missingCallData.length > 0) {
|
|
565
573
|
console.error(`[Indexer] Found ${missingCallData.length} files missing call graph data, re-indexing...`);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
574
|
+
const BATCH_SIZE = 100;
|
|
575
|
+
for (let i = 0; i < missingCallData.length; i += BATCH_SIZE) {
|
|
576
|
+
const batch = missingCallData.slice(i, i + BATCH_SIZE);
|
|
577
|
+
const results = await Promise.all(
|
|
578
|
+
batch.map(async (file) => {
|
|
579
|
+
try {
|
|
580
|
+
const stats = await fs.stat(file);
|
|
581
|
+
if (stats.isDirectory()) return null;
|
|
582
|
+
if (stats.size > this.config.maxFileSize) return null;
|
|
583
|
+
const content = await fs.readFile(file, "utf-8");
|
|
584
|
+
const hash = hashContent(content);
|
|
585
|
+
return { file, content, hash };
|
|
586
|
+
} catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
for (const result of results) {
|
|
593
|
+
if (!result) continue;
|
|
594
|
+
if (filesToProcessSet.has(result.file)) continue;
|
|
595
|
+
filesToProcess.push(result);
|
|
596
|
+
filesToProcessSet.add(result.file);
|
|
597
|
+
}
|
|
570
598
|
}
|
|
571
599
|
}
|
|
572
600
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
// Helper to expand ~ and %vars%
|
|
7
|
+
function expandPath(p) {
|
|
8
|
+
if (p.startsWith('~/')) {
|
|
9
|
+
return path.join(os.homedir(), p.slice(2));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (process.platform === 'win32') {
|
|
13
|
+
return p.replace(/%([^%]+)%/g, (_, n) => process.env[n] || '%' + n + '%');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Known config paths for different IDEs
|
|
20
|
+
function getConfigPaths() {
|
|
21
|
+
const platform = process.platform;
|
|
22
|
+
const home = os.homedir();
|
|
23
|
+
const paths = [];
|
|
24
|
+
|
|
25
|
+
// Antigravity
|
|
26
|
+
if (platform === 'win32') {
|
|
27
|
+
paths.push({
|
|
28
|
+
name: 'Antigravity',
|
|
29
|
+
path: expandPath('%USERPROFILE%\\.gemini\\antigravity\\mcp_config.json')
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
paths.push({
|
|
33
|
+
name: 'Antigravity',
|
|
34
|
+
path: expandPath('~/.gemini/antigravity/mcp_config.json')
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Claude Desktop
|
|
39
|
+
if (platform === 'darwin') {
|
|
40
|
+
paths.push({
|
|
41
|
+
name: 'Claude Desktop',
|
|
42
|
+
path: expandPath('~/Library/Application Support/Claude/claude_desktop_config.json')
|
|
43
|
+
});
|
|
44
|
+
} else if (platform === 'win32') {
|
|
45
|
+
paths.push({
|
|
46
|
+
name: 'Claude Desktop',
|
|
47
|
+
path: expandPath('%APPDATA%\\Claude\\claude_desktop_config.json')
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Cursor (Cascade) - Settings are usually in settings.json but MCP might have a specific spot?
|
|
52
|
+
// Cursor often uses VS Code's settings.json for some things, but explicit MCP support varies.
|
|
53
|
+
// For now, we'll stick to Antigravity and Claude as confirmed targets.
|
|
54
|
+
// NOTE: If Cursor adds a specific mcp_config, add it here.
|
|
55
|
+
|
|
56
|
+
return paths;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function register(filter = null) {
|
|
60
|
+
const binaryPath = process.execPath; // The node binary
|
|
61
|
+
const scriptPath = fileURLToPath(new URL('../index.js', import.meta.url)); // Absolute path to index.js
|
|
62
|
+
|
|
63
|
+
const serverConfig = {
|
|
64
|
+
command: binaryPath,
|
|
65
|
+
args: [scriptPath, "--workspace", process.cwd()],
|
|
66
|
+
disabled: false,
|
|
67
|
+
autoRegistered: true // Marker to know we did this
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const configPaths = getConfigPaths();
|
|
71
|
+
let registeredCount = 0;
|
|
72
|
+
|
|
73
|
+
console.log(`[Auto-Register] Detecting IDE configurations...`);
|
|
74
|
+
|
|
75
|
+
for (const { name, path: configPath } of configPaths) {
|
|
76
|
+
if (filter && name.toLowerCase() !== filter.toLowerCase()) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Check if file exists
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(configPath);
|
|
84
|
+
} catch {
|
|
85
|
+
console.log(`[Auto-Register] Skipped ${name}: Config file not found at ${configPath}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Read config
|
|
90
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
91
|
+
let config = {};
|
|
92
|
+
try {
|
|
93
|
+
config = JSON.parse(content);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(`[Auto-Register] Error parsing ${name} config: ${e.message}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Init mcpServers if missing
|
|
100
|
+
if (!config.mcpServers) {
|
|
101
|
+
config.mcpServers = {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Inject configuration
|
|
105
|
+
config.mcpServers['heuristic-mcp'] = serverConfig;
|
|
106
|
+
|
|
107
|
+
// Write back
|
|
108
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
109
|
+
console.log(`[Auto-Register] ✅ Successfully registered with ${name}`);
|
|
110
|
+
registeredCount++;
|
|
111
|
+
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`[Auto-Register] Failed to register with ${name}: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (registeredCount === 0) {
|
|
118
|
+
console.log(`[Auto-Register] No compatible IDE configurations found to update.`);
|
|
119
|
+
console.log(`[Auto-Register] Manual Config:\n${JSON.stringify({ mcpServers: { "heuristic-mcp": serverConfig } }, null, 2)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/index.js
CHANGED
|
@@ -19,10 +19,23 @@ import * as IndexCodebaseFeature from "./features/index-codebase.js";
|
|
|
19
19
|
import * as HybridSearchFeature from "./features/hybrid-search.js";
|
|
20
20
|
import * as ClearCacheFeature from "./features/clear-cache.js";
|
|
21
21
|
import * as FindSimilarCodeFeature from "./features/find-similar-code.js";
|
|
22
|
-
import
|
|
22
|
+
import { register } from "./features/register.js";
|
|
23
23
|
|
|
24
24
|
// Parse workspace from command line arguments
|
|
25
25
|
const args = process.argv.slice(2);
|
|
26
|
+
|
|
27
|
+
// Check if --register flag is present
|
|
28
|
+
if (args.includes('--register')) {
|
|
29
|
+
// Extract optional filter (e.g. --register antigravity)
|
|
30
|
+
const filterIndex = args.indexOf('--register');
|
|
31
|
+
const filter = args[filterIndex + 1] && !args[filterIndex + 1].startsWith('-')
|
|
32
|
+
? args[filterIndex + 1]
|
|
33
|
+
: null;
|
|
34
|
+
|
|
35
|
+
await register(filter);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
const workspaceIndex = args.findIndex(arg => arg.startsWith('--workspace'));
|
|
27
40
|
let workspaceDir = null;
|
|
28
41
|
|
package/lib/cache.js
CHANGED
|
@@ -6,6 +6,7 @@ const CACHE_META_FILE = "meta.json";
|
|
|
6
6
|
const ANN_META_VERSION = 1;
|
|
7
7
|
const ANN_INDEX_FILE = "ann-index.bin";
|
|
8
8
|
const ANN_META_FILE = "ann-meta.json";
|
|
9
|
+
const CALL_GRAPH_FILE = "call-graph.json";
|
|
9
10
|
|
|
10
11
|
let hnswlibPromise = null;
|
|
11
12
|
let hnswlibLoadError = null;
|
|
@@ -166,7 +167,7 @@ export class EmbeddingsCache {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
// Load call-graph data if it exists
|
|
169
|
-
const callGraphFile = path.join(this.config.cacheDirectory,
|
|
170
|
+
const callGraphFile = path.join(this.config.cacheDirectory, CALL_GRAPH_FILE);
|
|
170
171
|
try {
|
|
171
172
|
const callGraphData = await fs.readFile(callGraphFile, "utf8");
|
|
172
173
|
const parsed = JSON.parse(callGraphData);
|
|
@@ -203,10 +204,12 @@ export class EmbeddingsCache {
|
|
|
203
204
|
fs.writeFile(metaFile, JSON.stringify(this.cacheMeta, null, 2))
|
|
204
205
|
]);
|
|
205
206
|
|
|
206
|
-
// Save call-graph data
|
|
207
|
+
// Save call-graph data (or remove stale cache if empty)
|
|
208
|
+
const callGraphFile = path.join(this.config.cacheDirectory, CALL_GRAPH_FILE);
|
|
207
209
|
if (this.fileCallData.size > 0) {
|
|
208
|
-
const callGraphFile = path.join(this.config.cacheDirectory, "call-graph.json");
|
|
209
210
|
await fs.writeFile(callGraphFile, JSON.stringify(Object.fromEntries(this.fileCallData), null, 2));
|
|
211
|
+
} else {
|
|
212
|
+
await fs.rm(callGraphFile, { force: true });
|
|
210
213
|
}
|
|
211
214
|
} catch (error) {
|
|
212
215
|
console.error("[Cache] Failed to save cache:", error.message);
|
|
@@ -440,9 +443,7 @@ export class EmbeddingsCache {
|
|
|
440
443
|
this.vectorStore = [];
|
|
441
444
|
this.fileHashes = new Map();
|
|
442
445
|
this.invalidateAnnIndex();
|
|
443
|
-
|
|
444
|
-
this.fileCallData.clear();
|
|
445
|
-
this.callGraph = null;
|
|
446
|
+
await this.clearCallGraphData();
|
|
446
447
|
console.error(`[Cache] Cache cleared successfully: ${this.config.cacheDirectory}`);
|
|
447
448
|
} catch (error) {
|
|
448
449
|
console.error("[Cache] Failed to clear cache:", error.message);
|
|
@@ -497,6 +498,46 @@ export class EmbeddingsCache {
|
|
|
497
498
|
|
|
498
499
|
// ========== Call Graph Methods ==========
|
|
499
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Clear all call-graph data (optionally remove persisted cache file)
|
|
503
|
+
*/
|
|
504
|
+
async clearCallGraphData({ removeFile = false } = {}) {
|
|
505
|
+
this.fileCallData.clear();
|
|
506
|
+
this.callGraph = null;
|
|
507
|
+
|
|
508
|
+
if (removeFile && this.config.enableCache) {
|
|
509
|
+
const callGraphFile = path.join(this.config.cacheDirectory, CALL_GRAPH_FILE);
|
|
510
|
+
try {
|
|
511
|
+
await fs.rm(callGraphFile, { force: true });
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (this.config.verbose) {
|
|
514
|
+
console.error(`[Cache] Failed to remove call-graph cache: ${error.message}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Remove call-graph entries for files no longer in the codebase
|
|
522
|
+
*/
|
|
523
|
+
pruneCallGraphData(validFiles) {
|
|
524
|
+
if (!validFiles || this.fileCallData.size === 0) return 0;
|
|
525
|
+
|
|
526
|
+
let pruned = 0;
|
|
527
|
+
for (const file of Array.from(this.fileCallData.keys())) {
|
|
528
|
+
if (!validFiles.has(file)) {
|
|
529
|
+
this.fileCallData.delete(file);
|
|
530
|
+
pruned++;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (pruned > 0) {
|
|
535
|
+
this.callGraph = null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return pruned;
|
|
539
|
+
}
|
|
540
|
+
|
|
500
541
|
/**
|
|
501
542
|
* Store call data for a file
|
|
502
543
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softerist/heuristic-mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"description": "An enhanced MCP server providing intelligent semantic code search with find-similar-code, recency ranking, and improved chunking. Fork of smart-coding-mcp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|