@softerist/heuristic-mcp 2.1.47 → 3.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/.agent/workflows/code-review.md +60 -0
- package/.prettierrc +7 -0
- package/ARCHITECTURE.md +105 -170
- package/CONTRIBUTING.md +32 -113
- package/GEMINI.md +73 -0
- package/LICENSE +21 -21
- package/README.md +161 -54
- package/config.json +876 -75
- package/debug-pids.js +27 -0
- package/eslint.config.js +36 -0
- package/features/ann-config.js +37 -26
- package/features/clear-cache.js +28 -19
- package/features/find-similar-code.js +142 -66
- package/features/hybrid-search.js +253 -93
- package/features/index-codebase.js +1455 -394
- package/features/lifecycle.js +813 -180
- package/features/register.js +58 -52
- package/index.js +450 -306
- package/lib/cache-ops.js +22 -0
- package/lib/cache-utils.js +68 -0
- package/lib/cache.js +1392 -587
- package/lib/call-graph.js +165 -50
- package/lib/cli.js +154 -0
- package/lib/config.js +462 -121
- package/lib/embedding-process.js +77 -0
- package/lib/embedding-worker.js +545 -30
- package/lib/ignore-patterns.js +61 -59
- package/lib/json-worker.js +14 -0
- package/lib/json-writer.js +344 -0
- package/lib/logging.js +88 -0
- package/lib/memory-logger.js +13 -0
- package/lib/project-detector.js +13 -17
- package/lib/server-lifecycle.js +38 -0
- package/lib/settings-editor.js +645 -0
- package/lib/tokenizer.js +207 -104
- package/lib/utils.js +273 -198
- package/lib/vector-store-binary.js +592 -0
- package/mcp_config.example.json +13 -0
- package/package.json +13 -2
- package/scripts/clear-cache.js +6 -17
- package/scripts/download-model.js +14 -9
- package/scripts/postinstall.js +5 -5
- package/search-configs.js +36 -0
- package/test/ann-config.test.js +179 -0
- package/test/ann-fallback.test.js +6 -6
- package/test/binary-store.test.js +69 -0
- package/test/cache-branches.test.js +120 -0
- package/test/cache-errors.test.js +264 -0
- package/test/cache-extra.test.js +300 -0
- package/test/cache-helpers.test.js +205 -0
- package/test/cache-hnsw-failure.test.js +40 -0
- package/test/cache-json-worker.test.js +190 -0
- package/test/cache-worker.test.js +102 -0
- package/test/cache.test.js +443 -0
- package/test/call-graph.test.js +103 -4
- package/test/clear-cache.test.js +69 -68
- package/test/code-review-workflow.test.js +50 -0
- package/test/config.test.js +418 -0
- package/test/coverage-gap.test.js +497 -0
- package/test/coverage-maximizer.test.js +236 -0
- package/test/debug-analysis.js +107 -0
- package/test/embedding-model.test.js +173 -103
- package/test/embedding-worker-extra.test.js +272 -0
- package/test/embedding-worker.test.js +158 -0
- package/test/features.test.js +139 -0
- package/test/final-boost.test.js +271 -0
- package/test/final-polish.test.js +183 -0
- package/test/final.test.js +95 -0
- package/test/find-similar-code.test.js +191 -0
- package/test/helpers.js +92 -11
- package/test/helpers.test.js +46 -0
- package/test/hybrid-search-basic.test.js +62 -0
- package/test/hybrid-search-branch.test.js +202 -0
- package/test/hybrid-search-callgraph.test.js +229 -0
- package/test/hybrid-search-extra.test.js +81 -0
- package/test/hybrid-search.test.js +484 -71
- package/test/index-cli.test.js +520 -0
- package/test/index-codebase-batch.test.js +119 -0
- package/test/index-codebase-branches.test.js +585 -0
- package/test/index-codebase-core.test.js +1032 -0
- package/test/index-codebase-edge-cases.test.js +254 -0
- package/test/index-codebase-errors.test.js +132 -0
- package/test/index-codebase-gap.test.js +239 -0
- package/test/index-codebase-lines.test.js +151 -0
- package/test/index-codebase-watcher.test.js +259 -0
- package/test/index-codebase-zone.test.js +259 -0
- package/test/index-codebase.test.js +371 -69
- package/test/index-memory.test.js +220 -0
- package/test/indexer-detailed.test.js +176 -0
- package/test/integration.test.js +148 -92
- package/test/json-worker.test.js +50 -0
- package/test/lifecycle.test.js +541 -0
- package/test/master.test.js +198 -0
- package/test/perfection.test.js +349 -0
- package/test/project-detector.test.js +65 -0
- package/test/register.test.js +262 -0
- package/test/tokenizer.test.js +55 -93
- package/test/ultra-maximizer.test.js +116 -0
- package/test/utils-branches.test.js +161 -0
- package/test/utils-extra.test.js +116 -0
- package/test/utils.test.js +131 -0
- package/test/verify_fixes.js +76 -0
- package/test/worker-errors.test.js +96 -0
- package/test/worker-init.test.js +102 -0
- package/test/worker_throttling.test.js +93 -0
- package/tools/scripts/benchmark-search.js +95 -0
- package/tools/scripts/cache-stats.js +71 -0
- package/tools/scripts/manual-search.js +34 -0
- package/vitest.config.js +19 -9
package/index.js
CHANGED
|
@@ -1,306 +1,450 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Server } from
|
|
3
|
-
import { stop, start, status } from
|
|
4
|
-
import { StdioServerTransport } from
|
|
5
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from
|
|
6
|
-
import { pipeline } from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
async
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { stop, start, status, logs } from './features/lifecycle.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { pipeline, env } from '@xenova/transformers';
|
|
7
|
+
|
|
8
|
+
// Limit ONNX threads to prevent CPU saturation (tuned to 2 for balanced load)
|
|
9
|
+
if (env?.backends?.onnx) {
|
|
10
|
+
env.backends.onnx.numThreads = 2;
|
|
11
|
+
if (env.backends.onnx.wasm) {
|
|
12
|
+
env.backends.onnx.wasm.numThreads = 2;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
import fs from 'fs/promises';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
|
|
18
|
+
import { createRequire } from 'module';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
|
|
21
|
+
// Import package.json for version
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const packageJson = require('./package.json');
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
import { loadConfig, getGlobalCacheDir } from './lib/config.js';
|
|
27
|
+
import { clearStaleCaches } from './lib/cache-utils.js';
|
|
28
|
+
import { enableStderrOnlyLogging, setupFileLogging } from './lib/logging.js';
|
|
29
|
+
import { parseArgs, printHelp } from './lib/cli.js';
|
|
30
|
+
import { clearCache } from './lib/cache-ops.js';
|
|
31
|
+
import { logMemory, startMemoryLogger } from './lib/memory-logger.js';
|
|
32
|
+
import { registerSignalHandlers, setupPidFile } from './lib/server-lifecycle.js';
|
|
33
|
+
|
|
34
|
+
import { EmbeddingsCache } from './lib/cache.js';
|
|
35
|
+
import { CodebaseIndexer } from './features/index-codebase.js';
|
|
36
|
+
import { HybridSearch } from './features/hybrid-search.js';
|
|
37
|
+
|
|
38
|
+
import * as IndexCodebaseFeature from './features/index-codebase.js';
|
|
39
|
+
import * as HybridSearchFeature from './features/hybrid-search.js';
|
|
40
|
+
import * as ClearCacheFeature from './features/clear-cache.js';
|
|
41
|
+
import * as FindSimilarCodeFeature from './features/find-similar-code.js';
|
|
42
|
+
import * as AnnConfigFeature from './features/ann-config.js';
|
|
43
|
+
import { register } from './features/register.js';
|
|
44
|
+
|
|
45
|
+
const MEMORY_LOG_INTERVAL_MS = 15000;
|
|
46
|
+
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
47
|
+
|
|
48
|
+
// Arguments parsed in main()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// Global state
|
|
53
|
+
let embedder = null;
|
|
54
|
+
let cache = null;
|
|
55
|
+
let indexer = null;
|
|
56
|
+
let hybridSearch = null;
|
|
57
|
+
let config = null;
|
|
58
|
+
|
|
59
|
+
// Feature registry - ordered by priority (semantic_search first as primary tool)
|
|
60
|
+
const features = [
|
|
61
|
+
{
|
|
62
|
+
module: HybridSearchFeature,
|
|
63
|
+
instance: null,
|
|
64
|
+
handler: HybridSearchFeature.handleToolCall,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
module: IndexCodebaseFeature,
|
|
68
|
+
instance: null,
|
|
69
|
+
handler: IndexCodebaseFeature.handleToolCall,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
module: ClearCacheFeature,
|
|
73
|
+
instance: null,
|
|
74
|
+
handler: ClearCacheFeature.handleToolCall,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
module: FindSimilarCodeFeature,
|
|
78
|
+
instance: null,
|
|
79
|
+
handler: FindSimilarCodeFeature.handleToolCall,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
module: AnnConfigFeature,
|
|
83
|
+
instance: null,
|
|
84
|
+
handler: AnnConfigFeature.handleToolCall,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Initialize application
|
|
89
|
+
async function initialize(workspaceDir) {
|
|
90
|
+
// Load configuration with workspace support
|
|
91
|
+
config = await loadConfig(workspaceDir);
|
|
92
|
+
if (config.enableCache && config.autoCleanStaleCaches !== false) {
|
|
93
|
+
await clearStaleCaches();
|
|
94
|
+
}
|
|
95
|
+
const [pidPath, logPath] = await Promise.all([
|
|
96
|
+
setupPidFile({ pidFileName: PID_FILE_NAME }),
|
|
97
|
+
setupFileLogging(config),
|
|
98
|
+
]);
|
|
99
|
+
if (logPath) {
|
|
100
|
+
console.info(`[Logs] Writing server logs to ${logPath}`);
|
|
101
|
+
console.info(`[Logs] Log viewer: heuristic-mcp --logs --workspace "${config.searchDirectory}"`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Log effective configuration for debugging
|
|
105
|
+
console.info(
|
|
106
|
+
`[Server] Config: workerThreads=${config.workerThreads}, embeddingProcessPerBatch=${config.embeddingProcessPerBatch}`
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (pidPath) {
|
|
110
|
+
console.info(`[Server] PID file: ${pidPath}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Log cache directory logic for debugging
|
|
114
|
+
try {
|
|
115
|
+
const globalCache = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
116
|
+
const localCache = path.join(process.cwd(), '.heuristic-mcp');
|
|
117
|
+
console.info(`[Server] Cache debug: Global=${globalCache}, Local=${localCache}`);
|
|
118
|
+
console.info(`[Server] Process CWD: ${process.cwd()}`);
|
|
119
|
+
} catch (_e) { /* ignore */ }
|
|
120
|
+
|
|
121
|
+
let stopStartupMemory = null;
|
|
122
|
+
if (config.verbose) {
|
|
123
|
+
logMemory('[Server] Memory (startup)');
|
|
124
|
+
stopStartupMemory = startMemoryLogger('[Server] Memory (startup)', MEMORY_LOG_INTERVAL_MS);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Ensure search directory exists
|
|
128
|
+
try {
|
|
129
|
+
await fs.access(config.searchDirectory);
|
|
130
|
+
} catch {
|
|
131
|
+
console.error(`[Server] Error: Search directory "${config.searchDirectory}" does not exist`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Create a transparent lazy-loading embedder closure
|
|
136
|
+
console.info('[Server] Initializing features...');
|
|
137
|
+
let cachedEmbedderPromise = null;
|
|
138
|
+
const lazyEmbedder = async (...args) => {
|
|
139
|
+
if (!cachedEmbedderPromise) {
|
|
140
|
+
console.info(`[Server] Loading AI embedding model: ${config.embeddingModel}...`);
|
|
141
|
+
const modelLoadStart = Date.now();
|
|
142
|
+
cachedEmbedderPromise = pipeline('feature-extraction', config.embeddingModel, {
|
|
143
|
+
quantized: true,
|
|
144
|
+
session_options: {
|
|
145
|
+
numThreads: 2,
|
|
146
|
+
intraOpNumThreads: 2,
|
|
147
|
+
interOpNumThreads: 2,
|
|
148
|
+
},
|
|
149
|
+
}).then((model) => {
|
|
150
|
+
const loadSeconds = ((Date.now() - modelLoadStart) / 1000).toFixed(1);
|
|
151
|
+
console.info(
|
|
152
|
+
`[Server] Embedding model loaded (${loadSeconds}s). Starting intensive indexing (expect high CPU)...`,
|
|
153
|
+
);
|
|
154
|
+
console.info(`[Server] Embedding model ready: ${config.embeddingModel}`);
|
|
155
|
+
if (config.verbose) {
|
|
156
|
+
logMemory('[Server] Memory (after model load)');
|
|
157
|
+
}
|
|
158
|
+
return model;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const model = await cachedEmbedderPromise;
|
|
162
|
+
return model(...args);
|
|
163
|
+
};
|
|
164
|
+
embedder = lazyEmbedder;
|
|
165
|
+
let embedderPreloaded = false;
|
|
166
|
+
|
|
167
|
+
// Preload the embedding model to ensure deterministic startup logs
|
|
168
|
+
if (config.preloadEmbeddingModel !== false) {
|
|
169
|
+
try {
|
|
170
|
+
console.info('[Server] Preloading embedding model...');
|
|
171
|
+
await embedder(' ');
|
|
172
|
+
embedderPreloaded = true;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.warn(`[Server] Embedding model preload failed: ${err.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// In verbose mode, we trigger an early load to provide immediate resource feedback
|
|
179
|
+
if (config.verbose && !embedderPreloaded) {
|
|
180
|
+
embedder('').catch((err) => {
|
|
181
|
+
// Ignore "text may not be null" errors as we are just pre-warming
|
|
182
|
+
if (!err.message.includes('text may not be null')) {
|
|
183
|
+
console.error(`[Server] Warning: Early model load failed: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Initialize cache (load deferred until after server is ready)
|
|
189
|
+
cache = new EmbeddingsCache(config);
|
|
190
|
+
console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
|
|
191
|
+
|
|
192
|
+
// Initialize features
|
|
193
|
+
indexer = new CodebaseIndexer(embedder, cache, config, server);
|
|
194
|
+
hybridSearch = new HybridSearch(embedder, cache, config);
|
|
195
|
+
const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config, indexer);
|
|
196
|
+
const findSimilarCode = new FindSimilarCodeFeature.FindSimilarCode(embedder, cache, config);
|
|
197
|
+
const annConfig = new AnnConfigFeature.AnnConfigTool(cache, config);
|
|
198
|
+
|
|
199
|
+
// Store feature instances (matches features array order)
|
|
200
|
+
features[0].instance = hybridSearch;
|
|
201
|
+
features[1].instance = indexer;
|
|
202
|
+
features[2].instance = cacheClearer;
|
|
203
|
+
features[3].instance = findSimilarCode;
|
|
204
|
+
features[4].instance = annConfig;
|
|
205
|
+
|
|
206
|
+
// Attach hybridSearch to server for cross-feature access (e.g. cache invalidation)
|
|
207
|
+
server.hybridSearch = hybridSearch;
|
|
208
|
+
|
|
209
|
+
const startBackgroundTasks = async () => {
|
|
210
|
+
try {
|
|
211
|
+
console.info('[Server] Loading cache (deferred)...');
|
|
212
|
+
await cache.load();
|
|
213
|
+
if (config.verbose) {
|
|
214
|
+
logMemory('[Server] Memory (after cache load)');
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
if (stopStartupMemory) {
|
|
218
|
+
stopStartupMemory();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Start indexing in background (non-blocking)
|
|
223
|
+
console.info('[Server] Starting background indexing (delayed)...');
|
|
224
|
+
|
|
225
|
+
// Slight delay to allow server to bind and accept first request if immediate
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
indexer
|
|
228
|
+
.indexAll()
|
|
229
|
+
.then(() => {
|
|
230
|
+
// Only start file watcher if explicitly enabled in config
|
|
231
|
+
if (config.watchFiles) {
|
|
232
|
+
indexer.setupFileWatcher();
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
.catch((err) => {
|
|
236
|
+
console.error('[Server] Background indexing error:', err.message);
|
|
237
|
+
});
|
|
238
|
+
}, 3000);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return { startBackgroundTasks, config };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Setup MCP server
|
|
245
|
+
const server = new Server(
|
|
246
|
+
{
|
|
247
|
+
name: 'heuristic-mcp',
|
|
248
|
+
version: packageJson.version,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
capabilities: {
|
|
252
|
+
tools: {},
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Register tools from all features
|
|
258
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
259
|
+
const tools = [];
|
|
260
|
+
|
|
261
|
+
for (const feature of features) {
|
|
262
|
+
const toolDef = feature.module.getToolDefinition(config);
|
|
263
|
+
tools.push(toolDef);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { tools };
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Handle tool calls
|
|
270
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
271
|
+
for (const feature of features) {
|
|
272
|
+
const toolDef = feature.module.getToolDefinition(config);
|
|
273
|
+
|
|
274
|
+
if (request.params.name === toolDef.name) {
|
|
275
|
+
return await feature.handler(request, feature.instance);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: 'text',
|
|
283
|
+
text: `Unknown tool: ${request.params.name}`,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Main entry point
|
|
290
|
+
export async function main(argv = process.argv) {
|
|
291
|
+
const parsed = parseArgs(argv);
|
|
292
|
+
const {
|
|
293
|
+
isServerMode,
|
|
294
|
+
workspaceDir,
|
|
295
|
+
wantsVersion,
|
|
296
|
+
wantsHelp,
|
|
297
|
+
wantsLogs,
|
|
298
|
+
wantsNoFollow,
|
|
299
|
+
tailLines,
|
|
300
|
+
wantsStop,
|
|
301
|
+
wantsStart,
|
|
302
|
+
wantsStatus,
|
|
303
|
+
wantsClearCache,
|
|
304
|
+
wantsRegister,
|
|
305
|
+
registerFilter,
|
|
306
|
+
wantsFix,
|
|
307
|
+
unknownFlags,
|
|
308
|
+
} = parsed;
|
|
309
|
+
|
|
310
|
+
if (isServerMode && !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test')) {
|
|
311
|
+
enableStderrOnlyLogging();
|
|
312
|
+
}
|
|
313
|
+
if (wantsVersion) {
|
|
314
|
+
console.info(packageJson.version);
|
|
315
|
+
process.exit(0);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (wantsHelp) {
|
|
319
|
+
printHelp();
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (workspaceDir) {
|
|
324
|
+
console.info(`[Server] Workspace mode: ${workspaceDir}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (wantsLogs) {
|
|
328
|
+
process.env.SMART_CODING_LOGS = 'true';
|
|
329
|
+
process.env.SMART_CODING_VERBOSE = 'true';
|
|
330
|
+
console.info('[Server] Starting server with verbose logging enabled');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (wantsStop) {
|
|
334
|
+
await stop();
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (wantsStart) {
|
|
339
|
+
await start();
|
|
340
|
+
process.exit(0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (wantsStatus) {
|
|
344
|
+
await status({ fix: wantsFix });
|
|
345
|
+
process.exit(0);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (wantsClearCache) {
|
|
349
|
+
await clearCache(workspaceDir);
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (wantsRegister) {
|
|
354
|
+
await register(registerFilter);
|
|
355
|
+
process.exit(0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (wantsLogs) {
|
|
359
|
+
await logs({
|
|
360
|
+
workspaceDir,
|
|
361
|
+
tailLines,
|
|
362
|
+
follow: !wantsNoFollow,
|
|
363
|
+
});
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (unknownFlags.length > 0) {
|
|
368
|
+
console.error(`[Error] Unknown option(s): ${unknownFlags.join(', ')}`);
|
|
369
|
+
printHelp();
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (wantsFix && !wantsStatus) {
|
|
374
|
+
console.error('[Error] --fix can only be used with --status');
|
|
375
|
+
printHelp();
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
registerSignalHandlers(gracefulShutdown);
|
|
380
|
+
const { startBackgroundTasks } = await initialize(workspaceDir);
|
|
381
|
+
|
|
382
|
+
// (Blocking init moved below)
|
|
383
|
+
|
|
384
|
+
const transport = new StdioServerTransport();
|
|
385
|
+
await server.connect(transport);
|
|
386
|
+
|
|
387
|
+
console.info('[Server] MCP transport connected.');
|
|
388
|
+
console.info('[Server] Heuristic MCP server started.');
|
|
389
|
+
|
|
390
|
+
// Load cache and start indexing in background AFTER server is ready
|
|
391
|
+
startBackgroundTasks().catch(err => {
|
|
392
|
+
console.error(`[Server] Background task error: ${err.message}`);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
console.info('[Server] Heuristic MCP server started.');
|
|
396
|
+
console.info('[Server] MCP server is now fully ready to accept requests.');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Graceful shutdown
|
|
400
|
+
async function gracefulShutdown(signal) {
|
|
401
|
+
console.info(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
402
|
+
|
|
403
|
+
const cleanupTasks = [];
|
|
404
|
+
|
|
405
|
+
// Stop file watcher
|
|
406
|
+
if (indexer && indexer.watcher) {
|
|
407
|
+
cleanupTasks.push(
|
|
408
|
+
indexer.watcher.close()
|
|
409
|
+
.then(() => console.info('[Server] File watcher stopped'))
|
|
410
|
+
.catch(() => console.warn('[Server] Error closing watcher'))
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Give workers time to finish current batch
|
|
415
|
+
if (indexer && indexer.terminateWorkers) {
|
|
416
|
+
cleanupTasks.push(
|
|
417
|
+
(async () => {
|
|
418
|
+
console.info('[Server] Terminating workers...');
|
|
419
|
+
await indexer.terminateWorkers();
|
|
420
|
+
console.info('[Server] Workers terminated');
|
|
421
|
+
})().catch(() => console.info('[Server] Workers shutdown (with warnings)'))
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Save cache
|
|
426
|
+
if (cache) {
|
|
427
|
+
cleanupTasks.push(
|
|
428
|
+
cache.save()
|
|
429
|
+
.then(() => console.info('[Server] Cache saved'))
|
|
430
|
+
.catch((err) => console.error(`[Server] Failed to save cache: ${err.message}`))
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
await Promise.allSettled(cleanupTasks);
|
|
435
|
+
console.info('[Server] Goodbye!');
|
|
436
|
+
|
|
437
|
+
// Allow stdio buffers to flush
|
|
438
|
+
setTimeout(() => process.exit(0), 100);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const isMain = process.argv[1] && (
|
|
442
|
+
path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase() ||
|
|
443
|
+
process.argv[1].endsWith('heuristic-mcp') ||
|
|
444
|
+
process.argv[1].endsWith('heuristic-mcp.js') ||
|
|
445
|
+
path.basename(process.argv[1]) === 'index.js'
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
if (isMain) {
|
|
449
|
+
main().catch(console.error);
|
|
450
|
+
}
|