@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 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 binary.
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
- const cacheRelative = this.getCacheRelativePath();
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
- const vector = new Float32Array(output.data);
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 };