@softerist/heuristic-mcp 3.0.13 → 3.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -73
- package/index.js +155 -88
- package/lib/cli.js +1 -1
- package/lib/config.js +109 -49
- package/lib/constants.js +39 -8
- package/lib/settings-editor.js +123 -7
- package/lib/workspace-env.js +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,14 +76,20 @@ heuristic-mcp --version
|
|
|
76
76
|
### Start/Stop
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
heuristic-mcp --start
|
|
80
|
-
heuristic-mcp --start antigravity
|
|
81
|
-
heuristic-mcp --start
|
|
82
|
-
heuristic-mcp --start
|
|
83
|
-
heuristic-mcp --
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
heuristic-mcp --start
|
|
80
|
+
heuristic-mcp --start antigravity
|
|
81
|
+
heuristic-mcp --start codex
|
|
82
|
+
heuristic-mcp --start cursor
|
|
83
|
+
heuristic-mcp --start vscode
|
|
84
|
+
heuristic-mcp --start windsurf
|
|
85
|
+
heuristic-mcp --start warp
|
|
86
|
+
heuristic-mcp --start "Claude Desktop"
|
|
87
|
+
heuristic-mcp --stop
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`--start` registers (if needed) and enables the MCP server entry. `--stop` disables it so the IDE won't immediately respawn it. Restart/reload the IDE after `--start` to launch.
|
|
91
|
+
|
|
92
|
+
Warp note: this package now targets `~/.warp/mcp_settings.json` (and `%APPDATA%\\Warp\\mcp_settings.json` on Windows when present). If no local Warp MCP config is writable yet, use Warp MCP settings/UI once to initialize it, then re-run `--start warp`.
|
|
87
93
|
|
|
88
94
|
### Clear Cache
|
|
89
95
|
|
|
@@ -97,33 +103,50 @@ Clears the cache for the current working directory (or `--workspace` if provided
|
|
|
97
103
|
|
|
98
104
|
## Configuration (`config.jsonc`)
|
|
99
105
|
|
|
100
|
-
Configuration is loaded from your workspace root when the server runs with `--workspace
|
|
101
|
-
|
|
102
|
-
Example `config.jsonc`:
|
|
103
|
-
|
|
104
|
-
```json
|
|
105
|
-
{
|
|
106
|
-
"excludePatterns": ["**/legacy-code/**", "**/*.test.ts"],
|
|
107
|
-
"fileNames": ["Dockerfile", ".env.example", "Makefile"],
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
106
|
+
Configuration is loaded from your workspace root when the server runs with `--workspace`. If not provided by the IDE, the server auto-detects workspace via environment variables and current working directory. In server mode, it falls back to the package `config.jsonc` (or `config.json`) and then your current working directory.
|
|
107
|
+
|
|
108
|
+
Example `config.jsonc`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"excludePatterns": ["**/legacy-code/**", "**/*.test.ts"],
|
|
113
|
+
"fileNames": ["Dockerfile", ".env.example", "Makefile"],
|
|
114
|
+
"indexing": {
|
|
115
|
+
"smartIndexing": true
|
|
116
|
+
},
|
|
117
|
+
"worker": {
|
|
118
|
+
"workerThreads": 0
|
|
119
|
+
},
|
|
120
|
+
"embedding": {
|
|
121
|
+
"embeddingModel": "jinaai/jina-embeddings-v2-base-code",
|
|
122
|
+
"embeddingBatchSize": null,
|
|
123
|
+
"embeddingProcessNumThreads": 8
|
|
124
|
+
},
|
|
125
|
+
"search": {
|
|
126
|
+
"recencyBoost": 0.1,
|
|
127
|
+
"recencyDecayDays": 30
|
|
128
|
+
},
|
|
129
|
+
"callGraph": {
|
|
130
|
+
"callGraphEnabled": true,
|
|
131
|
+
"callGraphBoost": 0.15
|
|
132
|
+
},
|
|
133
|
+
"ann": {
|
|
134
|
+
"annEnabled": true
|
|
135
|
+
},
|
|
136
|
+
"vectorStore": {
|
|
137
|
+
"vectorStoreFormat": "binary",
|
|
138
|
+
"vectorStoreContentMode": "external",
|
|
139
|
+
"vectorStoreLoadMode": "disk",
|
|
140
|
+
"contentCacheEntries": 256,
|
|
141
|
+
"vectorCacheEntries": 64
|
|
142
|
+
},
|
|
143
|
+
"memoryCleanup": {
|
|
144
|
+
"clearCacheAfterIndex": true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Preferred style is namespaced keys (shown above). Legacy top-level keys are still supported for backward compatibility.
|
|
127
150
|
|
|
128
151
|
### Embedding Model & Dimension Options
|
|
129
152
|
|
|
@@ -133,12 +156,14 @@ Example `config.jsonc`:
|
|
|
133
156
|
|
|
134
157
|
For faster search with smaller embeddings, switch to an MRL-compatible model:
|
|
135
158
|
|
|
136
|
-
```json
|
|
137
|
-
{
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"embedding": {
|
|
162
|
+
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
|
163
|
+
"embeddingDimension": 128
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
142
167
|
|
|
143
168
|
**MRL-compatible models:**
|
|
144
169
|
- `nomic-ai/nomic-embed-text-v1.5` — recommended for 128d/256d
|
|
@@ -153,7 +178,9 @@ Cache location:
|
|
|
153
178
|
|
|
154
179
|
### Environment Variables
|
|
155
180
|
|
|
156
|
-
Selected overrides (prefix `SMART_CODING_`):
|
|
181
|
+
Selected overrides (prefix `SMART_CODING_`):
|
|
182
|
+
|
|
183
|
+
Environment overrides target runtime keys and are synced back into namespaces by `lib/config.js`.
|
|
157
184
|
|
|
158
185
|
- `SMART_CODING_VERBOSE=true|false` — enable detailed logging.
|
|
159
186
|
- `SMART_CODING_WORKER_THREADS=auto|N` — worker thread count.
|
|
@@ -179,35 +206,37 @@ Selected overrides (prefix `SMART_CODING_`):
|
|
|
179
206
|
|
|
180
207
|
See `lib/config.js` for the full list.
|
|
181
208
|
|
|
182
|
-
### Binary Vector Store
|
|
183
|
-
|
|
184
|
-
Set `vectorStoreFormat` to `binary` to use the on-disk binary cache. This keeps vectors and content out of JS heap
|
|
185
|
-
and reads on demand. Recommended for large repos.
|
|
186
|
-
|
|
187
|
-
- `vectorStoreContentMode=external` keeps content in the binary file and only loads for top-N results.
|
|
188
|
-
- `contentCacheEntries` controls the small in-memory LRU for decoded content strings.
|
|
189
|
-
- `vectorStoreLoadMode=disk` streams vectors from disk to reduce memory usage.
|
|
190
|
-
- `vectorCacheEntries` controls the small in-memory LRU for vectors when using disk mode.
|
|
191
|
-
- `clearCacheAfterIndex=true` drops in-memory vectors after indexing and reloads lazily on next query.
|
|
192
|
-
- `unloadModelAfterIndex=true` (default) unloads the embedding model after indexing to free ~500MB-1GB of RAM; the model will reload on the next search query.
|
|
193
|
-
- Note: `annEnabled=true` with `vectorStoreLoadMode=disk` can increase disk reads during ANN rebuilds on large indexes.
|
|
209
|
+
### Binary Vector Store
|
|
210
|
+
|
|
211
|
+
Set `vectorStore.vectorStoreFormat` to `binary` to use the on-disk binary cache. This keeps vectors and content out of JS heap
|
|
212
|
+
and reads on demand. Recommended for large repos.
|
|
213
|
+
|
|
214
|
+
- `vectorStore.vectorStoreContentMode=external` keeps content in the binary file and only loads for top-N results.
|
|
215
|
+
- `vectorStore.contentCacheEntries` controls the small in-memory LRU for decoded content strings.
|
|
216
|
+
- `vectorStore.vectorStoreLoadMode=disk` streams vectors from disk to reduce memory usage.
|
|
217
|
+
- `vectorStore.vectorCacheEntries` controls the small in-memory LRU for vectors when using disk mode.
|
|
218
|
+
- `memoryCleanup.clearCacheAfterIndex=true` drops in-memory vectors after indexing and reloads lazily on next query.
|
|
219
|
+
- `memoryCleanup.unloadModelAfterIndex=true` (default) unloads the embedding model after indexing to free ~500MB-1GB of RAM; the model will reload on the next search query.
|
|
220
|
+
- Note: `ann.annEnabled=true` with `vectorStore.vectorStoreLoadMode=disk` can increase disk reads during ANN rebuilds on large indexes.
|
|
194
221
|
|
|
195
222
|
### SQLite Vector Store
|
|
196
223
|
|
|
197
|
-
Set `vectorStoreFormat` to `sqlite` to use SQLite for persistence. This provides:
|
|
224
|
+
Set `vectorStore.vectorStoreFormat` to `sqlite` to use SQLite for persistence. This provides:
|
|
198
225
|
|
|
199
226
|
- ACID transactions for reliable writes
|
|
200
227
|
- Simpler concurrent access
|
|
201
228
|
- Standard database format for inspection
|
|
202
229
|
|
|
203
|
-
```json
|
|
204
|
-
{
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"vectorStore": {
|
|
233
|
+
"vectorStoreFormat": "sqlite"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The vectors and content are stored in `vectors.sqlite` in your cache directory. You can inspect it with any SQLite browser.
|
|
239
|
+
`vectorStore.vectorStoreContentMode` and `vectorStore.vectorStoreLoadMode` are respected for SQLite (use `vectorStore.vectorStoreLoadMode=disk` to avoid loading vectors into memory).
|
|
211
240
|
|
|
212
241
|
**Tradeoffs vs Binary:**
|
|
213
242
|
- Slightly higher read overhead (SQL queries vs direct memory access)
|
|
@@ -230,7 +259,7 @@ SMART_CODING_VECTOR_STORE_LOAD_MODE=disk node tools/scripts/benchmark-search.js
|
|
|
230
259
|
SMART_CODING_VECTOR_STORE_FORMAT=binary SMART_CODING_VECTOR_STORE_LOAD_MODE=disk node tools/scripts/benchmark-search.js --runs 10
|
|
231
260
|
```
|
|
232
261
|
|
|
233
|
-
Note: On small repos, disk mode may be slightly slower and show noisy RSS deltas; benefits are clearer on large indexes with a small `vectorCacheEntries`.
|
|
262
|
+
Note: On small repos, disk mode may be slightly slower and show noisy RSS deltas; benefits are clearer on large indexes with a small `vectorStore.vectorCacheEntries`.
|
|
234
263
|
|
|
235
264
|
---
|
|
236
265
|
|
|
@@ -268,12 +297,14 @@ Fetch the latest version of a package from its official registry.
|
|
|
268
297
|
- **Homebrew**: `brew:node`
|
|
269
298
|
- **Conda**: `conda:numpy`
|
|
270
299
|
|
|
271
|
-
### `f_set_workspace`
|
|
272
|
-
Change the workspace directory at runtime. Updates search directory, cache location, and optionally triggers reindex.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
300
|
+
### `f_set_workspace`
|
|
301
|
+
Change the workspace directory at runtime. Updates search directory, cache location, and optionally triggers reindex.
|
|
302
|
+
|
|
303
|
+
The server also attempts this automatically before each tool call when it detects a new workspace path from environment variables (for example `CODEX_WORKSPACE`, `CODEX_PROJECT_ROOT`, `WORKSPACE_FOLDER`).
|
|
304
|
+
|
|
305
|
+
**Parameters:**
|
|
306
|
+
- `workspacePath` (required): Absolute path to the new workspace
|
|
307
|
+
- `reindex` (optional, default: `true`): Whether to trigger a full reindex
|
|
277
308
|
|
|
278
309
|
---
|
|
279
310
|
|
|
@@ -293,8 +324,8 @@ Native ONNX backend unavailable: The operating system cannot run %1.
|
|
|
293
324
|
...onnxruntime_binding.node. Falling back to WASM.
|
|
294
325
|
```
|
|
295
326
|
|
|
296
|
-
The server will automatically disable workers and force `embeddingProcessPerBatch` to reduce memory spikes, but you
|
|
297
|
-
should fix the native binding to restore stable memory usage:
|
|
327
|
+
The server will automatically disable workers and force `embedding.embeddingProcessPerBatch` to reduce memory spikes, but you
|
|
328
|
+
should fix the native binding to restore stable memory usage:
|
|
298
329
|
|
|
299
330
|
- Ensure you are running **64-bit Node.js** (`node -p "process.arch"` should be `x64`).
|
|
300
331
|
- Install **Microsoft Visual C++ 2015–2022 Redistributable (x64)**.
|
|
@@ -334,7 +365,7 @@ node tools/scripts/cache-stats.js --workspace <path>
|
|
|
334
365
|
|
|
335
366
|
**Stop doesn't stick**
|
|
336
367
|
|
|
337
|
-
- The IDE will auto-restart the server if it's still enabled in its config. `--stop` now disables the server entry for Antigravity, Cursor, Claude Desktop, and VS Code (when using common MCP settings keys). Restart the IDE after `--start` to re-enable.
|
|
368
|
+
- The IDE will auto-restart the server if it's still enabled in its config. `--stop` now disables the server entry for Antigravity, Cursor (including `~/.cursor/mcp.json`), Windsurf (`~/.codeium/windsurf/mcp_config.json`), Warp (`~/.warp/mcp_settings.json` and `%APPDATA%\\Warp\\mcp_settings.json` when present), Claude Desktop, and VS Code (when using common MCP settings keys). Restart the IDE after `--start` to re-enable.
|
|
338
369
|
|
|
339
370
|
---
|
|
340
371
|
|
package/index.js
CHANGED
|
@@ -50,17 +50,18 @@ import * as ClearCacheFeature from './features/clear-cache.js';
|
|
|
50
50
|
import * as FindSimilarCodeFeature from './features/find-similar-code.js';
|
|
51
51
|
import * as AnnConfigFeature from './features/ann-config.js';
|
|
52
52
|
import * as PackageVersionFeature from './features/package-version.js';
|
|
53
|
-
import * as SetWorkspaceFeature from './features/set-workspace.js';
|
|
54
|
-
import { handleListResources, handleReadResource } from './features/resources.js';
|
|
55
|
-
|
|
53
|
+
import * as SetWorkspaceFeature from './features/set-workspace.js';
|
|
54
|
+
import { handleListResources, handleReadResource } from './features/resources.js';
|
|
55
|
+
import { getWorkspaceEnvKeys } from './lib/workspace-env.js';
|
|
56
|
+
|
|
56
57
|
import {
|
|
57
58
|
MEMORY_LOG_INTERVAL_MS,
|
|
58
59
|
ONNX_THREAD_LIMIT,
|
|
59
60
|
BACKGROUND_INDEX_DELAY_MS,
|
|
60
61
|
} from './lib/constants.js';
|
|
61
|
-
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
62
|
-
|
|
63
|
-
async function readLogTail(logPath, maxLines = 2000) {
|
|
62
|
+
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
63
|
+
|
|
64
|
+
async function readLogTail(logPath, maxLines = 2000) {
|
|
64
65
|
const data = await fs.readFile(logPath, 'utf-8');
|
|
65
66
|
if (!data) return [];
|
|
66
67
|
const lines = data.split(/\r?\n/).filter(Boolean);
|
|
@@ -118,12 +119,75 @@ async function printMemorySnapshot(workspaceDir) {
|
|
|
118
119
|
// Arguments parsed in main()
|
|
119
120
|
|
|
120
121
|
// Global state
|
|
121
|
-
let embedder = null;
|
|
122
|
-
let unloadMainEmbedder = null; // Function to unload the embedding model
|
|
123
|
-
let cache = null;
|
|
124
|
-
let indexer = null;
|
|
125
|
-
let hybridSearch = null;
|
|
126
|
-
let config = null;
|
|
122
|
+
let embedder = null;
|
|
123
|
+
let unloadMainEmbedder = null; // Function to unload the embedding model
|
|
124
|
+
let cache = null;
|
|
125
|
+
let indexer = null;
|
|
126
|
+
let hybridSearch = null;
|
|
127
|
+
let config = null;
|
|
128
|
+
let setWorkspaceFeatureInstance = null;
|
|
129
|
+
let autoWorkspaceSwitchPromise = null;
|
|
130
|
+
|
|
131
|
+
async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
132
|
+
if (!rawValue || rawValue.includes('${')) return null;
|
|
133
|
+
const resolved = path.resolve(rawValue);
|
|
134
|
+
try {
|
|
135
|
+
const stats = await fs.stat(resolved);
|
|
136
|
+
if (!stats.isDirectory()) return null;
|
|
137
|
+
return resolved;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function detectRuntimeWorkspaceFromEnv() {
|
|
144
|
+
for (const key of getWorkspaceEnvKeys()) {
|
|
145
|
+
const workspacePath = await resolveWorkspaceFromEnvValue(process.env[key]);
|
|
146
|
+
if (workspacePath) {
|
|
147
|
+
return { workspacePath, envKey: key };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function maybeAutoSwitchWorkspace(request) {
|
|
155
|
+
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') return;
|
|
156
|
+
if (!setWorkspaceFeatureInstance || !config?.searchDirectory) return;
|
|
157
|
+
if (request?.params?.name === 'f_set_workspace') return;
|
|
158
|
+
|
|
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
|
+
|
|
185
|
+
try {
|
|
186
|
+
await autoWorkspaceSwitchPromise;
|
|
187
|
+
} finally {
|
|
188
|
+
autoWorkspaceSwitchPromise = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
127
191
|
|
|
128
192
|
// Feature registry - ordered by priority (semantic_search first as primary tool)
|
|
129
193
|
const features = [
|
|
@@ -181,46 +245,46 @@ async function initialize(workspaceDir) {
|
|
|
181
245
|
}
|
|
182
246
|
}
|
|
183
247
|
|
|
184
|
-
// Skip gc check during tests (VITEST env is set)
|
|
185
|
-
const isTest = Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
|
|
186
|
-
if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
|
|
187
|
-
console.warn(
|
|
188
|
-
'[Server] enableExplicitGc=true but this process was not started with --expose-gc; continuing with explicit GC disabled.'
|
|
189
|
-
);
|
|
190
|
-
console.warn(
|
|
191
|
-
'[Server] Tip: start with "npm start" or add --expose-gc to enable explicit GC again.'
|
|
192
|
-
);
|
|
193
|
-
config.enableExplicitGc = false;
|
|
194
|
-
}
|
|
248
|
+
// Skip gc check during tests (VITEST env is set)
|
|
249
|
+
const isTest = Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
|
|
250
|
+
if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
|
|
251
|
+
console.warn(
|
|
252
|
+
'[Server] enableExplicitGc=true but this process was not started with --expose-gc; continuing with explicit GC disabled.'
|
|
253
|
+
);
|
|
254
|
+
console.warn(
|
|
255
|
+
'[Server] Tip: start with "npm start" or add --expose-gc to enable explicit GC again.'
|
|
256
|
+
);
|
|
257
|
+
config.enableExplicitGc = false;
|
|
258
|
+
}
|
|
195
259
|
|
|
196
260
|
let mainBackendConfigured = false;
|
|
197
261
|
let nativeOnnxAvailable = null;
|
|
198
|
-
const ensureMainOnnxBackend = () => {
|
|
199
|
-
if (mainBackendConfigured) return;
|
|
200
|
-
nativeOnnxAvailable = configureNativeOnnxBackend({
|
|
201
|
-
log: config.verbose ? console.info : null,
|
|
202
|
-
label: '[Server]',
|
|
203
|
-
threads: {
|
|
262
|
+
const ensureMainOnnxBackend = () => {
|
|
263
|
+
if (mainBackendConfigured) return;
|
|
264
|
+
nativeOnnxAvailable = configureNativeOnnxBackend({
|
|
265
|
+
log: config.verbose ? console.info : null,
|
|
266
|
+
label: '[Server]',
|
|
267
|
+
threads: {
|
|
204
268
|
intraOpNumThreads: ONNX_THREAD_LIMIT,
|
|
205
269
|
interOpNumThreads: 1,
|
|
206
270
|
},
|
|
207
271
|
});
|
|
208
|
-
mainBackendConfigured = true;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
ensureMainOnnxBackend();
|
|
212
|
-
if (nativeOnnxAvailable === false) {
|
|
213
|
-
try {
|
|
214
|
-
const { env } = await getTransformers();
|
|
215
|
-
if (env?.backends?.onnx?.wasm) {
|
|
216
|
-
env.backends.onnx.wasm.numThreads = ONNX_THREAD_LIMIT;
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
// ignore: fallback tuning is best effort
|
|
220
|
-
}
|
|
221
|
-
const status = getNativeOnnxStatus();
|
|
222
|
-
const reason = status?.message || 'onnxruntime-node not available';
|
|
223
|
-
console.warn(`[Server] Native ONNX backend unavailable (${reason}); using WASM backend.`);
|
|
272
|
+
mainBackendConfigured = true;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
ensureMainOnnxBackend();
|
|
276
|
+
if (nativeOnnxAvailable === false) {
|
|
277
|
+
try {
|
|
278
|
+
const { env } = await getTransformers();
|
|
279
|
+
if (env?.backends?.onnx?.wasm) {
|
|
280
|
+
env.backends.onnx.wasm.numThreads = ONNX_THREAD_LIMIT;
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// ignore: fallback tuning is best effort
|
|
284
|
+
}
|
|
285
|
+
const status = getNativeOnnxStatus();
|
|
286
|
+
const reason = status?.message || 'onnxruntime-node not available';
|
|
287
|
+
console.warn(`[Server] Native ONNX backend unavailable (${reason}); using WASM backend.`);
|
|
224
288
|
console.warn(
|
|
225
289
|
'[Server] Auto-safety: disabling workers and forcing embeddingProcessPerBatch for memory isolation.'
|
|
226
290
|
);
|
|
@@ -252,12 +316,12 @@ async function initialize(workspaceDir) {
|
|
|
252
316
|
}
|
|
253
317
|
|
|
254
318
|
// Log effective configuration for debugging
|
|
255
|
-
console.info(
|
|
256
|
-
`[Server] Config: workerThreads=${config.workerThreads}, embeddingProcessPerBatch=${config.embeddingProcessPerBatch}`
|
|
257
|
-
);
|
|
258
|
-
console.info(
|
|
259
|
-
`[Server] Config: vectorStoreLoadMode=${config.vectorStoreLoadMode}, vectorCacheEntries=${config.vectorCacheEntries}`
|
|
260
|
-
);
|
|
319
|
+
console.info(
|
|
320
|
+
`[Server] Config: workerThreads=${config.workerThreads}, embeddingProcessPerBatch=${config.embeddingProcessPerBatch}`
|
|
321
|
+
);
|
|
322
|
+
console.info(
|
|
323
|
+
`[Server] Config: vectorStoreLoadMode=${config.vectorStoreLoadMode}, vectorCacheEntries=${config.vectorCacheEntries}`
|
|
324
|
+
);
|
|
261
325
|
|
|
262
326
|
if (pidPath) {
|
|
263
327
|
console.info(`[Server] PID file: ${pidPath}`);
|
|
@@ -342,22 +406,22 @@ async function initialize(workspaceDir) {
|
|
|
342
406
|
cachedEmbedderPromise = null;
|
|
343
407
|
return false;
|
|
344
408
|
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
embedder = lazyEmbedder;
|
|
348
|
-
unloadMainEmbedder = unloader; // Store in module scope for tool handler access
|
|
349
|
-
const preloadEmbeddingModel = async () => {
|
|
350
|
-
if (config.preloadEmbeddingModel === false) return;
|
|
351
|
-
try {
|
|
352
|
-
console.info('[Server] Preloading embedding model (background)...');
|
|
353
|
-
await embedder(' ');
|
|
354
|
-
} catch (err) {
|
|
355
|
-
console.warn(`[Server] Embedding model preload failed: ${err.message}`);
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
// NOTE: We no longer auto-load in verbose mode when preloadEmbeddingModel=false.
|
|
360
|
-
// The model will be loaded lazily on first search or by child processes during indexing.
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
embedder = lazyEmbedder;
|
|
412
|
+
unloadMainEmbedder = unloader; // Store in module scope for tool handler access
|
|
413
|
+
const preloadEmbeddingModel = async () => {
|
|
414
|
+
if (config.preloadEmbeddingModel === false) return;
|
|
415
|
+
try {
|
|
416
|
+
console.info('[Server] Preloading embedding model (background)...');
|
|
417
|
+
await embedder(' ');
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.warn(`[Server] Embedding model preload failed: ${err.message}`);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// NOTE: We no longer auto-load in verbose mode when preloadEmbeddingModel=false.
|
|
424
|
+
// The model will be loaded lazily on first search or by child processes during indexing.
|
|
361
425
|
|
|
362
426
|
// Initialize cache (load deferred until after server is ready)
|
|
363
427
|
cache = new EmbeddingsCache(config);
|
|
@@ -379,25 +443,26 @@ async function initialize(workspaceDir) {
|
|
|
379
443
|
// Features 5 (PackageVersion) doesn't need instance
|
|
380
444
|
|
|
381
445
|
// Initialize SetWorkspace feature with shared state
|
|
382
|
-
const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
|
|
383
|
-
config,
|
|
384
|
-
cache,
|
|
385
|
-
indexer,
|
|
386
|
-
getGlobalCacheDir
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
features[6].
|
|
446
|
+
const setWorkspaceInstance = new SetWorkspaceFeature.SetWorkspaceFeature(
|
|
447
|
+
config,
|
|
448
|
+
cache,
|
|
449
|
+
indexer,
|
|
450
|
+
getGlobalCacheDir
|
|
451
|
+
);
|
|
452
|
+
setWorkspaceFeatureInstance = setWorkspaceInstance;
|
|
453
|
+
features[6].instance = setWorkspaceInstance;
|
|
454
|
+
features[6].handler = SetWorkspaceFeature.createHandleToolCall(setWorkspaceInstance);
|
|
390
455
|
|
|
391
456
|
// Attach hybridSearch to server for cross-feature access (e.g. cache invalidation)
|
|
392
457
|
server.hybridSearch = hybridSearch;
|
|
393
458
|
|
|
394
|
-
const startBackgroundTasks = async () => {
|
|
395
|
-
// Keep startup responsive: do not block server readiness on model preload.
|
|
396
|
-
void preloadEmbeddingModel();
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
console.info('[Server] Loading cache (deferred)...');
|
|
400
|
-
await cache.load();
|
|
459
|
+
const startBackgroundTasks = async () => {
|
|
460
|
+
// Keep startup responsive: do not block server readiness on model preload.
|
|
461
|
+
void preloadEmbeddingModel();
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
console.info('[Server] Loading cache (deferred)...');
|
|
465
|
+
await cache.load();
|
|
401
466
|
if (config.verbose) {
|
|
402
467
|
logMemory('[Server] Memory (after cache load)');
|
|
403
468
|
}
|
|
@@ -423,8 +488,8 @@ async function initialize(workspaceDir) {
|
|
|
423
488
|
.catch((err) => {
|
|
424
489
|
console.error('[Server] Background indexing error:', err.message);
|
|
425
490
|
});
|
|
426
|
-
}, BACKGROUND_INDEX_DELAY_MS);
|
|
427
|
-
};
|
|
491
|
+
}, BACKGROUND_INDEX_DELAY_MS);
|
|
492
|
+
};
|
|
428
493
|
|
|
429
494
|
return { startBackgroundTasks, config };
|
|
430
495
|
}
|
|
@@ -465,10 +530,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
465
530
|
return { tools };
|
|
466
531
|
});
|
|
467
532
|
|
|
468
|
-
// Handle tool calls
|
|
469
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
470
|
-
|
|
471
|
-
|
|
533
|
+
// Handle tool calls
|
|
534
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
535
|
+
await maybeAutoSwitchWorkspace(request);
|
|
536
|
+
|
|
537
|
+
for (const feature of features) {
|
|
538
|
+
const toolDef = feature.module.getToolDefinition(config);
|
|
472
539
|
|
|
473
540
|
if (request.params.name === toolDef.name) {
|
|
474
541
|
// Safety check: handler may be null if initialization is incomplete
|
package/lib/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ Options:
|
|
|
16
16
|
--mem Show last memory snapshot from logs (requires verbose logging)
|
|
17
17
|
--tail <lines> Lines to show with --logs (default: ${defaultTailLines})
|
|
18
18
|
--no-follow Do not follow log output with --logs
|
|
19
|
-
--start [ide] Register + enable in IDE config (antigravity|cursor|"Claude Desktop")
|
|
19
|
+
--start [ide] Register + enable in IDE config (antigravity|codex|cursor|vscode|windsurf|warp|"Claude Desktop")
|
|
20
20
|
--stop Stop running server instances
|
|
21
21
|
--workspace <path> Workspace path (used by IDE launch / log viewer)
|
|
22
22
|
--version, -v Show version
|
package/lib/config.js
CHANGED
|
@@ -3,13 +3,14 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { ProjectDetector } from './project-detector.js';
|
|
7
|
-
import { parseJsonc } from './settings-editor.js';
|
|
6
|
+
import { ProjectDetector } from './project-detector.js';
|
|
7
|
+
import { parseJsonc } from './settings-editor.js';
|
|
8
8
|
import {
|
|
9
9
|
EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION,
|
|
10
10
|
EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS,
|
|
11
11
|
EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB,
|
|
12
12
|
} from './constants.js';
|
|
13
|
+
import { getWorkspaceEnvKeys } from './workspace-env.js';
|
|
13
14
|
|
|
14
15
|
const DEFAULT_MEMORY_CLEANUP_CONFIG = {
|
|
15
16
|
enableExplicitGc: true, // Require --expose-gc for more aggressive memory cleanup
|
|
@@ -455,18 +456,7 @@ const DEFAULT_CONFIG = {
|
|
|
455
456
|
ann: { ...DEFAULT_ANN_CONFIG },
|
|
456
457
|
};
|
|
457
458
|
|
|
458
|
-
let config = { ...DEFAULT_CONFIG };
|
|
459
|
-
|
|
460
|
-
const WORKSPACE_ENV_VARS = [
|
|
461
|
-
'HEURISTIC_MCP_WORKSPACE',
|
|
462
|
-
'MCP_WORKSPACE',
|
|
463
|
-
'WORKSPACE_FOLDER',
|
|
464
|
-
'WORKSPACE_ROOT',
|
|
465
|
-
'CURSOR_WORKSPACE',
|
|
466
|
-
'CLAUDE_WORKSPACE',
|
|
467
|
-
'ANTIGRAVITY_WORKSPACE',
|
|
468
|
-
'INIT_CWD',
|
|
469
|
-
];
|
|
459
|
+
let config = { ...DEFAULT_CONFIG };
|
|
470
460
|
|
|
471
461
|
const WORKSPACE_MARKERS = [
|
|
472
462
|
'.git',
|
|
@@ -612,7 +602,7 @@ async function readConfigFile(filePath) {
|
|
|
612
602
|
}
|
|
613
603
|
}
|
|
614
604
|
|
|
615
|
-
async function findWorkspaceRoot(startDir) {
|
|
605
|
+
async function findWorkspaceRoot(startDir) {
|
|
616
606
|
let current = path.resolve(startDir);
|
|
617
607
|
while (true) {
|
|
618
608
|
for (const marker of WORKSPACE_MARKERS) {
|
|
@@ -624,43 +614,113 @@ async function findWorkspaceRoot(startDir) {
|
|
|
624
614
|
if (parent === current) break;
|
|
625
615
|
current = parent;
|
|
626
616
|
}
|
|
627
|
-
return path.resolve(startDir);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
async function
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
617
|
+
return path.resolve(startDir);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function resolveWorkspaceCandidate(rawValue) {
|
|
621
|
+
if (!rawValue || rawValue.includes('${')) return null;
|
|
622
|
+
const candidate = path.resolve(rawValue);
|
|
623
|
+
if (!(await pathExists(candidate))) return null;
|
|
624
|
+
try {
|
|
625
|
+
const stats = await fs.stat(candidate);
|
|
626
|
+
if (!stats.isDirectory()) return null;
|
|
627
|
+
} catch {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
return candidate;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function logWorkspaceResolution(resolution) {
|
|
634
|
+
if (!resolution || !resolution.path) return;
|
|
635
|
+
|
|
636
|
+
if (resolution.source === 'workspace-arg') {
|
|
637
|
+
console.info(`[Config] Workspace resolution: --workspace -> ${resolution.path}`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (resolution.source === 'env' && resolution.envKey) {
|
|
642
|
+
console.info(`[Config] Workspace resolution: env ${resolution.envKey} -> ${resolution.path}`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (resolution.source === 'test-cwd') {
|
|
647
|
+
console.info(`[Config] Workspace resolution: process.cwd() (test mode) -> ${resolution.path}`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (resolution.source === 'cwd-root-search') {
|
|
652
|
+
const from = resolution.fromPath || process.cwd();
|
|
653
|
+
console.info(
|
|
654
|
+
`[Config] Workspace resolution: workspace root from cwd (${from}) -> ${resolution.path}`
|
|
655
|
+
);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.info(`[Config] Workspace resolution: process.cwd() -> ${resolution.path}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function resolveWorkspaceDir(workspaceDir) {
|
|
663
|
+
if (workspaceDir) {
|
|
664
|
+
return {
|
|
665
|
+
path: path.resolve(workspaceDir),
|
|
666
|
+
source: 'workspace-arg',
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
|
|
670
|
+
return {
|
|
671
|
+
path: path.resolve(process.cwd()),
|
|
672
|
+
source: 'test-cwd',
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for (const key of getWorkspaceEnvKeys()) {
|
|
677
|
+
const candidate = await resolveWorkspaceCandidate(process.env[key]);
|
|
678
|
+
if (candidate) {
|
|
679
|
+
return {
|
|
680
|
+
path: candidate,
|
|
681
|
+
source: 'env',
|
|
682
|
+
envKey: key,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const cwd = path.resolve(process.cwd());
|
|
688
|
+
const root = await findWorkspaceRoot(cwd);
|
|
689
|
+
if (root !== cwd) {
|
|
690
|
+
return {
|
|
691
|
+
path: root,
|
|
692
|
+
source: 'cwd-root-search',
|
|
693
|
+
fromPath: cwd,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
path: cwd,
|
|
698
|
+
source: 'cwd',
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export async function loadConfig(workspaceDir = null) {
|
|
703
|
+
try {
|
|
648
704
|
// Determine the base directory for configuration
|
|
649
705
|
let baseDir;
|
|
650
706
|
let configPath;
|
|
651
|
-
|
|
652
|
-
let serverDir = null;
|
|
653
|
-
if (workspaceDir) {
|
|
654
|
-
// Workspace mode: load config from workspace root
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
707
|
+
|
|
708
|
+
let serverDir = null;
|
|
709
|
+
if (workspaceDir) {
|
|
710
|
+
// Workspace mode: load config from workspace root
|
|
711
|
+
const workspaceResolution = await resolveWorkspaceDir(workspaceDir);
|
|
712
|
+
baseDir = workspaceResolution.path;
|
|
713
|
+
console.info(`[Config] Workspace mode: ${baseDir}`);
|
|
714
|
+
logWorkspaceResolution(workspaceResolution);
|
|
715
|
+
} else {
|
|
716
|
+
// Server mode: load config from server directory for global settings,
|
|
717
|
+
// but use process.cwd() as base for searching if not specified otherwise
|
|
718
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
719
|
+
serverDir = path.resolve(scriptDir, '..');
|
|
720
|
+
const workspaceResolution = await resolveWorkspaceDir(null);
|
|
721
|
+
baseDir = workspaceResolution.path;
|
|
722
|
+
logWorkspaceResolution(workspaceResolution);
|
|
723
|
+
}
|
|
664
724
|
|
|
665
725
|
let userConfig = {};
|
|
666
726
|
const configNames = ['config.jsonc', 'config.json'];
|
package/lib/constants.js
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized constants for the heuristic-mcp project.
|
|
3
|
-
* Extracting magic numbers improves maintainability and documents design decisions.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// ================================
|
|
7
|
-
//
|
|
8
|
-
// ================================
|
|
1
|
+
/**
|
|
2
|
+
* Centralized constants for the heuristic-mcp project.
|
|
3
|
+
* Extracting magic numbers improves maintainability and documents design decisions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ================================
|
|
7
|
+
// Workspace Resolution Constants
|
|
8
|
+
// ================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Environment variables checked for workspace resolution, in precedence order.
|
|
12
|
+
*/
|
|
13
|
+
export const WORKSPACE_ENV_VARS = Object.freeze([
|
|
14
|
+
'HEURISTIC_MCP_WORKSPACE',
|
|
15
|
+
'MCP_WORKSPACE',
|
|
16
|
+
'CODEX_WORKSPACE',
|
|
17
|
+
'CODEX_PROJECT_ROOT',
|
|
18
|
+
'CODEX_CWD',
|
|
19
|
+
'WORKSPACE_FOLDER',
|
|
20
|
+
'WORKSPACE_ROOT',
|
|
21
|
+
'CURSOR_WORKSPACE',
|
|
22
|
+
'CLAUDE_WORKSPACE',
|
|
23
|
+
'ANTIGRAVITY_WORKSPACE',
|
|
24
|
+
'INIT_CWD',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Prefix for dynamic workspace-related env vars (provider-specific).
|
|
29
|
+
*/
|
|
30
|
+
export const DYNAMIC_WORKSPACE_ENV_PREFIX = 'CODEX_';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pattern used when ranking provider-specific workspace env vars.
|
|
34
|
+
*/
|
|
35
|
+
export const WORKSPACE_ENV_KEY_PATTERN = /(WORKSPACE|PROJECT|ROOT|CWD|DIR)/i;
|
|
36
|
+
|
|
37
|
+
// ================================
|
|
38
|
+
// Chunking Constants
|
|
39
|
+
// ================================
|
|
9
40
|
|
|
10
41
|
/**
|
|
11
42
|
* Minimum text length for a chunk to be considered valid.
|
package/lib/settings-editor.js
CHANGED
|
@@ -469,14 +469,23 @@ function replaceRange(text, start, end, replacement) {
|
|
|
469
469
|
return text.slice(0, start) + replacement + text.slice(end);
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
function resolveContainer(text, rootRange) {
|
|
473
|
-
const
|
|
472
|
+
function resolveContainer(text, rootRange, preferredContainerKey = 'mcpServers') {
|
|
473
|
+
const baseContainers = [
|
|
474
474
|
{ type: 'key', key: 'mcpServers' },
|
|
475
|
+
{ type: 'key', key: 'servers' },
|
|
475
476
|
{ type: 'key', key: 'cline.mcpServers' },
|
|
476
477
|
{ type: 'nested', key: 'cline', child: 'mcpServers' },
|
|
477
478
|
];
|
|
478
479
|
|
|
479
|
-
|
|
480
|
+
const preferredIndex = baseContainers.findIndex(
|
|
481
|
+
(candidate) => candidate.type === 'key' && candidate.key === preferredContainerKey
|
|
482
|
+
);
|
|
483
|
+
if (preferredIndex > 0) {
|
|
484
|
+
const [preferred] = baseContainers.splice(preferredIndex, 1);
|
|
485
|
+
baseContainers.unshift(preferred);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
for (const candidate of baseContainers) {
|
|
480
489
|
if (candidate.type === 'key') {
|
|
481
490
|
const entry = findPropertyValueRange(text, rootRange, candidate.key);
|
|
482
491
|
if (entry) {
|
|
@@ -517,14 +526,19 @@ function resolveContainer(text, rootRange) {
|
|
|
517
526
|
return null;
|
|
518
527
|
}
|
|
519
528
|
|
|
520
|
-
export function upsertMcpServerEntryInText(
|
|
529
|
+
export function upsertMcpServerEntryInText(
|
|
530
|
+
text,
|
|
531
|
+
serverName,
|
|
532
|
+
serverConfig,
|
|
533
|
+
preferredContainerKey = 'mcpServers'
|
|
534
|
+
) {
|
|
521
535
|
const newline = detectNewline(text);
|
|
522
536
|
const indentUnit = detectIndentUnit(text);
|
|
523
537
|
const trimmed = text.trim();
|
|
524
538
|
|
|
525
539
|
if (!trimmed) {
|
|
526
540
|
const payload = {
|
|
527
|
-
|
|
541
|
+
[preferredContainerKey]: {
|
|
528
542
|
[serverName]: serverConfig,
|
|
529
543
|
},
|
|
530
544
|
};
|
|
@@ -536,7 +550,7 @@ export function upsertMcpServerEntryInText(text, serverName, serverConfig) {
|
|
|
536
550
|
return null;
|
|
537
551
|
}
|
|
538
552
|
|
|
539
|
-
const container = resolveContainer(text, rootRange);
|
|
553
|
+
const container = resolveContainer(text, rootRange, preferredContainerKey);
|
|
540
554
|
|
|
541
555
|
if (!container) {
|
|
542
556
|
const objectIndent = getLineIndent(text, rootRange.start);
|
|
@@ -547,7 +561,14 @@ export function upsertMcpServerEntryInText(text, serverName, serverConfig) {
|
|
|
547
561
|
propertyIndent,
|
|
548
562
|
newline
|
|
549
563
|
);
|
|
550
|
-
return insertPropertyIntoObject(
|
|
564
|
+
return insertPropertyIntoObject(
|
|
565
|
+
text,
|
|
566
|
+
rootRange,
|
|
567
|
+
preferredContainerKey,
|
|
568
|
+
valueText,
|
|
569
|
+
indentUnit,
|
|
570
|
+
newline
|
|
571
|
+
);
|
|
551
572
|
}
|
|
552
573
|
|
|
553
574
|
if (container.needsObjectReplace) {
|
|
@@ -622,6 +643,9 @@ export function findMcpServerEntry(config, serverName) {
|
|
|
622
643
|
if (config.mcpServers && config.mcpServers[serverName]) {
|
|
623
644
|
return { containerKey: 'mcpServers', entry: config.mcpServers[serverName] };
|
|
624
645
|
}
|
|
646
|
+
if (config.servers && config.servers[serverName]) {
|
|
647
|
+
return { containerKey: 'servers', entry: config.servers[serverName] };
|
|
648
|
+
}
|
|
625
649
|
if (config['cline.mcpServers'] && config['cline.mcpServers'][serverName]) {
|
|
626
650
|
return {
|
|
627
651
|
containerKey: 'cline.mcpServers',
|
|
@@ -636,3 +660,95 @@ export function findMcpServerEntry(config, serverName) {
|
|
|
636
660
|
}
|
|
637
661
|
return null;
|
|
638
662
|
}
|
|
663
|
+
|
|
664
|
+
function formatTomlString(value) {
|
|
665
|
+
return JSON.stringify(String(value));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function formatTomlArray(values) {
|
|
669
|
+
const list = Array.isArray(values) ? values : [];
|
|
670
|
+
return `[${list.map((value) => formatTomlString(value)).join(', ')}]`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function formatTomlMcpSection(serverName, serverConfig, newline) {
|
|
674
|
+
const lines = [`[mcp_servers.${serverName}]`];
|
|
675
|
+
if (serverConfig.command !== undefined) {
|
|
676
|
+
lines.push(`command = ${formatTomlString(serverConfig.command)}`);
|
|
677
|
+
}
|
|
678
|
+
if (serverConfig.args !== undefined) {
|
|
679
|
+
lines.push(`args = ${formatTomlArray(serverConfig.args)}`);
|
|
680
|
+
}
|
|
681
|
+
if (serverConfig.disabled !== undefined) {
|
|
682
|
+
lines.push(`disabled = ${serverConfig.disabled ? 'true' : 'false'}`);
|
|
683
|
+
}
|
|
684
|
+
return lines.join(newline);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function findTomlSectionRange(source, sectionName) {
|
|
688
|
+
const headerRegex = /^\s*\[([^\]\r\n]+)\]\s*$/gm;
|
|
689
|
+
let start = -1;
|
|
690
|
+
let end = source.length;
|
|
691
|
+
let match;
|
|
692
|
+
|
|
693
|
+
while ((match = headerRegex.exec(source)) !== null) {
|
|
694
|
+
const currentSection = String(match[1] || '').trim();
|
|
695
|
+
if (start === -1) {
|
|
696
|
+
if (currentSection === sectionName) {
|
|
697
|
+
start = match.index;
|
|
698
|
+
}
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
end = match.index;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (start === -1) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return { start, end };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function upsertMcpServerEntryInToml(text, serverName, serverConfig) {
|
|
714
|
+
const source = String(text || '');
|
|
715
|
+
const newline = detectNewline(source || '\n');
|
|
716
|
+
const section = formatTomlMcpSection(serverName, serverConfig, newline);
|
|
717
|
+
const sectionName = `mcp_servers.${serverName}`;
|
|
718
|
+
const range = findTomlSectionRange(source, sectionName);
|
|
719
|
+
|
|
720
|
+
if (!source.trim()) {
|
|
721
|
+
return `${section}${newline}`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (range) {
|
|
725
|
+
const before = source.slice(0, range.start);
|
|
726
|
+
const after = source.slice(range.end).replace(/^\s*\r?\n?/, '');
|
|
727
|
+
const normalizedBefore =
|
|
728
|
+
before.endsWith('\n') || before.endsWith('\r') || !before ? before : `${before}${newline}`;
|
|
729
|
+
const between = after ? newline : '';
|
|
730
|
+
return `${normalizedBefore}${section}${between}${after}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const withTrailingNewline = source.endsWith('\n') || source.endsWith('\r') ? source : `${source}${newline}`;
|
|
734
|
+
return `${withTrailingNewline}${newline}${section}${newline}`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function setMcpServerDisabledInToml(text, serverName, disabled) {
|
|
738
|
+
const source = String(text || '');
|
|
739
|
+
const sectionName = `mcp_servers.${serverName}`;
|
|
740
|
+
const range = findTomlSectionRange(source, sectionName);
|
|
741
|
+
|
|
742
|
+
if (!range) {
|
|
743
|
+
return source;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const sectionBlock = source.slice(range.start, range.end);
|
|
747
|
+
const newline = detectNewline(sectionBlock || '\n');
|
|
748
|
+
const disabledLine = `disabled = ${disabled ? 'true' : 'false'}`;
|
|
749
|
+
const updatedSection = /^\s*disabled\s*=.*$/m.test(sectionBlock)
|
|
750
|
+
? sectionBlock.replace(/^\s*disabled\s*=.*$/m, disabledLine)
|
|
751
|
+
: `${sectionBlock.trimEnd()}${newline}${disabledLine}${newline}`;
|
|
752
|
+
|
|
753
|
+
return `${source.slice(0, range.start)}${updatedSection}${source.slice(range.end)}`;
|
|
754
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DYNAMIC_WORKSPACE_ENV_PREFIX,
|
|
3
|
+
WORKSPACE_ENV_KEY_PATTERN,
|
|
4
|
+
WORKSPACE_ENV_VARS,
|
|
5
|
+
} from './constants.js';
|
|
6
|
+
|
|
7
|
+
export function scoreWorkspaceEnvKey(key) {
|
|
8
|
+
const upper = String(key || '').toUpperCase();
|
|
9
|
+
let score = 0;
|
|
10
|
+
if (upper.includes('WORKSPACE')) score += 8;
|
|
11
|
+
if (upper.includes('PROJECT')) score += 4;
|
|
12
|
+
if (upper.includes('ROOT')) score += 3;
|
|
13
|
+
if (upper.includes('CWD')) score += 2;
|
|
14
|
+
if (upper.includes('DIR')) score += 1;
|
|
15
|
+
return score;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getDynamicWorkspaceEnvKeys(env = process.env) {
|
|
19
|
+
return Object.keys(env)
|
|
20
|
+
.filter((key) => key.startsWith(DYNAMIC_WORKSPACE_ENV_PREFIX))
|
|
21
|
+
.filter((key) => WORKSPACE_ENV_KEY_PATTERN.test(key))
|
|
22
|
+
.filter((key) => !WORKSPACE_ENV_VARS.includes(key))
|
|
23
|
+
.sort((a, b) => scoreWorkspaceEnvKey(b) - scoreWorkspaceEnvKey(a));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getWorkspaceEnvKeys(env = process.env) {
|
|
27
|
+
return [...WORKSPACE_ENV_VARS, ...getDynamicWorkspaceEnvKeys(env)];
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softerist/heuristic-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.15",
|
|
4
4
|
"description": "An enhanced MCP server providing intelligent semantic code search with find-similar-code, recency ranking, and improved chunking. Fork of smart-coding-mcp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|