@softerist/heuristic-mcp 3.0.17 → 3.1.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/config.jsonc +23 -6
- package/features/ann-config.js +7 -14
- package/features/clear-cache.js +3 -3
- package/features/find-similar-code.js +17 -22
- package/features/hybrid-search.js +59 -67
- package/features/index-codebase.js +305 -268
- package/features/lifecycle.js +370 -176
- package/features/package-version.js +15 -26
- package/features/register.js +75 -57
- package/features/resources.js +21 -47
- package/features/set-workspace.js +31 -43
- package/index.js +818 -172
- package/lib/cache-utils.js +95 -99
- package/lib/cache.js +121 -166
- package/lib/cli.js +246 -238
- package/lib/config.js +232 -62
- package/lib/constants.js +22 -2
- package/lib/embed-query-process.js +13 -29
- package/lib/embedding-process.js +29 -19
- package/lib/embedding-worker.js +166 -149
- package/lib/ignore-patterns.js +39 -39
- package/lib/json-writer.js +7 -34
- package/lib/logging.js +11 -42
- package/lib/onnx-backend.js +4 -4
- package/lib/path-utils.js +4 -21
- package/lib/project-detector.js +3 -3
- package/lib/server-lifecycle.js +109 -15
- package/lib/settings-editor.js +25 -18
- package/lib/slice-normalize.js +6 -16
- package/lib/tokenizer.js +56 -109
- package/lib/utils.js +62 -81
- package/lib/vector-store-binary.js +7 -7
- package/lib/vector-store-sqlite.js +35 -67
- package/lib/workspace-cache-key.js +36 -0
- package/lib/workspace-env.js +55 -14
- package/package.json +86 -86
package/index.js
CHANGED
|
@@ -7,11 +7,18 @@ import {
|
|
|
7
7
|
ListToolsRequestSchema,
|
|
8
8
|
ListResourcesRequestSchema,
|
|
9
9
|
ReadResourceRequestSchema,
|
|
10
|
+
RootsListChangedNotificationSchema,
|
|
10
11
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
12
|
let transformersModule = null;
|
|
12
13
|
async function getTransformers() {
|
|
13
14
|
if (!transformersModule) {
|
|
14
15
|
transformersModule = await import('@huggingface/transformers');
|
|
16
|
+
if (transformersModule?.env) {
|
|
17
|
+
transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
|
|
18
|
+
}
|
|
19
|
+
if (transformersModule?.env) {
|
|
20
|
+
transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
23
|
return transformersModule;
|
|
17
24
|
}
|
|
@@ -22,23 +29,25 @@ import path from 'path';
|
|
|
22
29
|
import os from 'os';
|
|
23
30
|
|
|
24
31
|
import { createRequire } from 'module';
|
|
25
|
-
import { fileURLToPath } from 'url';
|
|
32
|
+
import { fileURLToPath } from 'url';
|
|
33
|
+
import { getWorkspaceCachePath } from './lib/workspace-cache-key.js';
|
|
34
|
+
|
|
26
35
|
|
|
27
|
-
// Import package.json for version
|
|
28
36
|
const require = createRequire(import.meta.url);
|
|
29
37
|
const packageJson = require('./package.json');
|
|
30
38
|
|
|
31
|
-
import { loadConfig, getGlobalCacheDir } from './lib/config.js';
|
|
39
|
+
import { loadConfig, getGlobalCacheDir, isNonProjectDirectory } from './lib/config.js';
|
|
32
40
|
import { clearStaleCaches } from './lib/cache-utils.js';
|
|
33
41
|
import { enableStderrOnlyLogging, setupFileLogging, getLogFilePath } from './lib/logging.js';
|
|
34
42
|
import { parseArgs, printHelp } from './lib/cli.js';
|
|
35
43
|
import { clearCache } from './lib/cache-ops.js';
|
|
36
44
|
import { logMemory, startMemoryLogger } from './lib/memory-logger.js';
|
|
37
|
-
import {
|
|
38
|
-
registerSignalHandlers,
|
|
39
|
-
setupPidFile,
|
|
40
|
-
acquireWorkspaceLock,
|
|
41
|
-
|
|
45
|
+
import {
|
|
46
|
+
registerSignalHandlers,
|
|
47
|
+
setupPidFile,
|
|
48
|
+
acquireWorkspaceLock,
|
|
49
|
+
stopOtherHeuristicServers,
|
|
50
|
+
} from './lib/server-lifecycle.js';
|
|
42
51
|
|
|
43
52
|
import { EmbeddingsCache } from './lib/cache.js';
|
|
44
53
|
import { CodebaseIndexer } from './features/index-codebase.js';
|
|
@@ -116,17 +125,62 @@ async function printMemorySnapshot(workspaceDir) {
|
|
|
116
125
|
return true;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
// Arguments parsed in main()
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
let
|
|
124
|
-
let
|
|
125
|
-
let
|
|
126
|
-
let
|
|
127
|
-
let
|
|
128
|
-
let
|
|
129
|
-
let
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
let embedder = null;
|
|
132
|
+
let unloadMainEmbedder = null;
|
|
133
|
+
let cache = null;
|
|
134
|
+
let indexer = null;
|
|
135
|
+
let hybridSearch = null;
|
|
136
|
+
let config = null;
|
|
137
|
+
let workspaceLockAcquired = true;
|
|
138
|
+
let configReadyResolve = null;
|
|
139
|
+
let configInitError = null;
|
|
140
|
+
let configReadyPromise = new Promise((resolve) => {
|
|
141
|
+
configReadyResolve = resolve;
|
|
142
|
+
});
|
|
143
|
+
let setWorkspaceFeatureInstance = null;
|
|
144
|
+
let autoWorkspaceSwitchPromise = null;
|
|
145
|
+
let rootsCapabilitySupported = null;
|
|
146
|
+
let rootsProbeInFlight = null;
|
|
147
|
+
const WORKSPACE_BOUND_TOOL_NAMES = new Set([
|
|
148
|
+
'a_semantic_search',
|
|
149
|
+
'b_index_codebase',
|
|
150
|
+
'c_clear_cache',
|
|
151
|
+
'd_find_similar_code',
|
|
152
|
+
'd_ann_config',
|
|
153
|
+
]);
|
|
154
|
+
const trustedWorkspacePaths = new Set();
|
|
155
|
+
|
|
156
|
+
function shouldRequireTrustedWorkspaceSignalForTool(toolName) {
|
|
157
|
+
return WORKSPACE_BOUND_TOOL_NAMES.has(toolName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function trustWorkspacePath(workspacePath) {
|
|
161
|
+
const normalized = normalizePathForCompare(workspacePath);
|
|
162
|
+
if (normalized) {
|
|
163
|
+
trustedWorkspacePaths.add(normalized);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isCurrentWorkspaceTrusted() {
|
|
168
|
+
if (!config?.searchDirectory) return false;
|
|
169
|
+
return trustedWorkspacePaths.has(normalizePathForCompare(config.searchDirectory));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isToolResponseError(result) {
|
|
173
|
+
if (!result || typeof result !== 'object') return true;
|
|
174
|
+
if (result.isError === true) return true;
|
|
175
|
+
if (!Array.isArray(result.content)) return false;
|
|
176
|
+
|
|
177
|
+
return result.content.some(
|
|
178
|
+
(entry) =>
|
|
179
|
+
entry?.type === 'text' &&
|
|
180
|
+
typeof entry.text === 'string' &&
|
|
181
|
+
entry.text.trim().toLowerCase().startsWith('error:')
|
|
182
|
+
);
|
|
183
|
+
}
|
|
130
184
|
|
|
131
185
|
async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
132
186
|
if (!rawValue || rawValue.includes('${')) return null;
|
|
@@ -140,56 +194,272 @@ async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
|
140
194
|
}
|
|
141
195
|
}
|
|
142
196
|
|
|
143
|
-
async function detectRuntimeWorkspaceFromEnv() {
|
|
144
|
-
for (const key of getWorkspaceEnvKeys()) {
|
|
145
|
-
const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
|
|
197
|
+
async function detectRuntimeWorkspaceFromEnv() {
|
|
198
|
+
for (const key of getWorkspaceEnvKeys()) {
|
|
199
|
+
const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
|
|
146
200
|
if (workspacePath) {
|
|
147
201
|
return { workspacePath, envKey: key };
|
|
148
202
|
}
|
|
149
203
|
}
|
|
150
204
|
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizePathForCompare(targetPath) {
|
|
209
|
+
if (!targetPath) return '';
|
|
210
|
+
const resolved = path.resolve(targetPath);
|
|
211
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isProcessAlive(pid) {
|
|
215
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
216
|
+
try {
|
|
217
|
+
process.kill(pid, 0);
|
|
218
|
+
return true;
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return err?.code === 'EPERM';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function findAutoAttachWorkspaceCandidate({ excludeCacheDirectory = null } = {}) {
|
|
225
|
+
const cacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
226
|
+
const normalizedExclude = normalizePathForCompare(excludeCacheDirectory);
|
|
227
|
+
|
|
228
|
+
let cacheDirs = [];
|
|
229
|
+
try {
|
|
230
|
+
cacheDirs = await fs.readdir(cacheRoot, { withFileTypes: true });
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const candidatesByWorkspace = new Map();
|
|
236
|
+
const preferredWorkspaceFromEnv = (await detectRuntimeWorkspaceFromEnv())?.workspacePath ?? null;
|
|
237
|
+
const normalizedPreferred = normalizePathForCompare(preferredWorkspaceFromEnv);
|
|
238
|
+
|
|
239
|
+
const upsertCandidate = (candidate) => {
|
|
240
|
+
const key = normalizePathForCompare(candidate.workspace);
|
|
241
|
+
const existing = candidatesByWorkspace.get(key);
|
|
242
|
+
if (!existing || candidate.rank > existing.rank) {
|
|
243
|
+
candidatesByWorkspace.set(key, candidate);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (const entry of cacheDirs) {
|
|
248
|
+
if (!entry.isDirectory()) continue;
|
|
249
|
+
const cacheDirectory = path.join(cacheRoot, entry.name);
|
|
250
|
+
if (normalizedExclude && normalizePathForCompare(cacheDirectory) === normalizedExclude) continue;
|
|
251
|
+
|
|
252
|
+
const lockPath = path.join(cacheDirectory, 'server.lock.json');
|
|
253
|
+
try {
|
|
254
|
+
const rawLock = await fs.readFile(lockPath, 'utf-8');
|
|
255
|
+
const lock = JSON.parse(rawLock);
|
|
256
|
+
if (!isProcessAlive(lock?.pid)) continue;
|
|
257
|
+
const workspace = path.resolve(lock?.workspace || '');
|
|
258
|
+
if (!workspace || isNonProjectDirectory(workspace)) continue;
|
|
259
|
+
const stats = await fs.stat(workspace).catch(() => null);
|
|
260
|
+
if (!stats?.isDirectory()) continue;
|
|
261
|
+
const rank = Date.parse(lock?.startedAt || '') || 0;
|
|
262
|
+
upsertCandidate({
|
|
263
|
+
workspace,
|
|
264
|
+
cacheDirectory,
|
|
265
|
+
source: `lock:${lock.pid}`,
|
|
266
|
+
rank,
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
} catch {
|
|
270
|
+
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const metaPath = path.join(cacheDirectory, 'meta.json');
|
|
274
|
+
try {
|
|
275
|
+
const rawMeta = await fs.readFile(metaPath, 'utf-8');
|
|
276
|
+
const meta = JSON.parse(rawMeta);
|
|
277
|
+
const workspace = path.resolve(meta?.workspace || '');
|
|
278
|
+
if (!workspace || isNonProjectDirectory(workspace)) continue;
|
|
279
|
+
const stats = await fs.stat(workspace).catch(() => null);
|
|
280
|
+
if (!stats?.isDirectory()) continue;
|
|
281
|
+
const filesIndexed = Number(meta?.filesIndexed || 0);
|
|
282
|
+
if (filesIndexed <= 0) continue;
|
|
283
|
+
const rank = Date.parse(meta?.lastSaveTime || '') || 0;
|
|
284
|
+
upsertCandidate({
|
|
285
|
+
workspace,
|
|
286
|
+
cacheDirectory,
|
|
287
|
+
source: 'meta',
|
|
288
|
+
rank,
|
|
289
|
+
});
|
|
290
|
+
} catch {
|
|
291
|
+
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const candidates = Array.from(candidatesByWorkspace.values());
|
|
296
|
+
if (candidates.length === 0) return null;
|
|
297
|
+
if (normalizedPreferred) {
|
|
298
|
+
const preferred = candidates.find(
|
|
299
|
+
(candidate) => normalizePathForCompare(candidate.workspace) === normalizedPreferred
|
|
300
|
+
);
|
|
301
|
+
if (preferred) return preferred;
|
|
302
|
+
}
|
|
303
|
+
if (candidates.length === 1) return candidates[0];
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function maybeAutoSwitchWorkspace(request) {
|
|
308
|
+
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return null;
|
|
309
|
+
if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return null;
|
|
310
|
+
if (request?.params?.name === 'f_set_workspace') return null;
|
|
311
|
+
|
|
312
|
+
const detected = await detectRuntimeWorkspaceFromEnv();
|
|
313
|
+
if (!detected) return null;
|
|
314
|
+
if (isNonProjectDirectory(detected.workspacePath)) {
|
|
315
|
+
console.info(
|
|
316
|
+
`[Server] Ignoring auto-switch candidate from env ${detected.envKey}: non-project path ${detected.workspacePath}`
|
|
317
|
+
);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const currentWorkspace = normalizePathForCompare(config.searchDirectory);
|
|
322
|
+
const detectedWorkspace = normalizePathForCompare(detected.workspacePath);
|
|
323
|
+
if (detectedWorkspace === currentWorkspace) return detected.workspacePath;
|
|
324
|
+
|
|
325
|
+
await maybeAutoSwitchWorkspaceToPath(detected.workspacePath, {
|
|
326
|
+
source: `env ${detected.envKey}`,
|
|
327
|
+
reindex: false,
|
|
328
|
+
});
|
|
329
|
+
return detected.workspacePath;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async function detectWorkspaceFromRoots({ quiet = false } = {}) {
|
|
335
|
+
try {
|
|
336
|
+
const caps = server.getClientCapabilities();
|
|
337
|
+
if (!caps?.roots) {
|
|
338
|
+
rootsCapabilitySupported = false;
|
|
339
|
+
if (!quiet) {
|
|
340
|
+
console.info(
|
|
341
|
+
'[Server] Client does not support roots capability, skipping workspace auto-detection.'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
rootsCapabilitySupported = true;
|
|
347
|
+
|
|
348
|
+
const result = await server.listRoots();
|
|
349
|
+
if (!result?.roots?.length) {
|
|
350
|
+
if (!quiet) {
|
|
351
|
+
console.info('[Server] Client returned no roots.');
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!quiet) {
|
|
357
|
+
console.info(`[Server] MCP roots received: ${result.roots.map(r => r.uri).join(', ')}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
const rootPaths = result.roots
|
|
362
|
+
.map(r => r.uri)
|
|
363
|
+
.filter(uri => uri.startsWith('file://'))
|
|
364
|
+
.map(uri => {
|
|
365
|
+
try {
|
|
366
|
+
return fileURLToPath(uri);
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
.filter(Boolean);
|
|
372
|
+
|
|
373
|
+
if (rootPaths.length === 0) {
|
|
374
|
+
if (!quiet) {
|
|
375
|
+
console.info('[Server] No valid file:// roots found.');
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return path.resolve(rootPaths[0]);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
if (!quiet) {
|
|
383
|
+
console.warn(`[Server] MCP roots detection failed (non-fatal): ${err.message}`);
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function maybeAutoSwitchWorkspaceToPath(targetWorkspacePath, { source, reindex = false } = {}) {
|
|
390
|
+
if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return;
|
|
391
|
+
if (!targetWorkspacePath) return;
|
|
392
|
+
if (isNonProjectDirectory(targetWorkspacePath)) {
|
|
393
|
+
if (config?.verbose) {
|
|
394
|
+
console.info(
|
|
395
|
+
`[Server] Ignoring auto-switch candidate from ${source || 'unknown'}: non-project path ${targetWorkspacePath}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const currentWorkspace = normalizePathForCompare(config.searchDirectory);
|
|
402
|
+
const targetWorkspace = normalizePathForCompare(targetWorkspacePath);
|
|
403
|
+
if (targetWorkspace === currentWorkspace) return;
|
|
404
|
+
|
|
405
|
+
if (autoWorkspaceSwitchPromise) {
|
|
406
|
+
await autoWorkspaceSwitchPromise;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
autoWorkspaceSwitchPromise = (async () => {
|
|
411
|
+
console.info(
|
|
412
|
+
`[Server] Auto-switching workspace from ${currentWorkspace} to ${targetWorkspacePath} (${source || 'auto'})`
|
|
413
|
+
);
|
|
414
|
+
const result = await setWorkspaceFeatureInstance.execute({
|
|
415
|
+
workspacePath: targetWorkspacePath,
|
|
416
|
+
reindex,
|
|
417
|
+
});
|
|
418
|
+
if (!result.success) {
|
|
419
|
+
console.warn(
|
|
420
|
+
`[Server] Auto workspace switch failed (${source || 'auto'}): ${result.error}`
|
|
421
|
+
);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
trustWorkspacePath(targetWorkspacePath);
|
|
425
|
+
})();
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
await autoWorkspaceSwitchPromise;
|
|
429
|
+
} finally {
|
|
430
|
+
autoWorkspaceSwitchPromise = null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function maybeAutoSwitchWorkspaceFromRoots(request) {
|
|
435
|
+
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return null;
|
|
436
|
+
if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return null;
|
|
437
|
+
if (request?.params?.name === 'f_set_workspace') return null;
|
|
438
|
+
if (rootsCapabilitySupported === false) return null;
|
|
439
|
+
|
|
440
|
+
if (rootsProbeInFlight) {
|
|
441
|
+
return await rootsProbeInFlight;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
rootsProbeInFlight = (async () => {
|
|
445
|
+
const rootWorkspace = await detectWorkspaceFromRoots({ quiet: true });
|
|
446
|
+
if (!rootWorkspace) return null;
|
|
447
|
+
await maybeAutoSwitchWorkspaceToPath(rootWorkspace, {
|
|
448
|
+
source: 'roots probe',
|
|
449
|
+
reindex: false,
|
|
450
|
+
});
|
|
451
|
+
return rootWorkspace;
|
|
452
|
+
})();
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
return await rootsProbeInFlight;
|
|
456
|
+
} finally {
|
|
457
|
+
rootsProbeInFlight = null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
158
460
|
|
|
159
|
-
const detected = await detectRuntimeWorkspaceFromEnv();
|
|
160
|
-
if (!detected) return;
|
|
161
|
-
|
|
162
|
-
const currentWorkspace = path.resolve(config.searchDirectory);
|
|
163
|
-
if (detected.workspacePath === currentWorkspace) return;
|
|
164
|
-
|
|
165
|
-
if (autoWorkspaceSwitchPromise) {
|
|
166
|
-
await autoWorkspaceSwitchPromise;
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
autoWorkspaceSwitchPromise = (async () => {
|
|
171
|
-
console.info(
|
|
172
|
-
`[Server] Auto-switching workspace from ${currentWorkspace} to ${detected.workspacePath} (env ${detected.envKey})`
|
|
173
|
-
);
|
|
174
|
-
const result = await setWorkspaceFeatureInstance.execute({
|
|
175
|
-
workspacePath: detected.workspacePath,
|
|
176
|
-
reindex: false,
|
|
177
|
-
});
|
|
178
|
-
if (!result.success) {
|
|
179
|
-
console.warn(
|
|
180
|
-
`[Server] Auto workspace switch failed (env ${detected.envKey}): ${result.error}`
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
})();
|
|
184
461
|
|
|
185
|
-
try {
|
|
186
|
-
await autoWorkspaceSwitchPromise;
|
|
187
|
-
} finally {
|
|
188
|
-
autoWorkspaceSwitchPromise = null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
462
|
|
|
192
|
-
// Feature registry - ordered by priority (semantic_search first as primary tool)
|
|
193
463
|
const features = [
|
|
194
464
|
{
|
|
195
465
|
module: HybridSearchFeature,
|
|
@@ -221,19 +491,22 @@ const features = [
|
|
|
221
491
|
instance: null,
|
|
222
492
|
handler: PackageVersionFeature.handleToolCall,
|
|
223
493
|
},
|
|
224
|
-
{
|
|
225
|
-
module: SetWorkspaceFeature,
|
|
226
|
-
instance: null,
|
|
227
|
-
handler: null,
|
|
228
|
-
},
|
|
229
|
-
];
|
|
494
|
+
{
|
|
495
|
+
module: SetWorkspaceFeature,
|
|
496
|
+
instance: null,
|
|
497
|
+
handler: null,
|
|
498
|
+
},
|
|
499
|
+
];
|
|
230
500
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
async function initialize(workspaceDir) {
|
|
504
|
+
|
|
505
|
+
|
|
234
506
|
config = await loadConfig(workspaceDir);
|
|
235
507
|
|
|
236
|
-
|
|
508
|
+
|
|
509
|
+
|
|
237
510
|
if (config.enableCache && config.cacheCleanup?.autoCleanup) {
|
|
238
511
|
console.info('[Server] Running automatic cache cleanup...');
|
|
239
512
|
const results = await clearStaleCaches({
|
|
@@ -245,7 +518,8 @@ async function initialize(workspaceDir) {
|
|
|
245
518
|
}
|
|
246
519
|
}
|
|
247
520
|
|
|
248
|
-
|
|
521
|
+
|
|
522
|
+
|
|
249
523
|
const isTest = Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
|
|
250
524
|
if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
|
|
251
525
|
console.warn(
|
|
@@ -280,7 +554,8 @@ async function initialize(workspaceDir) {
|
|
|
280
554
|
env.backends.onnx.wasm.numThreads = ONNX_THREAD_LIMIT;
|
|
281
555
|
}
|
|
282
556
|
} catch {
|
|
283
|
-
|
|
557
|
+
|
|
558
|
+
|
|
284
559
|
}
|
|
285
560
|
const status = getNativeOnnxStatus();
|
|
286
561
|
const reason = status?.message || 'onnxruntime-node not available';
|
|
@@ -295,27 +570,101 @@ async function initialize(workspaceDir) {
|
|
|
295
570
|
config.embeddingProcessPerBatch = true;
|
|
296
571
|
}
|
|
297
572
|
}
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
573
|
+
const resolutionSource = config.workspaceResolution?.source || 'unknown';
|
|
574
|
+
if (resolutionSource === 'workspace-arg' || resolutionSource === 'env') {
|
|
575
|
+
trustWorkspacePath(config.searchDirectory);
|
|
576
|
+
}
|
|
577
|
+
const isSystemFallbackWorkspace =
|
|
578
|
+
(resolutionSource === 'cwd' || resolutionSource === 'cwd-root-search') &&
|
|
579
|
+
isNonProjectDirectory(config.searchDirectory);
|
|
580
|
+
|
|
581
|
+
let pidPath = null;
|
|
582
|
+
let logPath = null;
|
|
583
|
+
if (isSystemFallbackWorkspace) {
|
|
584
|
+
workspaceLockAcquired = false;
|
|
585
|
+
console.warn(
|
|
586
|
+
`[Server] System fallback workspace detected (${config.searchDirectory}); running in lightweight read-only mode.`
|
|
587
|
+
);
|
|
588
|
+
console.warn('[Server] Skipping lock/PID/log file setup for fallback workspace.');
|
|
589
|
+
} else {
|
|
590
|
+
if (config.autoStopOtherServersOnStartup !== false) {
|
|
591
|
+
const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
592
|
+
const { killed, failed } = await stopOtherHeuristicServers({
|
|
593
|
+
globalCacheRoot,
|
|
594
|
+
currentCacheDirectory: config.cacheDirectory,
|
|
595
|
+
});
|
|
596
|
+
if (killed.length > 0) {
|
|
597
|
+
const details = killed
|
|
598
|
+
.map((entry) => `${entry.pid}${entry.workspace ? ` (${entry.workspace})` : ''}`)
|
|
599
|
+
.join(', ');
|
|
600
|
+
console.info(`[Server] Auto-stopped ${killed.length} stale heuristic-mcp server(s): ${details}`);
|
|
601
|
+
}
|
|
602
|
+
if (failed.length > 0) {
|
|
603
|
+
const details = failed
|
|
604
|
+
.map((entry) => `${entry.pid}${entry.workspace ? ` (${entry.workspace})` : ''}`)
|
|
605
|
+
.join(', ');
|
|
606
|
+
console.warn(
|
|
607
|
+
`[Server] Failed to stop ${failed.length} older heuristic-mcp server(s): ${details}`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const lock = await acquireWorkspaceLock({
|
|
613
|
+
cacheDirectory: config.cacheDirectory,
|
|
614
|
+
workspaceDir: config.searchDirectory,
|
|
615
|
+
});
|
|
616
|
+
workspaceLockAcquired = lock.acquired;
|
|
617
|
+
if (!workspaceLockAcquired) {
|
|
618
|
+
console.warn(
|
|
619
|
+
`[Server] Another heuristic-mcp instance is already running for this workspace (pid ${lock.ownerPid ?? 'unknown'}).`
|
|
620
|
+
);
|
|
621
|
+
console.warn(
|
|
622
|
+
'[Server] Starting in secondary read-only mode: background indexing and cache writes are disabled for this instance.'
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
[pidPath, logPath] = workspaceLockAcquired
|
|
626
|
+
? await Promise.all([
|
|
627
|
+
setupPidFile({ pidFileName: PID_FILE_NAME, cacheDirectory: config.cacheDirectory }),
|
|
628
|
+
setupFileLogging(config),
|
|
629
|
+
])
|
|
630
|
+
: [null, await setupFileLogging(config)];
|
|
631
|
+
}
|
|
313
632
|
if (logPath) {
|
|
314
633
|
console.info(`[Logs] Writing server logs to ${logPath}`);
|
|
315
634
|
console.info(`[Logs] Log viewer: heuristic-mcp --logs --workspace "${config.searchDirectory}"`);
|
|
316
635
|
}
|
|
636
|
+
{
|
|
637
|
+
const resolution = config.workspaceResolution || {};
|
|
638
|
+
const sourceLabel =
|
|
639
|
+
resolution.source === 'env' && resolution.envKey
|
|
640
|
+
? `env:${resolution.envKey}`
|
|
641
|
+
: resolution.source || 'unknown';
|
|
642
|
+
const baseLabel = resolution.baseDirectory || '(unknown)';
|
|
643
|
+
const searchLabel = resolution.searchDirectory || config.searchDirectory;
|
|
644
|
+
const overrideLabel = resolution.searchDirectoryFromConfig ? 'yes' : 'no';
|
|
645
|
+
console.info(
|
|
646
|
+
`[Server] Workspace resolved: source=${sourceLabel}, base=${baseLabel}, search=${searchLabel}, configOverride=${overrideLabel}`
|
|
647
|
+
);
|
|
648
|
+
if (resolution.fromPath) {
|
|
649
|
+
console.info(`[Server] Workspace resolution origin cwd: ${resolution.fromPath}`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const workspaceEnvProbe = Array.isArray(resolution.workspaceEnvProbe)
|
|
653
|
+
? resolution.workspaceEnvProbe
|
|
654
|
+
: [];
|
|
655
|
+
if (workspaceEnvProbe.length > 0) {
|
|
656
|
+
const probePreview = workspaceEnvProbe.slice(0, 8).map((entry) => {
|
|
657
|
+
const scope = entry?.priority ? 'priority' : 'diagnostic';
|
|
658
|
+
const status = entry?.resolvedPath ? `valid:${entry.resolvedPath}` : `invalid:${entry?.value}`;
|
|
659
|
+
return `${entry?.key}[${scope}]=${status}`;
|
|
660
|
+
});
|
|
661
|
+
const suffix = workspaceEnvProbe.length > 8 ? ` (+${workspaceEnvProbe.length - 8} more)` : '';
|
|
662
|
+
console.info(`[Server] Workspace env probe: ${probePreview.join('; ')}${suffix}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
317
665
|
|
|
318
|
-
|
|
666
|
+
|
|
667
|
+
|
|
319
668
|
console.info(
|
|
320
669
|
`[Server] Config: workerThreads=${config.workerThreads}, embeddingProcessPerBatch=${config.embeddingProcessPerBatch}`
|
|
321
670
|
);
|
|
@@ -327,14 +676,17 @@ async function initialize(workspaceDir) {
|
|
|
327
676
|
console.info(`[Server] PID file: ${pidPath}`);
|
|
328
677
|
}
|
|
329
678
|
|
|
330
|
-
|
|
679
|
+
|
|
680
|
+
|
|
331
681
|
try {
|
|
332
682
|
const globalCache = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
333
683
|
const localCache = path.join(process.cwd(), '.heuristic-mcp');
|
|
334
684
|
console.info(`[Server] Cache debug: Global=${globalCache}, Local=${localCache}`);
|
|
335
685
|
console.info(`[Server] Process CWD: ${process.cwd()}`);
|
|
686
|
+
console.info(`[Server] Resolved workspace: ${config.searchDirectory} (via ${config.workspaceResolution?.source || 'unknown'})`);
|
|
336
687
|
} catch (_e) {
|
|
337
|
-
|
|
688
|
+
|
|
689
|
+
|
|
338
690
|
}
|
|
339
691
|
|
|
340
692
|
let stopStartupMemory = null;
|
|
@@ -343,7 +695,8 @@ async function initialize(workspaceDir) {
|
|
|
343
695
|
stopStartupMemory = startMemoryLogger('[Server] Memory (startup)', MEMORY_LOG_INTERVAL_MS);
|
|
344
696
|
}
|
|
345
697
|
|
|
346
|
-
|
|
698
|
+
|
|
699
|
+
|
|
347
700
|
try {
|
|
348
701
|
await fs.access(config.searchDirectory);
|
|
349
702
|
} catch {
|
|
@@ -351,7 +704,8 @@ async function initialize(workspaceDir) {
|
|
|
351
704
|
process.exit(1);
|
|
352
705
|
}
|
|
353
706
|
|
|
354
|
-
|
|
707
|
+
|
|
708
|
+
|
|
355
709
|
console.info('[Server] Initializing features...');
|
|
356
710
|
let cachedEmbedderPromise = null;
|
|
357
711
|
const lazyEmbedder = async (...args) => {
|
|
@@ -384,7 +738,7 @@ async function initialize(workspaceDir) {
|
|
|
384
738
|
return model(...args);
|
|
385
739
|
};
|
|
386
740
|
|
|
387
|
-
|
|
741
|
+
|
|
388
742
|
const unloader = async () => {
|
|
389
743
|
if (!cachedEmbedderPromise) return false;
|
|
390
744
|
try {
|
|
@@ -409,7 +763,7 @@ async function initialize(workspaceDir) {
|
|
|
409
763
|
};
|
|
410
764
|
|
|
411
765
|
embedder = lazyEmbedder;
|
|
412
|
-
unloadMainEmbedder = unloader;
|
|
766
|
+
unloadMainEmbedder = unloader;
|
|
413
767
|
const preloadEmbeddingModel = async () => {
|
|
414
768
|
if (config.preloadEmbeddingModel === false) return;
|
|
415
769
|
try {
|
|
@@ -420,29 +774,29 @@ async function initialize(workspaceDir) {
|
|
|
420
774
|
}
|
|
421
775
|
};
|
|
422
776
|
|
|
423
|
-
|
|
424
|
-
|
|
777
|
+
|
|
778
|
+
|
|
425
779
|
|
|
426
|
-
|
|
780
|
+
|
|
427
781
|
cache = new EmbeddingsCache(config);
|
|
428
782
|
console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
|
|
429
783
|
|
|
430
|
-
|
|
784
|
+
|
|
431
785
|
indexer = new CodebaseIndexer(embedder, cache, config, server);
|
|
432
786
|
hybridSearch = new HybridSearch(embedder, cache, config);
|
|
433
787
|
const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config, indexer);
|
|
434
788
|
const findSimilarCode = new FindSimilarCodeFeature.FindSimilarCode(embedder, cache, config);
|
|
435
789
|
const annConfig = new AnnConfigFeature.AnnConfigTool(cache, config);
|
|
436
790
|
|
|
437
|
-
|
|
791
|
+
|
|
438
792
|
features[0].instance = hybridSearch;
|
|
439
793
|
features[1].instance = indexer;
|
|
440
794
|
features[2].instance = cacheClearer;
|
|
441
795
|
features[3].instance = findSimilarCode;
|
|
442
796
|
features[4].instance = annConfig;
|
|
443
|
-
|
|
797
|
+
|
|
444
798
|
|
|
445
|
-
|
|
799
|
+
|
|
446
800
|
const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
|
|
447
801
|
config,
|
|
448
802
|
cache,
|
|
@@ -453,34 +807,99 @@ async function initialize(workspaceDir) {
|
|
|
453
807
|
features[6].instance = setWorkspaceInstance;
|
|
454
808
|
features[6].handler = SetWorkspaceFeature.createHandleToolCall(setWorkspaceInstance);
|
|
455
809
|
|
|
456
|
-
|
|
457
|
-
server.hybridSearch = hybridSearch;
|
|
458
|
-
|
|
459
|
-
const startBackgroundTasks = async () => {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
810
|
+
|
|
811
|
+
server.hybridSearch = hybridSearch;
|
|
812
|
+
|
|
813
|
+
const startBackgroundTasks = async () => {
|
|
814
|
+
const stopStartupMemoryLogger = () => {
|
|
815
|
+
if (stopStartupMemory) {
|
|
816
|
+
stopStartupMemory();
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
const tryAutoAttachWorkspaceCache = async (reason) => {
|
|
820
|
+
const candidate = await findAutoAttachWorkspaceCandidate({
|
|
821
|
+
excludeCacheDirectory: config.cacheDirectory,
|
|
822
|
+
});
|
|
823
|
+
if (!candidate) {
|
|
824
|
+
console.warn(
|
|
825
|
+
`[Server] Auto-attach skipped (${reason}): no unambiguous workspace cache candidate found.`
|
|
826
|
+
);
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
config.searchDirectory = candidate.workspace;
|
|
831
|
+
config.cacheDirectory = candidate.cacheDirectory;
|
|
832
|
+
await fs.mkdir(config.cacheDirectory, { recursive: true });
|
|
833
|
+
await cache.load();
|
|
834
|
+
console.info(
|
|
835
|
+
`[Server] Auto-attached workspace cache (${reason}): ${candidate.workspace} via ${candidate.source}`
|
|
836
|
+
);
|
|
837
|
+
if (config.verbose) {
|
|
838
|
+
logMemory('[Server] Memory (after cache load)');
|
|
839
|
+
}
|
|
840
|
+
return true;
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
const resolutionSource = config.workspaceResolution?.source || 'unknown';
|
|
844
|
+
const isSystemFallback =
|
|
845
|
+
(resolutionSource === 'cwd' || resolutionSource === 'cwd-root-search') &&
|
|
846
|
+
isNonProjectDirectory(config.searchDirectory);
|
|
847
|
+
|
|
848
|
+
if (isSystemFallback) {
|
|
849
|
+
try {
|
|
850
|
+
console.warn(
|
|
851
|
+
`[Server] Detected system fallback workspace: ${config.searchDirectory}. Attempting cache auto-attach.`
|
|
852
|
+
);
|
|
853
|
+
const attached = await tryAutoAttachWorkspaceCache('system-fallback');
|
|
854
|
+
if (!attached) {
|
|
855
|
+
console.warn(
|
|
856
|
+
'[Server] Waiting for a proper workspace root (MCP roots, env vars, or f_set_workspace).'
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
} finally {
|
|
860
|
+
stopStartupMemoryLogger();
|
|
861
|
+
}
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (!workspaceLockAcquired) {
|
|
866
|
+
try {
|
|
867
|
+
console.info('[Server] Secondary instance detected; loading cache in read-only mode.');
|
|
868
|
+
await cache.load();
|
|
869
|
+
if (cache.getStoreSize() === 0) {
|
|
870
|
+
await tryAutoAttachWorkspaceCache('secondary-empty-cache');
|
|
871
|
+
}
|
|
872
|
+
if (config.verbose) {
|
|
873
|
+
logMemory('[Server] Memory (after cache load)');
|
|
874
|
+
}
|
|
875
|
+
} finally {
|
|
876
|
+
stopStartupMemoryLogger();
|
|
877
|
+
}
|
|
878
|
+
console.info('[Server] Secondary instance ready; skipping background indexing.');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
void preloadEmbeddingModel();
|
|
883
|
+
|
|
463
884
|
try {
|
|
464
885
|
console.info('[Server] Loading cache (deferred)...');
|
|
465
|
-
await cache.load();
|
|
466
|
-
if (config.verbose) {
|
|
467
|
-
logMemory('[Server] Memory (after cache load)');
|
|
468
|
-
}
|
|
469
|
-
} finally {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
// Start indexing in background (non-blocking)
|
|
886
|
+
await cache.load();
|
|
887
|
+
if (config.verbose) {
|
|
888
|
+
logMemory('[Server] Memory (after cache load)');
|
|
889
|
+
}
|
|
890
|
+
} finally {
|
|
891
|
+
stopStartupMemoryLogger();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
|
|
476
895
|
console.info('[Server] Starting background indexing (delayed)...');
|
|
477
896
|
|
|
478
|
-
|
|
897
|
+
|
|
479
898
|
setTimeout(() => {
|
|
480
899
|
indexer
|
|
481
900
|
.indexAll()
|
|
482
901
|
.then(() => {
|
|
483
|
-
|
|
902
|
+
|
|
484
903
|
if (config.watchFiles) {
|
|
485
904
|
indexer.setupFileWatcher();
|
|
486
905
|
}
|
|
@@ -494,7 +913,7 @@ async function initialize(workspaceDir) {
|
|
|
494
913
|
return { startBackgroundTasks, config };
|
|
495
914
|
}
|
|
496
915
|
|
|
497
|
-
|
|
916
|
+
|
|
498
917
|
const server = new Server(
|
|
499
918
|
{
|
|
500
919
|
name: 'heuristic-mcp',
|
|
@@ -508,19 +927,46 @@ const server = new Server(
|
|
|
508
927
|
}
|
|
509
928
|
);
|
|
510
929
|
|
|
511
|
-
// Handle resources/list
|
|
512
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
513
|
-
return await handleListResources(config);
|
|
514
|
-
});
|
|
515
930
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
931
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
932
|
+
console.info('[Server] Received roots/list_changed notification from client.');
|
|
933
|
+
const newRoot = await detectWorkspaceFromRoots();
|
|
934
|
+
if (newRoot) {
|
|
935
|
+
await maybeAutoSwitchWorkspaceToPath(newRoot, {
|
|
936
|
+
source: 'roots changed',
|
|
937
|
+
reindex: true,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
|
|
520
943
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
944
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
945
|
+
await configReadyPromise;
|
|
946
|
+
if (configInitError || !config) {
|
|
947
|
+
throw configInitError ?? new Error('Server configuration is not initialized');
|
|
948
|
+
}
|
|
949
|
+
return await handleListResources(config);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
955
|
+
await configReadyPromise;
|
|
956
|
+
if (configInitError || !config) {
|
|
957
|
+
throw configInitError ?? new Error('Server configuration is not initialized');
|
|
958
|
+
}
|
|
959
|
+
return await handleReadResource(request.params.uri, config);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
965
|
+
await configReadyPromise;
|
|
966
|
+
if (configInitError || !config) {
|
|
967
|
+
throw configInitError ?? new Error('Server configuration is not initialized');
|
|
968
|
+
}
|
|
969
|
+
const tools = [];
|
|
524
970
|
|
|
525
971
|
for (const feature of features) {
|
|
526
972
|
const toolDef = feature.module.getToolDefinition(config);
|
|
@@ -530,15 +976,142 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
530
976
|
return { tools };
|
|
531
977
|
});
|
|
532
978
|
|
|
533
|
-
// Handle tool calls
|
|
534
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
535
|
-
await maybeAutoSwitchWorkspace(request);
|
|
536
979
|
|
|
537
|
-
|
|
980
|
+
|
|
981
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
982
|
+
await configReadyPromise;
|
|
983
|
+
if (configInitError || !config) {
|
|
984
|
+
return {
|
|
985
|
+
content: [
|
|
986
|
+
{
|
|
987
|
+
type: 'text',
|
|
988
|
+
text: `Server initialization failed: ${configInitError?.message || 'configuration not available'}`,
|
|
989
|
+
},
|
|
990
|
+
],
|
|
991
|
+
isError: true,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (!workspaceLockAcquired && request.params?.name === 'f_set_workspace') {
|
|
996
|
+
const args = request.params?.arguments || {};
|
|
997
|
+
const workspacePath = args.workspacePath;
|
|
998
|
+
const reindex = args.reindex !== false;
|
|
999
|
+
if (typeof workspacePath !== 'string' || workspacePath.trim().length === 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
content: [{ type: 'text', text: 'Error: workspacePath is required.' }],
|
|
1002
|
+
isError: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (reindex) {
|
|
1006
|
+
return {
|
|
1007
|
+
content: [
|
|
1008
|
+
{
|
|
1009
|
+
type: 'text',
|
|
1010
|
+
text: 'This server instance is in secondary read-only mode. Set reindex=false to attach cache only.',
|
|
1011
|
+
},
|
|
1012
|
+
],
|
|
1013
|
+
isError: true,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
const normalizedPath = path.resolve(workspacePath);
|
|
1017
|
+
try {
|
|
1018
|
+
const stats = await fs.stat(normalizedPath);
|
|
1019
|
+
if (!stats.isDirectory()) {
|
|
1020
|
+
return {
|
|
1021
|
+
content: [{ type: 'text', text: `Error: Path is not a directory: ${normalizedPath}` }],
|
|
1022
|
+
isError: true,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
return {
|
|
1027
|
+
content: [
|
|
1028
|
+
{
|
|
1029
|
+
type: 'text',
|
|
1030
|
+
text: `Error: Cannot access directory ${normalizedPath}: ${err.message}`,
|
|
1031
|
+
},
|
|
1032
|
+
],
|
|
1033
|
+
isError: true,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
config.searchDirectory = normalizedPath;
|
|
1038
|
+
config.cacheDirectory = getWorkspaceCachePath(normalizedPath, getGlobalCacheDir());
|
|
1039
|
+
try {
|
|
1040
|
+
await fs.mkdir(config.cacheDirectory, { recursive: true });
|
|
1041
|
+
await cache.load();
|
|
1042
|
+
trustWorkspacePath(normalizedPath);
|
|
1043
|
+
return {
|
|
1044
|
+
content: [
|
|
1045
|
+
{
|
|
1046
|
+
type: 'text',
|
|
1047
|
+
text: `Attached in read-only mode to workspace cache: ${normalizedPath}`,
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
};
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
return {
|
|
1053
|
+
content: [
|
|
1054
|
+
{
|
|
1055
|
+
type: 'text',
|
|
1056
|
+
text: `Error: Failed to attach cache for ${normalizedPath}: ${err.message}`,
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
isError: true,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (
|
|
1065
|
+
!workspaceLockAcquired &&
|
|
1066
|
+
['b_index_codebase', 'c_clear_cache'].includes(request.params?.name)
|
|
1067
|
+
) {
|
|
1068
|
+
return {
|
|
1069
|
+
content: [
|
|
1070
|
+
{
|
|
1071
|
+
type: 'text',
|
|
1072
|
+
text: 'This server instance is in secondary read-only mode. Use the primary instance for indexing/cache mutation tools.',
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
isError: true,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
const detectedFromRoots = await maybeAutoSwitchWorkspaceFromRoots(request);
|
|
1079
|
+
const detectedFromEnv = await maybeAutoSwitchWorkspace(request);
|
|
1080
|
+
if (detectedFromRoots) {
|
|
1081
|
+
trustWorkspacePath(detectedFromRoots);
|
|
1082
|
+
}
|
|
1083
|
+
if (detectedFromEnv) {
|
|
1084
|
+
trustWorkspacePath(detectedFromEnv);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const toolName = request.params?.name;
|
|
1088
|
+
if (
|
|
1089
|
+
config.requireTrustedWorkspaceSignalForTools === true &&
|
|
1090
|
+
shouldRequireTrustedWorkspaceSignalForTool(toolName) &&
|
|
1091
|
+
!detectedFromRoots &&
|
|
1092
|
+
!detectedFromEnv &&
|
|
1093
|
+
!isCurrentWorkspaceTrusted()
|
|
1094
|
+
) {
|
|
1095
|
+
return {
|
|
1096
|
+
content: [
|
|
1097
|
+
{
|
|
1098
|
+
type: 'text',
|
|
1099
|
+
text:
|
|
1100
|
+
`Workspace context appears stale for "${toolName}" (current: "${config.searchDirectory}"). ` +
|
|
1101
|
+
'Please reload your IDE window and retry. ' +
|
|
1102
|
+
'If needed, call MCP tool "f_set_workspace" from your chat/client with your opened folder path.',
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
isError: true,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
for (const feature of features) {
|
|
538
1110
|
const toolDef = feature.module.getToolDefinition(config);
|
|
539
1111
|
|
|
540
1112
|
if (request.params.name === toolDef.name) {
|
|
541
|
-
|
|
1113
|
+
|
|
1114
|
+
|
|
542
1115
|
if (typeof feature.handler !== 'function') {
|
|
543
1116
|
return {
|
|
544
1117
|
content: [{
|
|
@@ -547,14 +1120,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
547
1120
|
}],
|
|
548
1121
|
isError: true,
|
|
549
1122
|
};
|
|
550
|
-
}
|
|
551
|
-
const result = await feature.handler(request, feature.instance);
|
|
1123
|
+
}
|
|
1124
|
+
const result = await feature.handler(request, feature.instance);
|
|
1125
|
+
if (toolDef.name === 'f_set_workspace' && !isToolResponseError(result)) {
|
|
1126
|
+
trustWorkspacePath(config.searchDirectory);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
|
|
552
1132
|
|
|
553
|
-
// Unload embedding model after search-related tools to free memory
|
|
554
|
-
// Tools that use embedder: a_semantic_search, d_find_similar_code
|
|
555
1133
|
const searchTools = ['a_semantic_search', 'd_find_similar_code'];
|
|
556
1134
|
if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
|
|
557
|
-
|
|
1135
|
+
|
|
1136
|
+
|
|
558
1137
|
setImmediate(async () => {
|
|
559
1138
|
if (typeof unloadMainEmbedder === 'function') {
|
|
560
1139
|
await unloadMainEmbedder();
|
|
@@ -577,7 +1156,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
577
1156
|
};
|
|
578
1157
|
});
|
|
579
1158
|
|
|
580
|
-
|
|
1159
|
+
|
|
1160
|
+
|
|
581
1161
|
export async function main(argv = process.argv) {
|
|
582
1162
|
const parsed = parseArgs(argv);
|
|
583
1163
|
const {
|
|
@@ -641,19 +1221,23 @@ export async function main(argv = process.argv) {
|
|
|
641
1221
|
process.exit(0);
|
|
642
1222
|
}
|
|
643
1223
|
|
|
644
|
-
|
|
1224
|
+
|
|
1225
|
+
|
|
645
1226
|
if (wantsCache) {
|
|
646
1227
|
await status({ fix: wantsClean, cacheOnly: true, workspaceDir });
|
|
647
1228
|
process.exit(0);
|
|
648
1229
|
}
|
|
649
1230
|
|
|
650
|
-
|
|
1231
|
+
|
|
1232
|
+
|
|
651
1233
|
const clearIndex = parsed.rawArgs.indexOf('--clear');
|
|
652
1234
|
if (clearIndex !== -1) {
|
|
653
1235
|
const cacheId = parsed.rawArgs[clearIndex + 1];
|
|
654
1236
|
if (cacheId && !cacheId.startsWith('--')) {
|
|
655
|
-
|
|
656
|
-
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
|
|
657
1241
|
let cacheHome;
|
|
658
1242
|
if (process.platform === 'win32') {
|
|
659
1243
|
cacheHome = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
@@ -697,7 +1281,8 @@ export async function main(argv = process.argv) {
|
|
|
697
1281
|
}
|
|
698
1282
|
process.exit(0);
|
|
699
1283
|
}
|
|
700
|
-
|
|
1284
|
+
|
|
1285
|
+
|
|
701
1286
|
}
|
|
702
1287
|
|
|
703
1288
|
if (wantsClearCache) {
|
|
@@ -740,34 +1325,88 @@ export async function main(argv = process.argv) {
|
|
|
740
1325
|
}
|
|
741
1326
|
|
|
742
1327
|
registerSignalHandlers(requestShutdown);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const { startBackgroundTasks } = await initialize(workspaceDir);
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
|
|
748
1332
|
|
|
749
|
-
|
|
1333
|
+
|
|
1334
|
+
const detectedRootPromise = new Promise((resolve) => {
|
|
1335
|
+
const HANDSHAKE_TIMEOUT_MS = 1000;
|
|
1336
|
+
let settled = false;
|
|
1337
|
+
const resolveOnce = (value) => {
|
|
1338
|
+
if (settled) return;
|
|
1339
|
+
settled = true;
|
|
1340
|
+
resolve(value);
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
const timer = setTimeout(() => {
|
|
1344
|
+
console.warn(`[Server] MCP handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms, proceeding without roots.`);
|
|
1345
|
+
resolveOnce(null);
|
|
1346
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
1347
|
+
|
|
1348
|
+
server.oninitialized = async () => {
|
|
1349
|
+
clearTimeout(timer);
|
|
1350
|
+
console.info('[Server] MCP handshake complete.');
|
|
1351
|
+
const root = await detectWorkspaceFromRoots();
|
|
1352
|
+
resolveOnce(root);
|
|
1353
|
+
};
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
const transport = new StdioServerTransport();
|
|
1357
|
+
await server.connect(transport);
|
|
1358
|
+
console.info('[Server] MCP transport connected.');
|
|
1359
|
+
if (isServerMode) {
|
|
1360
|
+
process.stdin?.on?.('end', () => requestShutdown('stdin-end'));
|
|
1361
|
+
process.stdin?.on?.('close', () => requestShutdown('stdin-close'));
|
|
1362
|
+
process.stdout?.on?.('error', (err) => {
|
|
1363
|
+
if (err?.code === 'EPIPE') {
|
|
1364
|
+
requestShutdown('stdout-epipe');
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
const detectedRoot = await detectedRootPromise;
|
|
750
1372
|
|
|
751
|
-
|
|
752
|
-
|
|
1373
|
+
|
|
1374
|
+
const effectiveWorkspace = detectedRoot || workspaceDir;
|
|
1375
|
+
if (detectedRoot) {
|
|
1376
|
+
console.info(`[Server] Using workspace from MCP roots: ${detectedRoot}`);
|
|
1377
|
+
}
|
|
1378
|
+
const initPromise = initialize(effectiveWorkspace);
|
|
1379
|
+
const initWithResolve = initPromise
|
|
1380
|
+
.then((result) => {
|
|
1381
|
+
configReadyResolve();
|
|
1382
|
+
return result;
|
|
1383
|
+
})
|
|
1384
|
+
.catch((err) => {
|
|
1385
|
+
configInitError = err;
|
|
1386
|
+
configReadyResolve();
|
|
1387
|
+
throw err;
|
|
1388
|
+
});
|
|
1389
|
+
const { startBackgroundTasks } = await initWithResolve;
|
|
753
1390
|
|
|
754
|
-
console.info('[Server] MCP transport connected.');
|
|
755
1391
|
console.info('[Server] Heuristic MCP server started.');
|
|
756
1392
|
|
|
757
|
-
|
|
1393
|
+
|
|
1394
|
+
|
|
758
1395
|
void startBackgroundTasks().catch((err) => {
|
|
759
1396
|
console.error(`[Server] Background task error: ${err.message}`);
|
|
760
1397
|
});
|
|
761
1398
|
console.info('[Server] MCP server is now fully ready to accept requests.');
|
|
762
1399
|
}
|
|
763
1400
|
|
|
764
|
-
|
|
1401
|
+
|
|
1402
|
+
|
|
765
1403
|
async function gracefulShutdown(signal) {
|
|
766
1404
|
console.info(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
767
1405
|
|
|
768
1406
|
const cleanupTasks = [];
|
|
769
1407
|
|
|
770
|
-
|
|
1408
|
+
|
|
1409
|
+
|
|
771
1410
|
if (indexer && indexer.watcher) {
|
|
772
1411
|
cleanupTasks.push(
|
|
773
1412
|
indexer.watcher
|
|
@@ -777,7 +1416,8 @@ async function gracefulShutdown(signal) {
|
|
|
777
1416
|
);
|
|
778
1417
|
}
|
|
779
1418
|
|
|
780
|
-
|
|
1419
|
+
|
|
1420
|
+
|
|
781
1421
|
if (indexer && indexer.terminateWorkers) {
|
|
782
1422
|
cleanupTasks.push(
|
|
783
1423
|
(async () => {
|
|
@@ -788,20 +1428,26 @@ async function gracefulShutdown(signal) {
|
|
|
788
1428
|
);
|
|
789
1429
|
}
|
|
790
1430
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
if (cache) {
|
|
1434
|
+
if (!workspaceLockAcquired) {
|
|
1435
|
+
console.info('[Server] Secondary/fallback mode: skipping cache save.');
|
|
1436
|
+
} else {
|
|
1437
|
+
cleanupTasks.push(
|
|
1438
|
+
cache
|
|
1439
|
+
.save()
|
|
1440
|
+
.then(() => console.info('[Server] Cache saved'))
|
|
1441
|
+
.catch((err) => console.error(`[Server] Failed to save cache: ${err.message}`))
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
800
1445
|
|
|
801
1446
|
await Promise.allSettled(cleanupTasks);
|
|
802
1447
|
console.info('[Server] Goodbye!');
|
|
803
1448
|
|
|
804
|
-
|
|
1449
|
+
|
|
1450
|
+
|
|
805
1451
|
setTimeout(() => process.exit(0), 100);
|
|
806
1452
|
}
|
|
807
1453
|
|