@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.
Files changed (109) hide show
  1. package/.agent/workflows/code-review.md +60 -0
  2. package/.prettierrc +7 -0
  3. package/ARCHITECTURE.md +105 -170
  4. package/CONTRIBUTING.md +32 -113
  5. package/GEMINI.md +73 -0
  6. package/LICENSE +21 -21
  7. package/README.md +161 -54
  8. package/config.json +876 -75
  9. package/debug-pids.js +27 -0
  10. package/eslint.config.js +36 -0
  11. package/features/ann-config.js +37 -26
  12. package/features/clear-cache.js +28 -19
  13. package/features/find-similar-code.js +142 -66
  14. package/features/hybrid-search.js +253 -93
  15. package/features/index-codebase.js +1455 -394
  16. package/features/lifecycle.js +813 -180
  17. package/features/register.js +58 -52
  18. package/index.js +450 -306
  19. package/lib/cache-ops.js +22 -0
  20. package/lib/cache-utils.js +68 -0
  21. package/lib/cache.js +1392 -587
  22. package/lib/call-graph.js +165 -50
  23. package/lib/cli.js +154 -0
  24. package/lib/config.js +462 -121
  25. package/lib/embedding-process.js +77 -0
  26. package/lib/embedding-worker.js +545 -30
  27. package/lib/ignore-patterns.js +61 -59
  28. package/lib/json-worker.js +14 -0
  29. package/lib/json-writer.js +344 -0
  30. package/lib/logging.js +88 -0
  31. package/lib/memory-logger.js +13 -0
  32. package/lib/project-detector.js +13 -17
  33. package/lib/server-lifecycle.js +38 -0
  34. package/lib/settings-editor.js +645 -0
  35. package/lib/tokenizer.js +207 -104
  36. package/lib/utils.js +273 -198
  37. package/lib/vector-store-binary.js +592 -0
  38. package/mcp_config.example.json +13 -0
  39. package/package.json +13 -2
  40. package/scripts/clear-cache.js +6 -17
  41. package/scripts/download-model.js +14 -9
  42. package/scripts/postinstall.js +5 -5
  43. package/search-configs.js +36 -0
  44. package/test/ann-config.test.js +179 -0
  45. package/test/ann-fallback.test.js +6 -6
  46. package/test/binary-store.test.js +69 -0
  47. package/test/cache-branches.test.js +120 -0
  48. package/test/cache-errors.test.js +264 -0
  49. package/test/cache-extra.test.js +300 -0
  50. package/test/cache-helpers.test.js +205 -0
  51. package/test/cache-hnsw-failure.test.js +40 -0
  52. package/test/cache-json-worker.test.js +190 -0
  53. package/test/cache-worker.test.js +102 -0
  54. package/test/cache.test.js +443 -0
  55. package/test/call-graph.test.js +103 -4
  56. package/test/clear-cache.test.js +69 -68
  57. package/test/code-review-workflow.test.js +50 -0
  58. package/test/config.test.js +418 -0
  59. package/test/coverage-gap.test.js +497 -0
  60. package/test/coverage-maximizer.test.js +236 -0
  61. package/test/debug-analysis.js +107 -0
  62. package/test/embedding-model.test.js +173 -103
  63. package/test/embedding-worker-extra.test.js +272 -0
  64. package/test/embedding-worker.test.js +158 -0
  65. package/test/features.test.js +139 -0
  66. package/test/final-boost.test.js +271 -0
  67. package/test/final-polish.test.js +183 -0
  68. package/test/final.test.js +95 -0
  69. package/test/find-similar-code.test.js +191 -0
  70. package/test/helpers.js +92 -11
  71. package/test/helpers.test.js +46 -0
  72. package/test/hybrid-search-basic.test.js +62 -0
  73. package/test/hybrid-search-branch.test.js +202 -0
  74. package/test/hybrid-search-callgraph.test.js +229 -0
  75. package/test/hybrid-search-extra.test.js +81 -0
  76. package/test/hybrid-search.test.js +484 -71
  77. package/test/index-cli.test.js +520 -0
  78. package/test/index-codebase-batch.test.js +119 -0
  79. package/test/index-codebase-branches.test.js +585 -0
  80. package/test/index-codebase-core.test.js +1032 -0
  81. package/test/index-codebase-edge-cases.test.js +254 -0
  82. package/test/index-codebase-errors.test.js +132 -0
  83. package/test/index-codebase-gap.test.js +239 -0
  84. package/test/index-codebase-lines.test.js +151 -0
  85. package/test/index-codebase-watcher.test.js +259 -0
  86. package/test/index-codebase-zone.test.js +259 -0
  87. package/test/index-codebase.test.js +371 -69
  88. package/test/index-memory.test.js +220 -0
  89. package/test/indexer-detailed.test.js +176 -0
  90. package/test/integration.test.js +148 -92
  91. package/test/json-worker.test.js +50 -0
  92. package/test/lifecycle.test.js +541 -0
  93. package/test/master.test.js +198 -0
  94. package/test/perfection.test.js +349 -0
  95. package/test/project-detector.test.js +65 -0
  96. package/test/register.test.js +262 -0
  97. package/test/tokenizer.test.js +55 -93
  98. package/test/ultra-maximizer.test.js +116 -0
  99. package/test/utils-branches.test.js +161 -0
  100. package/test/utils-extra.test.js +116 -0
  101. package/test/utils.test.js +131 -0
  102. package/test/verify_fixes.js +76 -0
  103. package/test/worker-errors.test.js +96 -0
  104. package/test/worker-init.test.js +102 -0
  105. package/test/worker_throttling.test.js +93 -0
  106. package/tools/scripts/benchmark-search.js +95 -0
  107. package/tools/scripts/cache-stats.js +71 -0
  108. package/tools/scripts/manual-search.js +34 -0
  109. package/vitest.config.js +19 -9
package/index.js CHANGED
@@ -1,306 +1,450 @@
1
- #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { stop, start, status } 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 } from "@xenova/transformers";
7
- import fs from "fs/promises";
8
- import path from "path";
9
-
10
- import { createRequire } from "module";
11
-
12
- // Import package.json for version
13
- const require = createRequire(import.meta.url);
14
- const packageJson = require("./package.json");
15
-
16
- import { loadConfig, getGlobalCacheDir } from "./lib/config.js";
17
-
18
- import { EmbeddingsCache } from "./lib/cache.js";
19
- import { CodebaseIndexer } from "./features/index-codebase.js";
20
- import { HybridSearch } from "./features/hybrid-search.js";
21
-
22
- import * as IndexCodebaseFeature from "./features/index-codebase.js";
23
- import * as HybridSearchFeature from "./features/hybrid-search.js";
24
- import * as ClearCacheFeature from "./features/clear-cache.js";
25
- import * as FindSimilarCodeFeature from "./features/find-similar-code.js";
26
- import * as AnnConfigFeature from "./features/ann-config.js";
27
- import { register } from "./features/register.js";
28
-
29
- // Log cache directory logic for debugging
30
- try {
31
- const globalCache = path.join(getGlobalCacheDir(), 'heuristic-mcp');
32
- const localCache = path.join(process.cwd(), '.heuristic-mcp');
33
- console.error(`[Server] Cache debug: Global=${globalCache}, Local=${localCache}`);
34
- console.error(`[Server] Process CWD: ${process.cwd()}`);
35
- } catch (e) {}
36
-
37
-
38
- // Parse workspace from command line arguments
39
- let args = process.argv.slice(2);
40
- const hadLogs = args.includes('--logs');
41
- if (hadLogs) {
42
- process.env.SMART_CODING_VERBOSE = 'true';
43
- args = args.filter(arg => arg !== '--logs');
44
- console.log('[Logs] Starting server with verbose console output (Ctrl+C to stop)...');
45
- }
46
-
47
- if (args.includes('--stop')) {
48
- await stop();
49
- process.exit(0);
50
- }
51
-
52
- if (args.includes('--start')) {
53
- await start();
54
- process.exit(0);
55
- }
56
-
57
- if (args.includes('--status')) {
58
- await status();
59
- process.exit(0);
60
- }
61
-
62
-
63
- // Check if --register flag is present
64
- if (args.includes('--register')) {
65
- // Extract optional filter (e.g. --register antigravity)
66
- const filterIndex = args.indexOf('--register');
67
- const filter = args[filterIndex + 1] && !args[filterIndex + 1].startsWith('-')
68
- ? args[filterIndex + 1]
69
- : null;
70
-
71
- await register(filter);
72
- process.exit(0);
73
- }
74
-
75
- const workspaceIndex = args.findIndex(arg => arg.startsWith('--workspace'));
76
- let workspaceDir = null;
77
-
78
- if (workspaceIndex !== -1) {
79
- const arg = args[workspaceIndex];
80
- let rawWorkspace = null;
81
-
82
- if (arg.includes('=')) {
83
- rawWorkspace = arg.split('=')[1];
84
- } else if (workspaceIndex + 1 < args.length) {
85
- rawWorkspace = args[workspaceIndex + 1];
86
- }
87
-
88
- // Check if IDE variable wasn't expanded (contains ${})
89
- if (rawWorkspace && rawWorkspace.includes('${')) {
90
- console.error(`[Server] IDE variable not expanded: ${rawWorkspace}, using current directory`);
91
- workspaceDir = process.cwd();
92
- } else if (rawWorkspace) {
93
- workspaceDir = rawWorkspace;
94
- }
95
-
96
- if (workspaceDir) {
97
- console.error(`[Server] Workspace mode: ${workspaceDir}`);
98
- }
99
- }
100
-
101
- // Global state
102
- let embedder = null;
103
- let cache = null;
104
- let indexer = null;
105
- let hybridSearch = null;
106
- let config = null;
107
-
108
- // Feature registry - ordered by priority (semantic_search first as primary tool)
109
- const features = [
110
- {
111
- module: HybridSearchFeature,
112
- instance: null,
113
- handler: HybridSearchFeature.handleToolCall
114
- },
115
- {
116
- module: IndexCodebaseFeature,
117
- instance: null,
118
- handler: IndexCodebaseFeature.handleToolCall
119
- },
120
- {
121
- module: ClearCacheFeature,
122
- instance: null,
123
- handler: ClearCacheFeature.handleToolCall
124
- },
125
- {
126
- module: FindSimilarCodeFeature,
127
- instance: null,
128
- handler: FindSimilarCodeFeature.handleToolCall
129
- },
130
- {
131
- module: AnnConfigFeature,
132
- instance: null,
133
- handler: AnnConfigFeature.handleToolCall
134
- }
135
- ];
136
-
137
- // Initialize application
138
- async function initialize() {
139
- // Load configuration with workspace support
140
- config = await loadConfig(workspaceDir);
141
-
142
- // Ensure search directory exists
143
- try {
144
- await fs.access(config.searchDirectory);
145
- } catch {
146
- console.error(`[Server] Error: Search directory "${config.searchDirectory}" does not exist`);
147
- process.exit(1);
148
- }
149
-
150
- // Load AI model
151
- console.error("[Server] Loading AI embedding model (this may take time on first run)...");
152
- embedder = await pipeline("feature-extraction", config.embeddingModel);
153
-
154
- // Initialize cache
155
- cache = new EmbeddingsCache(config);
156
- console.error(`[Server] Cache directory: ${config.cacheDirectory}`);
157
- await cache.load();
158
-
159
- // Initialize features
160
- indexer = new CodebaseIndexer(embedder, cache, config, server);
161
- hybridSearch = new HybridSearch(embedder, cache, config);
162
- const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config, indexer);
163
- const findSimilarCode = new FindSimilarCodeFeature.FindSimilarCode(embedder, cache, config);
164
- const annConfig = new AnnConfigFeature.AnnConfigTool(cache, config);
165
-
166
- // Store feature instances (matches features array order)
167
- features[0].instance = hybridSearch;
168
- features[1].instance = indexer;
169
- features[2].instance = cacheClearer;
170
- features[3].instance = findSimilarCode;
171
- features[4].instance = annConfig;
172
-
173
- // Attach hybridSearch to server for cross-feature access (e.g. cache invalidation)
174
- server.hybridSearch = hybridSearch;
175
-
176
- // Start indexing in background (non-blocking)
177
- console.error("[Server] Starting background indexing...");
178
- indexer.indexAll().then(() => {
179
- // Only start file watcher if explicitly enabled in config
180
- if (config.watchFiles) {
181
- indexer.setupFileWatcher();
182
- }
183
- }).catch(err => {
184
- console.error("[Server] Background indexing error:", err.message);
185
- });
186
- }
187
-
188
- // Setup MCP server
189
- const server = new Server(
190
- {
191
- name: "heuristic-mcp",
192
- version: packageJson.version
193
- },
194
- {
195
- capabilities: {
196
- tools: {}
197
- }
198
- }
199
- );
200
-
201
- // Register tools from all features
202
- server.setRequestHandler(ListToolsRequestSchema, async () => {
203
- const tools = [];
204
-
205
- for (const feature of features) {
206
- const toolDef = feature.module.getToolDefinition(config);
207
- tools.push(toolDef);
208
- }
209
-
210
- return { tools };
211
- });
212
-
213
- // Handle tool calls
214
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
215
- for (const feature of features) {
216
- const toolDef = feature.module.getToolDefinition(config);
217
-
218
- if (request.params.name === toolDef.name) {
219
- return await feature.handler(request, feature.instance);
220
- }
221
- }
222
-
223
- return {
224
- content: [{
225
- type: "text",
226
- text: `Unknown tool: ${request.params.name}`
227
- }]
228
- };
229
- });
230
-
231
- // Main entry point
232
- async function main() {
233
- await initialize();
234
-
235
- const transport = new StdioServerTransport();
236
- await server.connect(transport);
237
-
238
- console.error("[Server] Heuristic MCP server ready!");
239
- }
240
-
241
- // Graceful shutdown
242
- process.on('SIGINT', async () => {
243
- console.error("\n[Server] Shutting down gracefully...");
244
-
245
- // Stop file watcher
246
- if (indexer && indexer.watcher) {
247
- await indexer.watcher.close();
248
- console.error("[Server] File watcher stopped");
249
- }
250
-
251
- // Give workers time to finish current batch (prevents core dump)
252
- if (indexer && indexer.terminateWorkers) {
253
- try {
254
- console.error("[Server] Waiting for workers to finish...");
255
- await new Promise(resolve => setTimeout(resolve, 500));
256
- await indexer.terminateWorkers();
257
- console.error("[Server] Workers terminated");
258
- } catch (err) {
259
- // Suppress native module errors during shutdown
260
- console.error("[Server] Workers shutdown (with warnings)");
261
- }
262
- }
263
-
264
- // Save cache
265
- if (cache) {
266
- await cache.save();
267
- console.error("[Server] Cache saved");
268
- }
269
-
270
- console.error("[Server] Goodbye!");
271
- process.exit(0);
272
- });
273
-
274
- process.on('SIGTERM', async () => {
275
- console.error("\n[Server] Received SIGTERM, shutting down...");
276
-
277
- // Stop file watcher
278
- if (indexer && indexer.watcher) {
279
- await indexer.watcher.close();
280
- console.error("[Server] File watcher stopped");
281
- }
282
-
283
- // Give workers time to finish current batch (prevents core dump)
284
- if (indexer && indexer.terminateWorkers) {
285
- try {
286
- console.error("[Server] Waiting for workers to finish...");
287
- await new Promise(resolve => setTimeout(resolve, 500));
288
- await indexer.terminateWorkers();
289
- console.error("[Server] Workers terminated");
290
- } catch (err) {
291
- // Suppress native module errors during shutdown
292
- console.error("[Server] Workers shutdown (with warnings)");
293
- }
294
- }
295
-
296
- // Save cache
297
- if (cache) {
298
- await cache.save();
299
- console.error("[Server] Cache saved");
300
- }
301
-
302
- console.error("[Server] Goodbye!");
303
- process.exit(0);
304
- });
305
-
306
- main().catch(console.error);
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
+ }