@softerist/heuristic-mcp 3.0.6 → 3.0.8
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 +91 -1
- package/config.jsonc +1 -1
- package/features/index-codebase.js +80 -12
- package/features/package-version.js +301 -0
- package/features/set-workspace.js +206 -0
- package/index.js +23 -0
- package/lib/cache.js +113 -3
- package/lib/config.js +21 -2
- package/lib/embedding-worker.js +49 -5
- package/lib/vector-store-sqlite.js +406 -0
- package/package.json +3 -2
- package/test/cache.test.js +50 -1
- package/test/config.test.js +2 -2
- package/test/index-cli.test.js +1 -1
package/README.md
CHANGED
|
@@ -10,9 +10,12 @@ An enhanced MCP server for your codebase. It provides intelligent semantic searc
|
|
|
10
10
|
- Smart indexing: detects project type and applies smart ignore patterns on top of your excludes.
|
|
11
11
|
- Semantic search: find code by meaning, not just keywords.
|
|
12
12
|
- Find similar code: locate near-duplicate or related patterns from a snippet.
|
|
13
|
+
- Package version lookup: check latest versions from npm, PyPI, crates.io, Maven, and more.
|
|
14
|
+
- Workspace switching: change workspace at runtime without restarting the server.
|
|
13
15
|
- Recency ranking and call-graph boosting: surfaces fresh and related code.
|
|
14
16
|
- Optional ANN index: faster candidate retrieval for large codebases.
|
|
15
17
|
- Optional binary vector store: mmap-friendly cache format for large repos.
|
|
18
|
+
- Flexible embedding dimensions: MRL-compatible dimension reduction (64-768d) for speed/quality tradeoffs.
|
|
16
19
|
|
|
17
20
|
---
|
|
18
21
|
|
|
@@ -128,6 +131,27 @@ Example `config.jsonc`:
|
|
|
128
131
|
}
|
|
129
132
|
```
|
|
130
133
|
|
|
134
|
+
### Embedding Model & Dimension Options
|
|
135
|
+
|
|
136
|
+
**Default model:** `jinaai/jina-embeddings-v2-base-code` (768 dimensions)
|
|
137
|
+
|
|
138
|
+
> **Important:** The default Jina model was **not** trained with Matryoshka Representation Learning (MRL). Dimension reduction (`embeddingDimension`) will significantly degrade search quality with this model. Only use dimension reduction with MRL-trained models.
|
|
139
|
+
|
|
140
|
+
For faster search with smaller embeddings, switch to an MRL-compatible model:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
|
145
|
+
"embeddingDimension": 128
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**MRL-compatible models:**
|
|
150
|
+
- `nomic-ai/nomic-embed-text-v1.5` — recommended for 128d/256d
|
|
151
|
+
- Other models explicitly trained with Matryoshka loss
|
|
152
|
+
|
|
153
|
+
**embeddingDimension values:** `64 | 128 | 256 | 512 | 768 | null` (null = full dimensions)
|
|
154
|
+
|
|
131
155
|
Cache location:
|
|
132
156
|
|
|
133
157
|
- By default, the cache is stored in a global OS cache directory under `heuristic-mcp/<hash>`.
|
|
@@ -148,7 +172,7 @@ Selected overrides (prefix `SMART_CODING_`):
|
|
|
148
172
|
- `SMART_CODING_RECENCY_DECAY_DAYS=30` — days until recency boost decays to 0.
|
|
149
173
|
- `SMART_CODING_ANN_ENABLED=true|false` — enable ANN index.
|
|
150
174
|
- `SMART_CODING_ANN_EF_SEARCH=64` — ANN search quality/speed tradeoff.
|
|
151
|
-
- `SMART_CODING_VECTOR_STORE_FORMAT=json|binary` — on-disk vector store format.
|
|
175
|
+
- `SMART_CODING_VECTOR_STORE_FORMAT=json|binary|sqlite` — on-disk vector store format.
|
|
152
176
|
- `SMART_CODING_VECTOR_STORE_CONTENT_MODE=external|inline` — where content is stored for binary format.
|
|
153
177
|
- `SMART_CODING_VECTOR_STORE_LOAD_MODE=memory|disk` — vector loading strategy.
|
|
154
178
|
- `SMART_CODING_CONTENT_CACHE_ENTRIES=256` — LRU entries for decoded content.
|
|
@@ -156,6 +180,7 @@ Selected overrides (prefix `SMART_CODING_`):
|
|
|
156
180
|
- `SMART_CODING_CLEAR_CACHE_AFTER_INDEX=true|false` — drop in-memory vectors after indexing.
|
|
157
181
|
- `SMART_CODING_EXPLICIT_GC=true|false` — opt-in to explicit GC (requires `--expose-gc`).
|
|
158
182
|
- `SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB=2048` — RSS threshold for running incremental GC after watcher updates (requires explicit GC).
|
|
183
|
+
- `SMART_CODING_EMBEDDING_DIMENSION=64|128|256|512|768` — MRL dimension reduction (only for MRL-trained models).
|
|
159
184
|
|
|
160
185
|
See `lib/config.js` for the full list.
|
|
161
186
|
|
|
@@ -171,6 +196,28 @@ and reads on demand. Recommended for large repos.
|
|
|
171
196
|
- `clearCacheAfterIndex=true` drops in-memory vectors after indexing and reloads lazily on next query.
|
|
172
197
|
- Note: `annEnabled=true` with `vectorStoreLoadMode=disk` can increase disk reads during ANN rebuilds on large indexes.
|
|
173
198
|
|
|
199
|
+
### SQLite Vector Store
|
|
200
|
+
|
|
201
|
+
Set `vectorStoreFormat` to `sqlite` to use SQLite for persistence. This provides:
|
|
202
|
+
|
|
203
|
+
- ACID transactions for reliable writes
|
|
204
|
+
- Simpler concurrent access
|
|
205
|
+
- Standard database format for inspection
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"vectorStoreFormat": "sqlite"
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The vectors and content are stored in `vectors.sqlite` in your cache directory. You can inspect it with any SQLite browser.
|
|
214
|
+
`vectorStoreContentMode` and `vectorStoreLoadMode` are respected for SQLite (use `vectorStoreLoadMode=disk` to avoid loading vectors into memory).
|
|
215
|
+
|
|
216
|
+
**Tradeoffs vs Binary:**
|
|
217
|
+
- Slightly higher read overhead (SQL queries vs direct memory access)
|
|
218
|
+
- Better write reliability (transactions)
|
|
219
|
+
- Easier debugging (standard SQLite file)
|
|
220
|
+
|
|
174
221
|
### Benchmarking Search
|
|
175
222
|
|
|
176
223
|
Use the built-in script to compare memory vs latency tradeoffs:
|
|
@@ -191,6 +238,49 @@ Note: On small repos, disk mode may be slightly slower and show noisy RSS deltas
|
|
|
191
238
|
|
|
192
239
|
---
|
|
193
240
|
|
|
241
|
+
## MCP Tools Reference
|
|
242
|
+
|
|
243
|
+
### `a_semantic_search`
|
|
244
|
+
Find code by meaning. Ideal for natural language queries like "authentication logic" or "database queries".
|
|
245
|
+
|
|
246
|
+
### `b_index_codebase`
|
|
247
|
+
Manually trigger a full reindex. Useful after large code changes.
|
|
248
|
+
|
|
249
|
+
### `c_clear_cache`
|
|
250
|
+
Clear the embeddings cache and force reindex.
|
|
251
|
+
|
|
252
|
+
### `d_ann_config`
|
|
253
|
+
Configure the ANN (Approximate Nearest Neighbor) index. Actions: `stats`, `set_ef_search`, `rebuild`.
|
|
254
|
+
|
|
255
|
+
### `d_find_similar_code`
|
|
256
|
+
Find similar code patterns given a snippet. Useful for finding duplicates or refactoring opportunities.
|
|
257
|
+
|
|
258
|
+
### `e_check_package_version`
|
|
259
|
+
Fetch the latest version of a package from its official registry.
|
|
260
|
+
|
|
261
|
+
**Supported registries:**
|
|
262
|
+
- **npm** (default): `lodash`, `@types/node`
|
|
263
|
+
- **PyPI**: `pip:requests`, `pypi:django`
|
|
264
|
+
- **crates.io**: `cargo:serde`, `rust:tokio`
|
|
265
|
+
- **Maven**: `maven:org.springframework:spring-core`
|
|
266
|
+
- **Go**: `go:github.com/gin-gonic/gin`
|
|
267
|
+
- **RubyGems**: `gem:rails`
|
|
268
|
+
- **NuGet**: `nuget:Newtonsoft.Json`
|
|
269
|
+
- **Packagist**: `composer:laravel/framework`
|
|
270
|
+
- **Hex**: `hex:phoenix`
|
|
271
|
+
- **pub.dev**: `pub:flutter`
|
|
272
|
+
- **Homebrew**: `brew:node`
|
|
273
|
+
- **Conda**: `conda:numpy`
|
|
274
|
+
|
|
275
|
+
### `f_set_workspace`
|
|
276
|
+
Change the workspace directory at runtime. Updates search directory, cache location, and optionally triggers reindex.
|
|
277
|
+
|
|
278
|
+
**Parameters:**
|
|
279
|
+
- `workspacePath` (required): Absolute path to the new workspace
|
|
280
|
+
- `reindex` (optional, default: `true`): Whether to trigger a full reindex
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
194
284
|
## Troubleshooting
|
|
195
285
|
|
|
196
286
|
**Server isn't starting**
|
package/config.jsonc
CHANGED
|
@@ -666,7 +666,7 @@
|
|
|
666
666
|
"embeddingModel": "jinaai/jina-embeddings-v2-base-code",
|
|
667
667
|
// Preload the embedding model on startup.
|
|
668
668
|
"preloadEmbeddingModel": true,
|
|
669
|
-
// Vector store format: json or
|
|
669
|
+
// Vector store format: json, binary, or sqlite.
|
|
670
670
|
"vectorStoreFormat": "binary",
|
|
671
671
|
// Content storage: external or inline.
|
|
672
672
|
"vectorStoreContentMode": "external",
|
|
@@ -18,6 +18,25 @@ function toFloat32Array(vector) {
|
|
|
18
18
|
return new Float32Array(vector);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function sliceAndNormalize(vector, targetDim) {
|
|
22
|
+
if (!targetDim || targetDim >= vector.length) {
|
|
23
|
+
return vector;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sliced = vector.slice(0, targetDim);
|
|
27
|
+
let sumSquares = 0;
|
|
28
|
+
for (let i = 0; i < targetDim; i++) {
|
|
29
|
+
sumSquares += sliced[i] * sliced[i];
|
|
30
|
+
}
|
|
31
|
+
const norm = Math.sqrt(sumSquares);
|
|
32
|
+
if (norm > 0) {
|
|
33
|
+
for (let i = 0; i < targetDim; i++) {
|
|
34
|
+
sliced[i] /= norm;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return sliced;
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
function isTestEnv() {
|
|
22
41
|
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
|
|
23
42
|
}
|
|
@@ -93,15 +112,7 @@ export class CodebaseIndexer {
|
|
|
93
112
|
this.isIndexing = false;
|
|
94
113
|
this.processingWatchEvents = false;
|
|
95
114
|
this.pendingWatchEvents = new Map();
|
|
96
|
-
|
|
97
|
-
const autoExclude = ['.smart-coding-cache'];
|
|
98
|
-
if (cacheRelative) {
|
|
99
|
-
autoExclude.push(cacheRelative, `${cacheRelative}/**`);
|
|
100
|
-
}
|
|
101
|
-
this.excludeMatchers = buildExcludeMatchers([
|
|
102
|
-
...autoExclude,
|
|
103
|
-
...(this.config.excludePatterns || []),
|
|
104
|
-
]);
|
|
115
|
+
this.rebuildExcludeMatchers();
|
|
105
116
|
this.gitignore = ignore();
|
|
106
117
|
this.workerFailureCount = 0;
|
|
107
118
|
this.workersDisabledUntil = 0;
|
|
@@ -138,6 +149,45 @@ export class CodebaseIndexer {
|
|
|
138
149
|
this._embeddingChildRestartThresholdMb = this.getEmbeddingChildRestartThresholdMb();
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
rebuildExcludeMatchers() {
|
|
153
|
+
const cacheRelative = this.getCacheRelativePath();
|
|
154
|
+
const autoExclude = ['.smart-coding-cache'];
|
|
155
|
+
if (cacheRelative) {
|
|
156
|
+
autoExclude.push(cacheRelative, `${cacheRelative}/**`);
|
|
157
|
+
}
|
|
158
|
+
this.excludeMatchers = buildExcludeMatchers([
|
|
159
|
+
...autoExclude,
|
|
160
|
+
...(this.config.excludePatterns || []),
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async updateWorkspaceState({ restartWatcher = false } = {}) {
|
|
165
|
+
this.workspaceRoot = this.config.searchDirectory
|
|
166
|
+
? path.resolve(this.config.searchDirectory)
|
|
167
|
+
: null;
|
|
168
|
+
this.workspaceRootReal = null;
|
|
169
|
+
this.rebuildExcludeMatchers();
|
|
170
|
+
this.gitignore = ignore();
|
|
171
|
+
if (this.pendingWatchEvents) {
|
|
172
|
+
this.pendingWatchEvents.clear();
|
|
173
|
+
}
|
|
174
|
+
if (this._watcherDebounceTimers) {
|
|
175
|
+
for (const timer of this._watcherDebounceTimers.values()) {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
}
|
|
178
|
+
this._watcherDebounceTimers.clear();
|
|
179
|
+
}
|
|
180
|
+
if (this._watcherInProgress) {
|
|
181
|
+
this._watcherInProgress.clear();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (restartWatcher && this.config.watchFiles) {
|
|
185
|
+
await this.setupFileWatcher();
|
|
186
|
+
} else if (this.config.watchFiles) {
|
|
187
|
+
await this.loadGitignore();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
141
191
|
getEmbeddingChildRestartThresholdMb() {
|
|
142
192
|
const totalMb = typeof os.totalmem === 'function' ? os.totalmem() / 1024 / 1024 : 8192;
|
|
143
193
|
if (this.isHeavyEmbeddingModel()) {
|
|
@@ -396,6 +446,7 @@ export class CodebaseIndexer {
|
|
|
396
446
|
workerData: {
|
|
397
447
|
workerId: i,
|
|
398
448
|
embeddingModel: this.config.embeddingModel,
|
|
449
|
+
embeddingDimension: this.config.embeddingDimension || null,
|
|
399
450
|
verbose: this.config.verbose,
|
|
400
451
|
numThreads: threadsPerWorker,
|
|
401
452
|
searchDirectory: this.config.searchDirectory,
|
|
@@ -546,6 +597,7 @@ export class CodebaseIndexer {
|
|
|
546
597
|
workerData: {
|
|
547
598
|
workerId: index,
|
|
548
599
|
embeddingModel: this.config.embeddingModel,
|
|
600
|
+
embeddingDimension: this.config.embeddingDimension || null,
|
|
549
601
|
verbose: this.config.verbose,
|
|
550
602
|
numThreads: 12, // Use 12 threads (50% of cores) for max throughput
|
|
551
603
|
searchDirectory: this.config.searchDirectory,
|
|
@@ -1201,10 +1253,23 @@ export class CodebaseIndexer {
|
|
|
1201
1253
|
await this.stopEmbeddingProcessSession({ preserveStats: true });
|
|
1202
1254
|
await this.startEmbeddingProcessSession();
|
|
1203
1255
|
}
|
|
1204
|
-
return results;
|
|
1256
|
+
return this.applyEmbeddingDimensionToResults(results);
|
|
1205
1257
|
});
|
|
1206
1258
|
}
|
|
1207
1259
|
|
|
1260
|
+
applyEmbeddingDimensionToResults(results) {
|
|
1261
|
+
const targetDim = this.config.embeddingDimension;
|
|
1262
|
+
if (!targetDim || !Array.isArray(results)) {
|
|
1263
|
+
return results;
|
|
1264
|
+
}
|
|
1265
|
+
for (const result of results) {
|
|
1266
|
+
if (!result || !result.vector) continue;
|
|
1267
|
+
const floatVector = toFloat32Array(result.vector);
|
|
1268
|
+
result.vector = sliceAndNormalize(floatVector, targetDim);
|
|
1269
|
+
}
|
|
1270
|
+
return results;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1208
1273
|
async processChunksInChildProcess(chunks) {
|
|
1209
1274
|
if (this._embeddingProcessSessionActive) {
|
|
1210
1275
|
return this.processChunksInPersistentChild(chunks);
|
|
@@ -1294,7 +1359,7 @@ export class CodebaseIndexer {
|
|
|
1294
1359
|
}
|
|
1295
1360
|
try {
|
|
1296
1361
|
const parsed = JSON.parse(stdout);
|
|
1297
|
-
resolve(parsed?.results || []);
|
|
1362
|
+
resolve(this.applyEmbeddingDimensionToResults(parsed?.results || []));
|
|
1298
1363
|
} catch (err) {
|
|
1299
1364
|
this.recordWorkerFailure(`child process parse error (${err.message})`);
|
|
1300
1365
|
resolve([]);
|
|
@@ -1324,7 +1389,10 @@ export class CodebaseIndexer {
|
|
|
1324
1389
|
normalize: true,
|
|
1325
1390
|
});
|
|
1326
1391
|
// CRITICAL: Deep copy to release ONNX tensor memory
|
|
1327
|
-
|
|
1392
|
+
let vector = toFloat32Array(output.data);
|
|
1393
|
+
if (this.config.embeddingDimension) {
|
|
1394
|
+
vector = sliceAndNormalize(vector, this.config.embeddingDimension);
|
|
1395
|
+
}
|
|
1328
1396
|
// Help GC by nullifying the large buffer reference (dispose() doesn't exist in transformers.js)
|
|
1329
1397
|
try {
|
|
1330
1398
|
output.data = null;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Version Lookup Tool
|
|
3
|
+
*
|
|
4
|
+
* Fetches the latest version of packages from various registries.
|
|
5
|
+
* Supports npm, PyPI, crates.io, Maven, Go, RubyGems, NuGet, Packagist, Hex, pub.dev, and more.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Registry configurations with their API endpoints and response parsers
|
|
9
|
+
const REGISTRIES = {
|
|
10
|
+
npm: {
|
|
11
|
+
name: 'npm',
|
|
12
|
+
pattern: /^(?:npm:)?(.+)$/,
|
|
13
|
+
url: (pkg) => `https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`,
|
|
14
|
+
parse: (data) => data.version,
|
|
15
|
+
detect: (pkg) =>
|
|
16
|
+
pkg.startsWith('@') || /^[a-z0-9][-a-z0-9._]*$/i.test(pkg.replace(/^npm:/, '')),
|
|
17
|
+
},
|
|
18
|
+
pypi: {
|
|
19
|
+
name: 'PyPI',
|
|
20
|
+
pattern: /^(?:pip:|pypi:)(.+)$/,
|
|
21
|
+
url: (pkg) => `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`,
|
|
22
|
+
parse: (data) => data.info.version,
|
|
23
|
+
detect: () => false, // Requires explicit prefix
|
|
24
|
+
},
|
|
25
|
+
crates: {
|
|
26
|
+
name: 'crates.io',
|
|
27
|
+
pattern: /^(?:cargo:|crates:|rust:)(.+)$/,
|
|
28
|
+
url: (pkg) => `https://crates.io/api/v1/crates/${encodeURIComponent(pkg)}`,
|
|
29
|
+
parse: (data) => data.crate.max_version,
|
|
30
|
+
headers: { 'User-Agent': 'heuristic-mcp/1.0' },
|
|
31
|
+
detect: () => false,
|
|
32
|
+
},
|
|
33
|
+
go: {
|
|
34
|
+
name: 'Go',
|
|
35
|
+
pattern: /^(?:go:)(.+)$/,
|
|
36
|
+
url: (pkg) => `https://proxy.golang.org/${encodeURIComponent(pkg)}/@latest`,
|
|
37
|
+
parse: (data) => data.Version,
|
|
38
|
+
detect: (pkg) => pkg.includes('/') && pkg.includes('.'),
|
|
39
|
+
},
|
|
40
|
+
rubygems: {
|
|
41
|
+
name: 'RubyGems',
|
|
42
|
+
pattern: /^(?:gem:|ruby:)(.+)$/,
|
|
43
|
+
url: (pkg) => `https://rubygems.org/api/v1/gems/${encodeURIComponent(pkg)}.json`,
|
|
44
|
+
parse: (data) => data.version,
|
|
45
|
+
detect: () => false,
|
|
46
|
+
},
|
|
47
|
+
nuget: {
|
|
48
|
+
name: 'NuGet',
|
|
49
|
+
pattern: /^(?:nuget:|dotnet:)(.+)$/,
|
|
50
|
+
url: (pkg) =>
|
|
51
|
+
`https://api.nuget.org/v3-flatcontainer/${encodeURIComponent(pkg.toLowerCase())}/index.json`,
|
|
52
|
+
parse: (data) => data.versions[data.versions.length - 1],
|
|
53
|
+
detect: () => false,
|
|
54
|
+
},
|
|
55
|
+
packagist: {
|
|
56
|
+
name: 'Packagist',
|
|
57
|
+
pattern: /^(?:composer:|php:)(.+)$/,
|
|
58
|
+
url: (pkg) => `https://repo.packagist.org/p2/${encodeURIComponent(pkg)}.json`,
|
|
59
|
+
parse: (data) => {
|
|
60
|
+
const pkgName = Object.keys(data.packages)[0];
|
|
61
|
+
const versions = data.packages[pkgName];
|
|
62
|
+
// Filter out dev versions and get latest stable
|
|
63
|
+
const stable = versions.find((v) => !v.version.includes('dev'));
|
|
64
|
+
return stable ? stable.version : versions[0].version;
|
|
65
|
+
},
|
|
66
|
+
detect: (pkg) => pkg.includes('/'),
|
|
67
|
+
},
|
|
68
|
+
hex: {
|
|
69
|
+
name: 'Hex',
|
|
70
|
+
pattern: /^(?:hex:|elixir:|mix:)(.+)$/,
|
|
71
|
+
url: (pkg) => `https://hex.pm/api/packages/${encodeURIComponent(pkg)}`,
|
|
72
|
+
parse: (data) => {
|
|
73
|
+
const releases = data.releases;
|
|
74
|
+
return releases.length > 0 ? releases[0].version : null;
|
|
75
|
+
},
|
|
76
|
+
detect: () => false,
|
|
77
|
+
},
|
|
78
|
+
pub: {
|
|
79
|
+
name: 'pub.dev',
|
|
80
|
+
pattern: /^(?:pub:|dart:|flutter:)(.+)$/,
|
|
81
|
+
url: (pkg) => `https://pub.dev/api/packages/${encodeURIComponent(pkg)}`,
|
|
82
|
+
parse: (data) => data.latest.version,
|
|
83
|
+
detect: () => false,
|
|
84
|
+
},
|
|
85
|
+
maven: {
|
|
86
|
+
name: 'Maven Central',
|
|
87
|
+
pattern: /^(?:maven:|java:)(.+)$/,
|
|
88
|
+
url: (pkg) => {
|
|
89
|
+
// Maven packages are in format group:artifact or group/artifact
|
|
90
|
+
const [group, artifact] = pkg.includes(':') ? pkg.split(':') : pkg.split('/');
|
|
91
|
+
if (!artifact) return null;
|
|
92
|
+
const groupPath = group.replace(/\./g, '/');
|
|
93
|
+
return `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(group)}+AND+a:${encodeURIComponent(artifact)}&rows=1&wt=json`;
|
|
94
|
+
},
|
|
95
|
+
parse: (data) => {
|
|
96
|
+
if (data.response.docs.length === 0) return null;
|
|
97
|
+
return data.response.docs[0].latestVersion;
|
|
98
|
+
},
|
|
99
|
+
detect: (pkg) => pkg.includes(':') || (pkg.includes('.') && pkg.includes('/')),
|
|
100
|
+
},
|
|
101
|
+
homebrew: {
|
|
102
|
+
name: 'Homebrew',
|
|
103
|
+
pattern: /^(?:brew:|homebrew:)(.+)$/,
|
|
104
|
+
url: (pkg) => `https://formulae.brew.sh/api/formula/${encodeURIComponent(pkg)}.json`,
|
|
105
|
+
parse: (data) => data.versions.stable,
|
|
106
|
+
detect: () => false,
|
|
107
|
+
},
|
|
108
|
+
conda: {
|
|
109
|
+
name: 'Conda',
|
|
110
|
+
pattern: /^(?:conda:)(.+)$/,
|
|
111
|
+
url: (pkg) =>
|
|
112
|
+
`https://api.anaconda.org/package/conda-forge/${encodeURIComponent(pkg)}`,
|
|
113
|
+
parse: (data) => data.latest_version,
|
|
114
|
+
detect: () => false,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Detect the registry for a package based on its name or prefix
|
|
120
|
+
*/
|
|
121
|
+
function detectRegistry(packageName) {
|
|
122
|
+
// Check for explicit prefixes first
|
|
123
|
+
for (const [key, registry] of Object.entries(REGISTRIES)) {
|
|
124
|
+
if (registry.pattern.test(packageName) && key !== 'npm') {
|
|
125
|
+
const match = packageName.match(registry.pattern);
|
|
126
|
+
if (match) {
|
|
127
|
+
return { registry, cleanName: match[1] };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try to detect based on package name patterns
|
|
133
|
+
for (const registry of Object.values(REGISTRIES)) {
|
|
134
|
+
if (registry.detect(packageName)) {
|
|
135
|
+
const match = packageName.match(registry.pattern);
|
|
136
|
+
return { registry, cleanName: match ? match[1] : packageName };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Default to npm
|
|
141
|
+
const npmMatch = packageName.match(REGISTRIES.npm.pattern);
|
|
142
|
+
return { registry: REGISTRIES.npm, cleanName: npmMatch ? npmMatch[1] : packageName };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fetch the latest version of a package from its registry
|
|
147
|
+
*/
|
|
148
|
+
async function fetchPackageVersion(packageName, timeoutMs = 10000) {
|
|
149
|
+
const { registry, cleanName } = detectRegistry(packageName);
|
|
150
|
+
|
|
151
|
+
const url = registry.url(cleanName);
|
|
152
|
+
if (!url) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: `Invalid package format for ${registry.name}: ${cleanName}`,
|
|
156
|
+
registry: registry.name,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const headers = {
|
|
165
|
+
Accept: 'application/json',
|
|
166
|
+
...(registry.headers || {}),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const response = await fetch(url, {
|
|
170
|
+
headers,
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
if (response.status === 404) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: `Package "${cleanName}" not found on ${registry.name}`,
|
|
179
|
+
registry: registry.name,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
error: `${registry.name} returned status ${response.status}`,
|
|
185
|
+
registry: registry.name,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const data = await response.json();
|
|
190
|
+
const version = registry.parse(data);
|
|
191
|
+
|
|
192
|
+
if (!version) {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
error: `Could not parse version from ${registry.name} response`,
|
|
196
|
+
registry: registry.name,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
package: cleanName,
|
|
203
|
+
version,
|
|
204
|
+
registry: registry.name,
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error.name === 'AbortError') {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: `Request to ${registry.name} timed out`,
|
|
211
|
+
registry: registry.name,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: `Failed to fetch from ${registry.name}: ${error.message}`,
|
|
217
|
+
registry: registry.name,
|
|
218
|
+
};
|
|
219
|
+
} finally {
|
|
220
|
+
clearTimeout(timeout);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get supported registries list for help text
|
|
226
|
+
*/
|
|
227
|
+
function getSupportedRegistries() {
|
|
228
|
+
return Object.entries(REGISTRIES).map(([key, reg]) => ({
|
|
229
|
+
key,
|
|
230
|
+
name: reg.name,
|
|
231
|
+
prefix: reg.pattern.source.match(/\?:([^)]+)\)/)?.[1] || key + ':',
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// MCP Tool definition
|
|
236
|
+
export function getToolDefinition() {
|
|
237
|
+
return {
|
|
238
|
+
name: 'e_check_package_version',
|
|
239
|
+
description:
|
|
240
|
+
'Fetches the latest version of a package from its official registry. Supports npm, PyPI, crates.io, Maven, Go, RubyGems, NuGet, Packagist, Hex, pub.dev, Homebrew, and Conda. Use prefix like "pip:requests" for non-npm packages.',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
package: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description:
|
|
247
|
+
'Package name, optionally prefixed with registry (e.g., "lodash", "pip:requests", "cargo:serde", "go:github.com/gin-gonic/gin")',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
required: ['package'],
|
|
251
|
+
},
|
|
252
|
+
annotations: {
|
|
253
|
+
title: 'Check Package Version',
|
|
254
|
+
readOnlyHint: true,
|
|
255
|
+
destructiveHint: false,
|
|
256
|
+
idempotentHint: true,
|
|
257
|
+
openWorldHint: true, // Makes external network requests
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Tool handler
|
|
263
|
+
export async function handleToolCall(request) {
|
|
264
|
+
const packageName = request.params.arguments.package;
|
|
265
|
+
|
|
266
|
+
if (!packageName || typeof packageName !== 'string' || packageName.trim() === '') {
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: 'text',
|
|
271
|
+
text: 'Error: Please provide a package name.',
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = await fetchPackageVersion(packageName.trim());
|
|
278
|
+
|
|
279
|
+
if (result.success) {
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: 'text',
|
|
284
|
+
text: `**${result.package}** (${result.registry})\n\nLatest version: \`${result.version}\``,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
} else {
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: 'text',
|
|
293
|
+
text: `Error: ${result.error}`,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Export for testing
|
|
301
|
+
export { fetchPackageVersion, detectRegistry, getSupportedRegistries, REGISTRIES };
|