@softerist/heuristic-mcp 3.0.17 → 3.2.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 +912 -200
- 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 +52 -48
- 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 +148 -35
- 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';
|
|
32
|
-
import { clearStaleCaches } from './lib/cache-utils.js';
|
|
33
|
-
import { enableStderrOnlyLogging, setupFileLogging, getLogFilePath } from './lib/logging.js';
|
|
34
|
-
import { parseArgs, printHelp } from './lib/cli.js';
|
|
35
|
-
import { clearCache } from './lib/cache-ops.js';
|
|
36
|
-
import { logMemory, startMemoryLogger } from './lib/memory-logger.js';
|
|
37
|
-
import {
|
|
38
|
-
registerSignalHandlers,
|
|
39
|
-
setupPidFile,
|
|
40
|
-
acquireWorkspaceLock,
|
|
41
|
-
|
|
39
|
+
import { loadConfig, getGlobalCacheDir, isNonProjectDirectory } from './lib/config.js';
|
|
40
|
+
import { clearStaleCaches } from './lib/cache-utils.js';
|
|
41
|
+
import { enableStderrOnlyLogging, setupFileLogging, getLogFilePath, flushLogs } from './lib/logging.js';
|
|
42
|
+
import { parseArgs, printHelp } from './lib/cli.js';
|
|
43
|
+
import { clearCache } from './lib/cache-ops.js';
|
|
44
|
+
import { logMemory, startMemoryLogger } from './lib/memory-logger.js';
|
|
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,115 @@ 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
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatCrashDetail(detail) {
|
|
186
|
+
if (detail instanceof Error) {
|
|
187
|
+
return detail.stack || detail.message || String(detail);
|
|
188
|
+
}
|
|
189
|
+
if (typeof detail === 'string') {
|
|
190
|
+
return detail;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return JSON.stringify(detail);
|
|
194
|
+
} catch {
|
|
195
|
+
return String(detail);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isCrashShutdownReason(reason) {
|
|
200
|
+
const normalized = String(reason || '').toLowerCase();
|
|
201
|
+
return normalized.includes('uncaughtexception') || normalized.includes('unhandledrejection');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdownReason }) {
|
|
205
|
+
if (!isServerMode) return;
|
|
206
|
+
|
|
207
|
+
process.on('beforeExit', (code) => {
|
|
208
|
+
const reason = getShutdownReason() || 'natural';
|
|
209
|
+
console.info(`[Server] Process beforeExit (code=${code}, reason=${reason}).`);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
process.on('exit', (code) => {
|
|
213
|
+
const reason = getShutdownReason() || 'natural';
|
|
214
|
+
console.info(`[Server] Process exit (code=${code}, reason=${reason}).`);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
let fatalHandled = false;
|
|
218
|
+
const handleFatalError = (reason, detail) => {
|
|
219
|
+
if (fatalHandled) return;
|
|
220
|
+
fatalHandled = true;
|
|
221
|
+
console.error(`[Server] Fatal ${reason}: ${formatCrashDetail(detail)}`);
|
|
222
|
+
requestShutdown(reason);
|
|
223
|
+
const forceExitTimer = setTimeout(() => {
|
|
224
|
+
console.error(`[Server] Forced exit after fatal ${reason}.`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}, 5000);
|
|
227
|
+
forceExitTimer.unref?.();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
process.on('uncaughtException', (err) => {
|
|
231
|
+
handleFatalError('uncaughtException', err);
|
|
232
|
+
});
|
|
233
|
+
process.on('unhandledRejection', (reason) => {
|
|
234
|
+
handleFatalError('unhandledRejection', reason);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
130
237
|
|
|
131
238
|
async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
132
239
|
if (!rawValue || rawValue.includes('${')) return null;
|
|
@@ -140,56 +247,272 @@ async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
|
140
247
|
}
|
|
141
248
|
}
|
|
142
249
|
|
|
143
|
-
async function detectRuntimeWorkspaceFromEnv() {
|
|
144
|
-
for (const key of getWorkspaceEnvKeys()) {
|
|
145
|
-
const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
|
|
250
|
+
async function detectRuntimeWorkspaceFromEnv() {
|
|
251
|
+
for (const key of getWorkspaceEnvKeys()) {
|
|
252
|
+
const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
|
|
146
253
|
if (workspacePath) {
|
|
147
254
|
return { workspacePath, envKey: key };
|
|
148
255
|
}
|
|
149
256
|
}
|
|
150
257
|
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizePathForCompare(targetPath) {
|
|
262
|
+
if (!targetPath) return '';
|
|
263
|
+
const resolved = path.resolve(targetPath);
|
|
264
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isProcessAlive(pid) {
|
|
268
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
269
|
+
try {
|
|
270
|
+
process.kill(pid, 0);
|
|
271
|
+
return true;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return err?.code === 'EPERM';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function findAutoAttachWorkspaceCandidate({ excludeCacheDirectory = null } = {}) {
|
|
278
|
+
const cacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
279
|
+
const normalizedExclude = normalizePathForCompare(excludeCacheDirectory);
|
|
280
|
+
|
|
281
|
+
let cacheDirs = [];
|
|
282
|
+
try {
|
|
283
|
+
cacheDirs = await fs.readdir(cacheRoot, { withFileTypes: true });
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const candidatesByWorkspace = new Map();
|
|
289
|
+
const preferredWorkspaceFromEnv = (await detectRuntimeWorkspaceFromEnv())?.workspacePath ?? null;
|
|
290
|
+
const normalizedPreferred = normalizePathForCompare(preferredWorkspaceFromEnv);
|
|
291
|
+
|
|
292
|
+
const upsertCandidate = (candidate) => {
|
|
293
|
+
const key = normalizePathForCompare(candidate.workspace);
|
|
294
|
+
const existing = candidatesByWorkspace.get(key);
|
|
295
|
+
if (!existing || candidate.rank > existing.rank) {
|
|
296
|
+
candidatesByWorkspace.set(key, candidate);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
for (const entry of cacheDirs) {
|
|
301
|
+
if (!entry.isDirectory()) continue;
|
|
302
|
+
const cacheDirectory = path.join(cacheRoot, entry.name);
|
|
303
|
+
if (normalizedExclude && normalizePathForCompare(cacheDirectory) === normalizedExclude) continue;
|
|
304
|
+
|
|
305
|
+
const lockPath = path.join(cacheDirectory, 'server.lock.json');
|
|
306
|
+
try {
|
|
307
|
+
const rawLock = await fs.readFile(lockPath, 'utf-8');
|
|
308
|
+
const lock = JSON.parse(rawLock);
|
|
309
|
+
if (!isProcessAlive(lock?.pid)) continue;
|
|
310
|
+
const workspace = path.resolve(lock?.workspace || '');
|
|
311
|
+
if (!workspace || isNonProjectDirectory(workspace)) continue;
|
|
312
|
+
const stats = await fs.stat(workspace).catch(() => null);
|
|
313
|
+
if (!stats?.isDirectory()) continue;
|
|
314
|
+
const rank = Date.parse(lock?.startedAt || '') || 0;
|
|
315
|
+
upsertCandidate({
|
|
316
|
+
workspace,
|
|
317
|
+
cacheDirectory,
|
|
318
|
+
source: `lock:${lock.pid}`,
|
|
319
|
+
rank,
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
} catch {
|
|
323
|
+
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const metaPath = path.join(cacheDirectory, 'meta.json');
|
|
327
|
+
try {
|
|
328
|
+
const rawMeta = await fs.readFile(metaPath, 'utf-8');
|
|
329
|
+
const meta = JSON.parse(rawMeta);
|
|
330
|
+
const workspace = path.resolve(meta?.workspace || '');
|
|
331
|
+
if (!workspace || isNonProjectDirectory(workspace)) continue;
|
|
332
|
+
const stats = await fs.stat(workspace).catch(() => null);
|
|
333
|
+
if (!stats?.isDirectory()) continue;
|
|
334
|
+
const filesIndexed = Number(meta?.filesIndexed || 0);
|
|
335
|
+
if (filesIndexed <= 0) continue;
|
|
336
|
+
const rank = Date.parse(meta?.lastSaveTime || '') || 0;
|
|
337
|
+
upsertCandidate({
|
|
338
|
+
workspace,
|
|
339
|
+
cacheDirectory,
|
|
340
|
+
source: 'meta',
|
|
341
|
+
rank,
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const candidates = Array.from(candidatesByWorkspace.values());
|
|
349
|
+
if (candidates.length === 0) return null;
|
|
350
|
+
if (normalizedPreferred) {
|
|
351
|
+
const preferred = candidates.find(
|
|
352
|
+
(candidate) => normalizePathForCompare(candidate.workspace) === normalizedPreferred
|
|
353
|
+
);
|
|
354
|
+
if (preferred) return preferred;
|
|
355
|
+
}
|
|
356
|
+
if (candidates.length === 1) return candidates[0];
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function maybeAutoSwitchWorkspace(request) {
|
|
361
|
+
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return null;
|
|
362
|
+
if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return null;
|
|
363
|
+
if (request?.params?.name === 'f_set_workspace') return null;
|
|
364
|
+
|
|
365
|
+
const detected = await detectRuntimeWorkspaceFromEnv();
|
|
366
|
+
if (!detected) return null;
|
|
367
|
+
if (isNonProjectDirectory(detected.workspacePath)) {
|
|
368
|
+
console.info(
|
|
369
|
+
`[Server] Ignoring auto-switch candidate from env ${detected.envKey}: non-project path ${detected.workspacePath}`
|
|
370
|
+
);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const currentWorkspace = normalizePathForCompare(config.searchDirectory);
|
|
375
|
+
const detectedWorkspace = normalizePathForCompare(detected.workspacePath);
|
|
376
|
+
if (detectedWorkspace === currentWorkspace) return detected.workspacePath;
|
|
377
|
+
|
|
378
|
+
await maybeAutoSwitchWorkspaceToPath(detected.workspacePath, {
|
|
379
|
+
source: `env ${detected.envKey}`,
|
|
380
|
+
reindex: false,
|
|
381
|
+
});
|
|
382
|
+
return detected.workspacePath;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async function detectWorkspaceFromRoots({ quiet = false } = {}) {
|
|
388
|
+
try {
|
|
389
|
+
const caps = server.getClientCapabilities();
|
|
390
|
+
if (!caps?.roots) {
|
|
391
|
+
rootsCapabilitySupported = false;
|
|
392
|
+
if (!quiet) {
|
|
393
|
+
console.info(
|
|
394
|
+
'[Server] Client does not support roots capability, skipping workspace auto-detection.'
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
rootsCapabilitySupported = true;
|
|
400
|
+
|
|
401
|
+
const result = await server.listRoots();
|
|
402
|
+
if (!result?.roots?.length) {
|
|
403
|
+
if (!quiet) {
|
|
404
|
+
console.info('[Server] Client returned no roots.');
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!quiet) {
|
|
410
|
+
console.info(`[Server] MCP roots received: ${result.roots.map(r => r.uri).join(', ')}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
const rootPaths = result.roots
|
|
415
|
+
.map(r => r.uri)
|
|
416
|
+
.filter(uri => uri.startsWith('file://'))
|
|
417
|
+
.map(uri => {
|
|
418
|
+
try {
|
|
419
|
+
return fileURLToPath(uri);
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
|
|
426
|
+
if (rootPaths.length === 0) {
|
|
427
|
+
if (!quiet) {
|
|
428
|
+
console.info('[Server] No valid file:// roots found.');
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return path.resolve(rootPaths[0]);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
if (!quiet) {
|
|
436
|
+
console.warn(`[Server] MCP roots detection failed (non-fatal): ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function maybeAutoSwitchWorkspaceToPath(targetWorkspacePath, { source, reindex = false } = {}) {
|
|
443
|
+
if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return;
|
|
444
|
+
if (!targetWorkspacePath) return;
|
|
445
|
+
if (isNonProjectDirectory(targetWorkspacePath)) {
|
|
446
|
+
if (config?.verbose) {
|
|
447
|
+
console.info(
|
|
448
|
+
`[Server] Ignoring auto-switch candidate from ${source || 'unknown'}: non-project path ${targetWorkspacePath}`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const currentWorkspace = normalizePathForCompare(config.searchDirectory);
|
|
455
|
+
const targetWorkspace = normalizePathForCompare(targetWorkspacePath);
|
|
456
|
+
if (targetWorkspace === currentWorkspace) return;
|
|
457
|
+
|
|
458
|
+
if (autoWorkspaceSwitchPromise) {
|
|
459
|
+
await autoWorkspaceSwitchPromise;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
autoWorkspaceSwitchPromise = (async () => {
|
|
464
|
+
console.info(
|
|
465
|
+
`[Server] Auto-switching workspace from ${currentWorkspace} to ${targetWorkspacePath} (${source || 'auto'})`
|
|
466
|
+
);
|
|
467
|
+
const result = await setWorkspaceFeatureInstance.execute({
|
|
468
|
+
workspacePath: targetWorkspacePath,
|
|
469
|
+
reindex,
|
|
470
|
+
});
|
|
471
|
+
if (!result.success) {
|
|
472
|
+
console.warn(
|
|
473
|
+
`[Server] Auto workspace switch failed (${source || 'auto'}): ${result.error}`
|
|
474
|
+
);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
trustWorkspacePath(targetWorkspacePath);
|
|
478
|
+
})();
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
await autoWorkspaceSwitchPromise;
|
|
482
|
+
} finally {
|
|
483
|
+
autoWorkspaceSwitchPromise = null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function maybeAutoSwitchWorkspaceFromRoots(request) {
|
|
488
|
+
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return null;
|
|
489
|
+
if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return null;
|
|
490
|
+
if (request?.params?.name === 'f_set_workspace') return null;
|
|
491
|
+
if (rootsCapabilitySupported === false) return null;
|
|
492
|
+
|
|
493
|
+
if (rootsProbeInFlight) {
|
|
494
|
+
return await rootsProbeInFlight;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
rootsProbeInFlight = (async () => {
|
|
498
|
+
const rootWorkspace = await detectWorkspaceFromRoots({ quiet: true });
|
|
499
|
+
if (!rootWorkspace) return null;
|
|
500
|
+
await maybeAutoSwitchWorkspaceToPath(rootWorkspace, {
|
|
501
|
+
source: 'roots probe',
|
|
502
|
+
reindex: false,
|
|
503
|
+
});
|
|
504
|
+
return rootWorkspace;
|
|
505
|
+
})();
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
return await rootsProbeInFlight;
|
|
509
|
+
} finally {
|
|
510
|
+
rootsProbeInFlight = null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
158
513
|
|
|
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
514
|
|
|
185
|
-
try {
|
|
186
|
-
await autoWorkspaceSwitchPromise;
|
|
187
|
-
} finally {
|
|
188
|
-
autoWorkspaceSwitchPromise = null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
515
|
|
|
192
|
-
// Feature registry - ordered by priority (semantic_search first as primary tool)
|
|
193
516
|
const features = [
|
|
194
517
|
{
|
|
195
518
|
module: HybridSearchFeature,
|
|
@@ -221,19 +544,22 @@ const features = [
|
|
|
221
544
|
instance: null,
|
|
222
545
|
handler: PackageVersionFeature.handleToolCall,
|
|
223
546
|
},
|
|
224
|
-
{
|
|
225
|
-
module: SetWorkspaceFeature,
|
|
226
|
-
instance: null,
|
|
227
|
-
handler: null,
|
|
228
|
-
},
|
|
229
|
-
];
|
|
547
|
+
{
|
|
548
|
+
module: SetWorkspaceFeature,
|
|
549
|
+
instance: null,
|
|
550
|
+
handler: null,
|
|
551
|
+
},
|
|
552
|
+
];
|
|
553
|
+
|
|
230
554
|
|
|
231
|
-
|
|
232
|
-
async function initialize(workspaceDir) {
|
|
233
|
-
|
|
555
|
+
|
|
556
|
+
async function initialize(workspaceDir) {
|
|
557
|
+
|
|
558
|
+
|
|
234
559
|
config = await loadConfig(workspaceDir);
|
|
235
560
|
|
|
236
|
-
|
|
561
|
+
|
|
562
|
+
|
|
237
563
|
if (config.enableCache && config.cacheCleanup?.autoCleanup) {
|
|
238
564
|
console.info('[Server] Running automatic cache cleanup...');
|
|
239
565
|
const results = await clearStaleCaches({
|
|
@@ -245,7 +571,8 @@ async function initialize(workspaceDir) {
|
|
|
245
571
|
}
|
|
246
572
|
}
|
|
247
573
|
|
|
248
|
-
|
|
574
|
+
|
|
575
|
+
|
|
249
576
|
const isTest = Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
|
|
250
577
|
if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
|
|
251
578
|
console.warn(
|
|
@@ -280,7 +607,8 @@ async function initialize(workspaceDir) {
|
|
|
280
607
|
env.backends.onnx.wasm.numThreads = ONNX_THREAD_LIMIT;
|
|
281
608
|
}
|
|
282
609
|
} catch {
|
|
283
|
-
|
|
610
|
+
|
|
611
|
+
|
|
284
612
|
}
|
|
285
613
|
const status = getNativeOnnxStatus();
|
|
286
614
|
const reason = status?.message || 'onnxruntime-node not available';
|
|
@@ -295,27 +623,101 @@ async function initialize(workspaceDir) {
|
|
|
295
623
|
config.embeddingProcessPerBatch = true;
|
|
296
624
|
}
|
|
297
625
|
}
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
626
|
+
const resolutionSource = config.workspaceResolution?.source || 'unknown';
|
|
627
|
+
if (resolutionSource === 'workspace-arg' || resolutionSource === 'env') {
|
|
628
|
+
trustWorkspacePath(config.searchDirectory);
|
|
629
|
+
}
|
|
630
|
+
const isSystemFallbackWorkspace =
|
|
631
|
+
(resolutionSource === 'cwd' || resolutionSource === 'cwd-root-search') &&
|
|
632
|
+
isNonProjectDirectory(config.searchDirectory);
|
|
633
|
+
|
|
634
|
+
let pidPath = null;
|
|
635
|
+
let logPath = null;
|
|
636
|
+
if (isSystemFallbackWorkspace) {
|
|
637
|
+
workspaceLockAcquired = false;
|
|
638
|
+
console.warn(
|
|
639
|
+
`[Server] System fallback workspace detected (${config.searchDirectory}); running in lightweight read-only mode.`
|
|
640
|
+
);
|
|
641
|
+
console.warn('[Server] Skipping lock/PID/log file setup for fallback workspace.');
|
|
642
|
+
} else {
|
|
643
|
+
if (config.autoStopOtherServersOnStartup !== false) {
|
|
644
|
+
const globalCacheRoot = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
645
|
+
const { killed, failed } = await stopOtherHeuristicServers({
|
|
646
|
+
globalCacheRoot,
|
|
647
|
+
currentCacheDirectory: config.cacheDirectory,
|
|
648
|
+
});
|
|
649
|
+
if (killed.length > 0) {
|
|
650
|
+
const details = killed
|
|
651
|
+
.map((entry) => `${entry.pid}${entry.workspace ? ` (${entry.workspace})` : ''}`)
|
|
652
|
+
.join(', ');
|
|
653
|
+
console.info(`[Server] Auto-stopped ${killed.length} stale heuristic-mcp server(s): ${details}`);
|
|
654
|
+
}
|
|
655
|
+
if (failed.length > 0) {
|
|
656
|
+
const details = failed
|
|
657
|
+
.map((entry) => `${entry.pid}${entry.workspace ? ` (${entry.workspace})` : ''}`)
|
|
658
|
+
.join(', ');
|
|
659
|
+
console.warn(
|
|
660
|
+
`[Server] Failed to stop ${failed.length} older heuristic-mcp server(s): ${details}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const lock = await acquireWorkspaceLock({
|
|
666
|
+
cacheDirectory: config.cacheDirectory,
|
|
667
|
+
workspaceDir: config.searchDirectory,
|
|
668
|
+
});
|
|
669
|
+
workspaceLockAcquired = lock.acquired;
|
|
670
|
+
if (!workspaceLockAcquired) {
|
|
671
|
+
console.warn(
|
|
672
|
+
`[Server] Another heuristic-mcp instance is already running for this workspace (pid ${lock.ownerPid ?? 'unknown'}).`
|
|
673
|
+
);
|
|
674
|
+
console.warn(
|
|
675
|
+
'[Server] Starting in secondary read-only mode: background indexing and cache writes are disabled for this instance.'
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
[pidPath, logPath] = workspaceLockAcquired
|
|
679
|
+
? await Promise.all([
|
|
680
|
+
setupPidFile({ pidFileName: PID_FILE_NAME, cacheDirectory: config.cacheDirectory }),
|
|
681
|
+
setupFileLogging(config),
|
|
682
|
+
])
|
|
683
|
+
: [null, await setupFileLogging(config)];
|
|
684
|
+
}
|
|
313
685
|
if (logPath) {
|
|
314
686
|
console.info(`[Logs] Writing server logs to ${logPath}`);
|
|
315
687
|
console.info(`[Logs] Log viewer: heuristic-mcp --logs --workspace "${config.searchDirectory}"`);
|
|
316
688
|
}
|
|
689
|
+
{
|
|
690
|
+
const resolution = config.workspaceResolution || {};
|
|
691
|
+
const sourceLabel =
|
|
692
|
+
resolution.source === 'env' && resolution.envKey
|
|
693
|
+
? `env:${resolution.envKey}`
|
|
694
|
+
: resolution.source || 'unknown';
|
|
695
|
+
const baseLabel = resolution.baseDirectory || '(unknown)';
|
|
696
|
+
const searchLabel = resolution.searchDirectory || config.searchDirectory;
|
|
697
|
+
const overrideLabel = resolution.searchDirectoryFromConfig ? 'yes' : 'no';
|
|
698
|
+
console.info(
|
|
699
|
+
`[Server] Workspace resolved: source=${sourceLabel}, base=${baseLabel}, search=${searchLabel}, configOverride=${overrideLabel}`
|
|
700
|
+
);
|
|
701
|
+
if (resolution.fromPath) {
|
|
702
|
+
console.info(`[Server] Workspace resolution origin cwd: ${resolution.fromPath}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const workspaceEnvProbe = Array.isArray(resolution.workspaceEnvProbe)
|
|
706
|
+
? resolution.workspaceEnvProbe
|
|
707
|
+
: [];
|
|
708
|
+
if (workspaceEnvProbe.length > 0) {
|
|
709
|
+
const probePreview = workspaceEnvProbe.slice(0, 8).map((entry) => {
|
|
710
|
+
const scope = entry?.priority ? 'priority' : 'diagnostic';
|
|
711
|
+
const status = entry?.resolvedPath ? `valid:${entry.resolvedPath}` : `invalid:${entry?.value}`;
|
|
712
|
+
return `${entry?.key}[${scope}]=${status}`;
|
|
713
|
+
});
|
|
714
|
+
const suffix = workspaceEnvProbe.length > 8 ? ` (+${workspaceEnvProbe.length - 8} more)` : '';
|
|
715
|
+
console.info(`[Server] Workspace env probe: ${probePreview.join('; ')}${suffix}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
317
718
|
|
|
318
|
-
|
|
719
|
+
|
|
720
|
+
|
|
319
721
|
console.info(
|
|
320
722
|
`[Server] Config: workerThreads=${config.workerThreads}, embeddingProcessPerBatch=${config.embeddingProcessPerBatch}`
|
|
321
723
|
);
|
|
@@ -327,14 +729,17 @@ async function initialize(workspaceDir) {
|
|
|
327
729
|
console.info(`[Server] PID file: ${pidPath}`);
|
|
328
730
|
}
|
|
329
731
|
|
|
330
|
-
|
|
732
|
+
|
|
733
|
+
|
|
331
734
|
try {
|
|
332
735
|
const globalCache = path.join(getGlobalCacheDir(), 'heuristic-mcp');
|
|
333
736
|
const localCache = path.join(process.cwd(), '.heuristic-mcp');
|
|
334
737
|
console.info(`[Server] Cache debug: Global=${globalCache}, Local=${localCache}`);
|
|
335
738
|
console.info(`[Server] Process CWD: ${process.cwd()}`);
|
|
739
|
+
console.info(`[Server] Resolved workspace: ${config.searchDirectory} (via ${config.workspaceResolution?.source || 'unknown'})`);
|
|
336
740
|
} catch (_e) {
|
|
337
|
-
|
|
741
|
+
|
|
742
|
+
|
|
338
743
|
}
|
|
339
744
|
|
|
340
745
|
let stopStartupMemory = null;
|
|
@@ -343,7 +748,8 @@ async function initialize(workspaceDir) {
|
|
|
343
748
|
stopStartupMemory = startMemoryLogger('[Server] Memory (startup)', MEMORY_LOG_INTERVAL_MS);
|
|
344
749
|
}
|
|
345
750
|
|
|
346
|
-
|
|
751
|
+
|
|
752
|
+
|
|
347
753
|
try {
|
|
348
754
|
await fs.access(config.searchDirectory);
|
|
349
755
|
} catch {
|
|
@@ -351,7 +757,8 @@ async function initialize(workspaceDir) {
|
|
|
351
757
|
process.exit(1);
|
|
352
758
|
}
|
|
353
759
|
|
|
354
|
-
|
|
760
|
+
|
|
761
|
+
|
|
355
762
|
console.info('[Server] Initializing features...');
|
|
356
763
|
let cachedEmbedderPromise = null;
|
|
357
764
|
const lazyEmbedder = async (...args) => {
|
|
@@ -384,7 +791,7 @@ async function initialize(workspaceDir) {
|
|
|
384
791
|
return model(...args);
|
|
385
792
|
};
|
|
386
793
|
|
|
387
|
-
|
|
794
|
+
|
|
388
795
|
const unloader = async () => {
|
|
389
796
|
if (!cachedEmbedderPromise) return false;
|
|
390
797
|
try {
|
|
@@ -409,7 +816,7 @@ async function initialize(workspaceDir) {
|
|
|
409
816
|
};
|
|
410
817
|
|
|
411
818
|
embedder = lazyEmbedder;
|
|
412
|
-
unloadMainEmbedder = unloader;
|
|
819
|
+
unloadMainEmbedder = unloader;
|
|
413
820
|
const preloadEmbeddingModel = async () => {
|
|
414
821
|
if (config.preloadEmbeddingModel === false) return;
|
|
415
822
|
try {
|
|
@@ -420,29 +827,29 @@ async function initialize(workspaceDir) {
|
|
|
420
827
|
}
|
|
421
828
|
};
|
|
422
829
|
|
|
423
|
-
|
|
424
|
-
|
|
830
|
+
|
|
831
|
+
|
|
425
832
|
|
|
426
|
-
|
|
833
|
+
|
|
427
834
|
cache = new EmbeddingsCache(config);
|
|
428
835
|
console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
|
|
429
836
|
|
|
430
|
-
|
|
837
|
+
|
|
431
838
|
indexer = new CodebaseIndexer(embedder, cache, config, server);
|
|
432
839
|
hybridSearch = new HybridSearch(embedder, cache, config);
|
|
433
840
|
const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config, indexer);
|
|
434
841
|
const findSimilarCode = new FindSimilarCodeFeature.FindSimilarCode(embedder, cache, config);
|
|
435
842
|
const annConfig = new AnnConfigFeature.AnnConfigTool(cache, config);
|
|
436
843
|
|
|
437
|
-
|
|
844
|
+
|
|
438
845
|
features[0].instance = hybridSearch;
|
|
439
846
|
features[1].instance = indexer;
|
|
440
847
|
features[2].instance = cacheClearer;
|
|
441
848
|
features[3].instance = findSimilarCode;
|
|
442
849
|
features[4].instance = annConfig;
|
|
443
|
-
|
|
850
|
+
|
|
444
851
|
|
|
445
|
-
|
|
852
|
+
|
|
446
853
|
const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
|
|
447
854
|
config,
|
|
448
855
|
cache,
|
|
@@ -453,34 +860,99 @@ async function initialize(workspaceDir) {
|
|
|
453
860
|
features[6].instance = setWorkspaceInstance;
|
|
454
861
|
features[6].handler = SetWorkspaceFeature.createHandleToolCall(setWorkspaceInstance);
|
|
455
862
|
|
|
456
|
-
|
|
457
|
-
server.hybridSearch = hybridSearch;
|
|
458
|
-
|
|
459
|
-
const startBackgroundTasks = async () => {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
863
|
+
|
|
864
|
+
server.hybridSearch = hybridSearch;
|
|
865
|
+
|
|
866
|
+
const startBackgroundTasks = async () => {
|
|
867
|
+
const stopStartupMemoryLogger = () => {
|
|
868
|
+
if (stopStartupMemory) {
|
|
869
|
+
stopStartupMemory();
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
const tryAutoAttachWorkspaceCache = async (reason) => {
|
|
873
|
+
const candidate = await findAutoAttachWorkspaceCandidate({
|
|
874
|
+
excludeCacheDirectory: config.cacheDirectory,
|
|
875
|
+
});
|
|
876
|
+
if (!candidate) {
|
|
877
|
+
console.warn(
|
|
878
|
+
`[Server] Auto-attach skipped (${reason}): no unambiguous workspace cache candidate found.`
|
|
879
|
+
);
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
config.searchDirectory = candidate.workspace;
|
|
884
|
+
config.cacheDirectory = candidate.cacheDirectory;
|
|
885
|
+
await fs.mkdir(config.cacheDirectory, { recursive: true });
|
|
886
|
+
await cache.load();
|
|
887
|
+
console.info(
|
|
888
|
+
`[Server] Auto-attached workspace cache (${reason}): ${candidate.workspace} via ${candidate.source}`
|
|
889
|
+
);
|
|
890
|
+
if (config.verbose) {
|
|
891
|
+
logMemory('[Server] Memory (after cache load)');
|
|
892
|
+
}
|
|
893
|
+
return true;
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const resolutionSource = config.workspaceResolution?.source || 'unknown';
|
|
897
|
+
const isSystemFallback =
|
|
898
|
+
(resolutionSource === 'cwd' || resolutionSource === 'cwd-root-search') &&
|
|
899
|
+
isNonProjectDirectory(config.searchDirectory);
|
|
900
|
+
|
|
901
|
+
if (isSystemFallback) {
|
|
902
|
+
try {
|
|
903
|
+
console.warn(
|
|
904
|
+
`[Server] Detected system fallback workspace: ${config.searchDirectory}. Attempting cache auto-attach.`
|
|
905
|
+
);
|
|
906
|
+
const attached = await tryAutoAttachWorkspaceCache('system-fallback');
|
|
907
|
+
if (!attached) {
|
|
908
|
+
console.warn(
|
|
909
|
+
'[Server] Waiting for a proper workspace root (MCP roots, env vars, or f_set_workspace).'
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
} finally {
|
|
913
|
+
stopStartupMemoryLogger();
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (!workspaceLockAcquired) {
|
|
919
|
+
try {
|
|
920
|
+
console.info('[Server] Secondary instance detected; loading cache in read-only mode.');
|
|
921
|
+
await cache.load();
|
|
922
|
+
if (cache.getStoreSize() === 0) {
|
|
923
|
+
await tryAutoAttachWorkspaceCache('secondary-empty-cache');
|
|
924
|
+
}
|
|
925
|
+
if (config.verbose) {
|
|
926
|
+
logMemory('[Server] Memory (after cache load)');
|
|
927
|
+
}
|
|
928
|
+
} finally {
|
|
929
|
+
stopStartupMemoryLogger();
|
|
930
|
+
}
|
|
931
|
+
console.info('[Server] Secondary instance ready; skipping background indexing.');
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
void preloadEmbeddingModel();
|
|
936
|
+
|
|
463
937
|
try {
|
|
464
938
|
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)
|
|
939
|
+
await cache.load();
|
|
940
|
+
if (config.verbose) {
|
|
941
|
+
logMemory('[Server] Memory (after cache load)');
|
|
942
|
+
}
|
|
943
|
+
} finally {
|
|
944
|
+
stopStartupMemoryLogger();
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
|
|
476
948
|
console.info('[Server] Starting background indexing (delayed)...');
|
|
477
949
|
|
|
478
|
-
|
|
950
|
+
|
|
479
951
|
setTimeout(() => {
|
|
480
952
|
indexer
|
|
481
953
|
.indexAll()
|
|
482
954
|
.then(() => {
|
|
483
|
-
|
|
955
|
+
|
|
484
956
|
if (config.watchFiles) {
|
|
485
957
|
indexer.setupFileWatcher();
|
|
486
958
|
}
|
|
@@ -494,7 +966,7 @@ async function initialize(workspaceDir) {
|
|
|
494
966
|
return { startBackgroundTasks, config };
|
|
495
967
|
}
|
|
496
968
|
|
|
497
|
-
|
|
969
|
+
|
|
498
970
|
const server = new Server(
|
|
499
971
|
{
|
|
500
972
|
name: 'heuristic-mcp',
|
|
@@ -508,19 +980,46 @@ const server = new Server(
|
|
|
508
980
|
}
|
|
509
981
|
);
|
|
510
982
|
|
|
511
|
-
// Handle resources/list
|
|
512
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
513
|
-
return await handleListResources(config);
|
|
514
|
-
});
|
|
515
983
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
984
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
985
|
+
console.info('[Server] Received roots/list_changed notification from client.');
|
|
986
|
+
const newRoot = await detectWorkspaceFromRoots();
|
|
987
|
+
if (newRoot) {
|
|
988
|
+
await maybeAutoSwitchWorkspaceToPath(newRoot, {
|
|
989
|
+
source: 'roots changed',
|
|
990
|
+
reindex: true,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
998
|
+
await configReadyPromise;
|
|
999
|
+
if (configInitError || !config) {
|
|
1000
|
+
throw configInitError ?? new Error('Server configuration is not initialized');
|
|
1001
|
+
}
|
|
1002
|
+
return await handleListResources(config);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1008
|
+
await configReadyPromise;
|
|
1009
|
+
if (configInitError || !config) {
|
|
1010
|
+
throw configInitError ?? new Error('Server configuration is not initialized');
|
|
1011
|
+
}
|
|
1012
|
+
return await handleReadResource(request.params.uri, config);
|
|
1013
|
+
});
|
|
520
1014
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1018
|
+
await configReadyPromise;
|
|
1019
|
+
if (configInitError || !config) {
|
|
1020
|
+
throw configInitError ?? new Error('Server configuration is not initialized');
|
|
1021
|
+
}
|
|
1022
|
+
const tools = [];
|
|
524
1023
|
|
|
525
1024
|
for (const feature of features) {
|
|
526
1025
|
const toolDef = feature.module.getToolDefinition(config);
|
|
@@ -530,15 +1029,142 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
530
1029
|
return { tools };
|
|
531
1030
|
});
|
|
532
1031
|
|
|
533
|
-
// Handle tool calls
|
|
534
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
535
|
-
await maybeAutoSwitchWorkspace(request);
|
|
536
1032
|
|
|
537
|
-
|
|
1033
|
+
|
|
1034
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1035
|
+
await configReadyPromise;
|
|
1036
|
+
if (configInitError || !config) {
|
|
1037
|
+
return {
|
|
1038
|
+
content: [
|
|
1039
|
+
{
|
|
1040
|
+
type: 'text',
|
|
1041
|
+
text: `Server initialization failed: ${configInitError?.message || 'configuration not available'}`,
|
|
1042
|
+
},
|
|
1043
|
+
],
|
|
1044
|
+
isError: true,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (!workspaceLockAcquired && request.params?.name === 'f_set_workspace') {
|
|
1049
|
+
const args = request.params?.arguments || {};
|
|
1050
|
+
const workspacePath = args.workspacePath;
|
|
1051
|
+
const reindex = args.reindex !== false;
|
|
1052
|
+
if (typeof workspacePath !== 'string' || workspacePath.trim().length === 0) {
|
|
1053
|
+
return {
|
|
1054
|
+
content: [{ type: 'text', text: 'Error: workspacePath is required.' }],
|
|
1055
|
+
isError: true,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (reindex) {
|
|
1059
|
+
return {
|
|
1060
|
+
content: [
|
|
1061
|
+
{
|
|
1062
|
+
type: 'text',
|
|
1063
|
+
text: 'This server instance is in secondary read-only mode. Set reindex=false to attach cache only.',
|
|
1064
|
+
},
|
|
1065
|
+
],
|
|
1066
|
+
isError: true,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const normalizedPath = path.resolve(workspacePath);
|
|
1070
|
+
try {
|
|
1071
|
+
const stats = await fs.stat(normalizedPath);
|
|
1072
|
+
if (!stats.isDirectory()) {
|
|
1073
|
+
return {
|
|
1074
|
+
content: [{ type: 'text', text: `Error: Path is not a directory: ${normalizedPath}` }],
|
|
1075
|
+
isError: true,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
return {
|
|
1080
|
+
content: [
|
|
1081
|
+
{
|
|
1082
|
+
type: 'text',
|
|
1083
|
+
text: `Error: Cannot access directory ${normalizedPath}: ${err.message}`,
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1086
|
+
isError: true,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
config.searchDirectory = normalizedPath;
|
|
1091
|
+
config.cacheDirectory = getWorkspaceCachePath(normalizedPath, getGlobalCacheDir());
|
|
1092
|
+
try {
|
|
1093
|
+
await fs.mkdir(config.cacheDirectory, { recursive: true });
|
|
1094
|
+
await cache.load();
|
|
1095
|
+
trustWorkspacePath(normalizedPath);
|
|
1096
|
+
return {
|
|
1097
|
+
content: [
|
|
1098
|
+
{
|
|
1099
|
+
type: 'text',
|
|
1100
|
+
text: `Attached in read-only mode to workspace cache: ${normalizedPath}`,
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
};
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
return {
|
|
1106
|
+
content: [
|
|
1107
|
+
{
|
|
1108
|
+
type: 'text',
|
|
1109
|
+
text: `Error: Failed to attach cache for ${normalizedPath}: ${err.message}`,
|
|
1110
|
+
},
|
|
1111
|
+
],
|
|
1112
|
+
isError: true,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (
|
|
1118
|
+
!workspaceLockAcquired &&
|
|
1119
|
+
['b_index_codebase', 'c_clear_cache'].includes(request.params?.name)
|
|
1120
|
+
) {
|
|
1121
|
+
return {
|
|
1122
|
+
content: [
|
|
1123
|
+
{
|
|
1124
|
+
type: 'text',
|
|
1125
|
+
text: 'This server instance is in secondary read-only mode. Use the primary instance for indexing/cache mutation tools.',
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
isError: true,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
const detectedFromRoots = await maybeAutoSwitchWorkspaceFromRoots(request);
|
|
1132
|
+
const detectedFromEnv = await maybeAutoSwitchWorkspace(request);
|
|
1133
|
+
if (detectedFromRoots) {
|
|
1134
|
+
trustWorkspacePath(detectedFromRoots);
|
|
1135
|
+
}
|
|
1136
|
+
if (detectedFromEnv) {
|
|
1137
|
+
trustWorkspacePath(detectedFromEnv);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const toolName = request.params?.name;
|
|
1141
|
+
if (
|
|
1142
|
+
config.requireTrustedWorkspaceSignalForTools === true &&
|
|
1143
|
+
shouldRequireTrustedWorkspaceSignalForTool(toolName) &&
|
|
1144
|
+
!detectedFromRoots &&
|
|
1145
|
+
!detectedFromEnv &&
|
|
1146
|
+
!isCurrentWorkspaceTrusted()
|
|
1147
|
+
) {
|
|
1148
|
+
return {
|
|
1149
|
+
content: [
|
|
1150
|
+
{
|
|
1151
|
+
type: 'text',
|
|
1152
|
+
text:
|
|
1153
|
+
`Workspace context appears stale for "${toolName}" (current: "${config.searchDirectory}"). ` +
|
|
1154
|
+
'Please reload your IDE window and retry. ' +
|
|
1155
|
+
'If needed, call MCP tool "f_set_workspace" from your chat/client with your opened folder path.',
|
|
1156
|
+
},
|
|
1157
|
+
],
|
|
1158
|
+
isError: true,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
for (const feature of features) {
|
|
538
1163
|
const toolDef = feature.module.getToolDefinition(config);
|
|
539
1164
|
|
|
540
1165
|
if (request.params.name === toolDef.name) {
|
|
541
|
-
|
|
1166
|
+
|
|
1167
|
+
|
|
542
1168
|
if (typeof feature.handler !== 'function') {
|
|
543
1169
|
return {
|
|
544
1170
|
content: [{
|
|
@@ -547,14 +1173,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
547
1173
|
}],
|
|
548
1174
|
isError: true,
|
|
549
1175
|
};
|
|
550
|
-
}
|
|
551
|
-
const result = await feature.handler(request, feature.instance);
|
|
1176
|
+
}
|
|
1177
|
+
const result = await feature.handler(request, feature.instance);
|
|
1178
|
+
if (toolDef.name === 'f_set_workspace' && !isToolResponseError(result)) {
|
|
1179
|
+
trustWorkspacePath(config.searchDirectory);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
|
|
552
1185
|
|
|
553
|
-
// Unload embedding model after search-related tools to free memory
|
|
554
|
-
// Tools that use embedder: a_semantic_search, d_find_similar_code
|
|
555
1186
|
const searchTools = ['a_semantic_search', 'd_find_similar_code'];
|
|
556
1187
|
if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
|
|
557
|
-
|
|
1188
|
+
|
|
1189
|
+
|
|
558
1190
|
setImmediate(async () => {
|
|
559
1191
|
if (typeof unloadMainEmbedder === 'function') {
|
|
560
1192
|
await unloadMainEmbedder();
|
|
@@ -577,8 +1209,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
577
1209
|
};
|
|
578
1210
|
});
|
|
579
1211
|
|
|
580
|
-
|
|
581
|
-
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
export async function main(argv = process.argv) {
|
|
582
1215
|
const parsed = parseArgs(argv);
|
|
583
1216
|
const {
|
|
584
1217
|
isServerMode,
|
|
@@ -600,17 +1233,24 @@ export async function main(argv = process.argv) {
|
|
|
600
1233
|
unknownFlags,
|
|
601
1234
|
} = parsed;
|
|
602
1235
|
|
|
603
|
-
let shutdownRequested = false;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
shutdownRequested
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1236
|
+
let shutdownRequested = false;
|
|
1237
|
+
let shutdownReason = 'natural';
|
|
1238
|
+
const requestShutdown = (reason) => {
|
|
1239
|
+
if (shutdownRequested) return;
|
|
1240
|
+
shutdownRequested = true;
|
|
1241
|
+
shutdownReason = String(reason || 'unknown');
|
|
1242
|
+
console.info(`[Server] Shutdown requested (${reason}).`);
|
|
1243
|
+
void gracefulShutdown(reason);
|
|
1244
|
+
};
|
|
1245
|
+
registerProcessDiagnostics({
|
|
1246
|
+
isServerMode,
|
|
1247
|
+
requestShutdown,
|
|
1248
|
+
getShutdownReason: () => shutdownReason,
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
if (isServerMode && !(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test')) {
|
|
1252
|
+
enableStderrOnlyLogging();
|
|
1253
|
+
}
|
|
614
1254
|
if (wantsVersion) {
|
|
615
1255
|
console.info(packageJson.version);
|
|
616
1256
|
process.exit(0);
|
|
@@ -641,19 +1281,23 @@ export async function main(argv = process.argv) {
|
|
|
641
1281
|
process.exit(0);
|
|
642
1282
|
}
|
|
643
1283
|
|
|
644
|
-
|
|
1284
|
+
|
|
1285
|
+
|
|
645
1286
|
if (wantsCache) {
|
|
646
1287
|
await status({ fix: wantsClean, cacheOnly: true, workspaceDir });
|
|
647
1288
|
process.exit(0);
|
|
648
1289
|
}
|
|
649
1290
|
|
|
650
|
-
|
|
1291
|
+
|
|
1292
|
+
|
|
651
1293
|
const clearIndex = parsed.rawArgs.indexOf('--clear');
|
|
652
1294
|
if (clearIndex !== -1) {
|
|
653
1295
|
const cacheId = parsed.rawArgs[clearIndex + 1];
|
|
654
1296
|
if (cacheId && !cacheId.startsWith('--')) {
|
|
655
|
-
|
|
656
|
-
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
|
|
657
1301
|
let cacheHome;
|
|
658
1302
|
if (process.platform === 'win32') {
|
|
659
1303
|
cacheHome = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
@@ -697,7 +1341,8 @@ export async function main(argv = process.argv) {
|
|
|
697
1341
|
}
|
|
698
1342
|
process.exit(0);
|
|
699
1343
|
}
|
|
700
|
-
|
|
1344
|
+
|
|
1345
|
+
|
|
701
1346
|
}
|
|
702
1347
|
|
|
703
1348
|
if (wantsClearCache) {
|
|
@@ -740,34 +1385,89 @@ export async function main(argv = process.argv) {
|
|
|
740
1385
|
}
|
|
741
1386
|
|
|
742
1387
|
registerSignalHandlers(requestShutdown);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const { startBackgroundTasks } = await initialize(workspaceDir);
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
|
|
748
1392
|
|
|
749
|
-
|
|
1393
|
+
|
|
1394
|
+
const detectedRootPromise = new Promise((resolve) => {
|
|
1395
|
+
const HANDSHAKE_TIMEOUT_MS = 1000;
|
|
1396
|
+
let settled = false;
|
|
1397
|
+
const resolveOnce = (value) => {
|
|
1398
|
+
if (settled) return;
|
|
1399
|
+
settled = true;
|
|
1400
|
+
resolve(value);
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
const timer = setTimeout(() => {
|
|
1404
|
+
console.warn(`[Server] MCP handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms, proceeding without roots.`);
|
|
1405
|
+
resolveOnce(null);
|
|
1406
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
1407
|
+
|
|
1408
|
+
server.oninitialized = async () => {
|
|
1409
|
+
clearTimeout(timer);
|
|
1410
|
+
console.info('[Server] MCP handshake complete.');
|
|
1411
|
+
const root = await detectWorkspaceFromRoots();
|
|
1412
|
+
resolveOnce(root);
|
|
1413
|
+
};
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
const transport = new StdioServerTransport();
|
|
1417
|
+
await server.connect(transport);
|
|
1418
|
+
console.info('[Server] MCP transport connected.');
|
|
1419
|
+
if (isServerMode) {
|
|
1420
|
+
process.stdin?.on?.('end', () => requestShutdown('stdin-end'));
|
|
1421
|
+
process.stdin?.on?.('close', () => requestShutdown('stdin-close'));
|
|
1422
|
+
process.stdout?.on?.('error', (err) => {
|
|
1423
|
+
if (err?.code === 'EPIPE') {
|
|
1424
|
+
requestShutdown('stdout-epipe');
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
const detectedRoot = await detectedRootPromise;
|
|
750
1432
|
|
|
751
|
-
|
|
752
|
-
|
|
1433
|
+
|
|
1434
|
+
const effectiveWorkspace = detectedRoot || workspaceDir;
|
|
1435
|
+
if (detectedRoot) {
|
|
1436
|
+
console.info(`[Server] Using workspace from MCP roots: ${detectedRoot}`);
|
|
1437
|
+
}
|
|
1438
|
+
const initPromise = initialize(effectiveWorkspace);
|
|
1439
|
+
const initWithResolve = initPromise
|
|
1440
|
+
.then((result) => {
|
|
1441
|
+
configReadyResolve();
|
|
1442
|
+
return result;
|
|
1443
|
+
})
|
|
1444
|
+
.catch((err) => {
|
|
1445
|
+
configInitError = err;
|
|
1446
|
+
configReadyResolve();
|
|
1447
|
+
throw err;
|
|
1448
|
+
});
|
|
1449
|
+
const { startBackgroundTasks } = await initWithResolve;
|
|
753
1450
|
|
|
754
|
-
console.info('[Server] MCP transport connected.');
|
|
755
1451
|
console.info('[Server] Heuristic MCP server started.');
|
|
756
1452
|
|
|
757
|
-
|
|
1453
|
+
|
|
1454
|
+
|
|
758
1455
|
void startBackgroundTasks().catch((err) => {
|
|
759
1456
|
console.error(`[Server] Background task error: ${err.message}`);
|
|
760
1457
|
});
|
|
761
1458
|
console.info('[Server] MCP server is now fully ready to accept requests.');
|
|
762
1459
|
}
|
|
763
1460
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
async function gracefulShutdown(signal) {
|
|
1464
|
+
console.info(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
1465
|
+
const exitCode = isCrashShutdownReason(signal) ? 1 : 0;
|
|
767
1466
|
|
|
768
1467
|
const cleanupTasks = [];
|
|
769
1468
|
|
|
770
|
-
|
|
1469
|
+
|
|
1470
|
+
|
|
771
1471
|
if (indexer && indexer.watcher) {
|
|
772
1472
|
cleanupTasks.push(
|
|
773
1473
|
indexer.watcher
|
|
@@ -777,7 +1477,8 @@ async function gracefulShutdown(signal) {
|
|
|
777
1477
|
);
|
|
778
1478
|
}
|
|
779
1479
|
|
|
780
|
-
|
|
1480
|
+
|
|
1481
|
+
|
|
781
1482
|
if (indexer && indexer.terminateWorkers) {
|
|
782
1483
|
cleanupTasks.push(
|
|
783
1484
|
(async () => {
|
|
@@ -788,22 +1489,29 @@ async function gracefulShutdown(signal) {
|
|
|
788
1489
|
);
|
|
789
1490
|
}
|
|
790
1491
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
if (cache) {
|
|
1495
|
+
if (!workspaceLockAcquired) {
|
|
1496
|
+
console.info('[Server] Secondary/fallback mode: skipping cache save.');
|
|
1497
|
+
} else {
|
|
1498
|
+
cleanupTasks.push(
|
|
1499
|
+
cache
|
|
1500
|
+
.save()
|
|
1501
|
+
.then(() => console.info('[Server] Cache saved'))
|
|
1502
|
+
.catch((err) => console.error(`[Server] Failed to save cache: ${err.message}`))
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
await Promise.allSettled(cleanupTasks);
|
|
1508
|
+
console.info('[Server] Goodbye!');
|
|
1509
|
+
await flushLogs({ close: true, timeoutMs: 1500 }).catch(() => {});
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
setTimeout(() => process.exit(exitCode), 100);
|
|
1514
|
+
}
|
|
807
1515
|
|
|
808
1516
|
const isMain =
|
|
809
1517
|
process.argv[1] &&
|
|
@@ -813,6 +1521,10 @@ const isMain =
|
|
|
813
1521
|
path.basename(process.argv[1]) === 'index.js') &&
|
|
814
1522
|
!(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
|
|
815
1523
|
|
|
816
|
-
if (isMain) {
|
|
817
|
-
main().catch(
|
|
818
|
-
|
|
1524
|
+
if (isMain) {
|
|
1525
|
+
main().catch(async (err) => {
|
|
1526
|
+
console.error(err);
|
|
1527
|
+
await flushLogs({ close: true, timeoutMs: 500 }).catch(() => {});
|
|
1528
|
+
process.exit(1);
|
|
1529
|
+
});
|
|
1530
|
+
}
|