smart-coding-mcp 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,15 @@
7
7
 
8
8
  An extensible Model Context Protocol (MCP) server that provides intelligent semantic code search for AI assistants. Built with local AI models (RAG), inspired by Cursor's semantic search research.
9
9
 
10
+ ### Available Tools
11
+
12
+ | Tool | Description | Example |
13
+ | ---------------------- | ------------------------------------------------- | ---------------------------------------------- |
14
+ | `semantic_search` | Find code by meaning, not just keywords | `"Where do we validate user input?"` |
15
+ | `index_codebase` | Manually trigger reindexing | Use after major refactoring or branch switches |
16
+ | `clear_cache` | Reset the embeddings cache | Useful when cache becomes corrupted |
17
+ | `d_check_last_version` | Get latest version of any package (20 ecosystems) | `"express"`, `"npm:react"`, `"pip:requests"` |
18
+
10
19
  ## What This Does
11
20
 
12
21
  AI coding assistants work better when they can find relevant code quickly. Traditional keyword search falls short - if you ask "where do we handle authentication?" but your code uses "login" and "session", keyword search misses it.
@@ -63,7 +72,46 @@ Add to your MCP configuration file. The location depends on your IDE and OS:
63
72
 
64
73
  Add the server configuration to the `mcpServers` object in your config file:
65
74
 
66
- ### Option 1: Specific Project (Recommended)
75
+ ### Option 1: Auto-Detection (Recommended)
76
+
77
+ By default, the server indexes the directory it is started in. Most clients start MCP servers in the workspace root automatically:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "smart-coding-mcp": {
83
+ "command": "smart-coding-mcp",
84
+ "args": []
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ For explicit workspace control, use the `${workspaceFolder}` variable:
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "smart-coding-mcp": {
96
+ "command": "smart-coding-mcp",
97
+ "args": ["--workspace", "${workspaceFolder}"]
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Client Compatibility:**
104
+
105
+ | Client | Supports `${workspaceFolder}` |
106
+ | ---------------- | ----------------------------- |
107
+ | VS Code | Yes |
108
+ | Cursor (Cascade) | Yes |
109
+ | Antigravity | Yes |
110
+ | Claude Desktop | No (use Option 2) |
111
+
112
+ ### Option 2: Absolute Path (Claude Desktop)
113
+
114
+ For clients that don't support dynamic variables:
67
115
 
68
116
  ```json
69
117
  {
@@ -76,18 +124,18 @@ Add the server configuration to the `mcpServers` object in your config file:
76
124
  }
77
125
  ```
78
126
 
79
- ### Option 2: Multi-Project Support
127
+ ### Option 3: Multi-Project Support
80
128
 
81
129
  ```json
82
130
  {
83
131
  "mcpServers": {
84
- "smart-coding-mcp-project-a": {
132
+ "smart-coding-mcp-frontend": {
85
133
  "command": "smart-coding-mcp",
86
- "args": ["--workspace", "/path/to/project-a"]
134
+ "args": ["--workspace", "/path/to/frontend"]
87
135
  },
88
- "smart-coding-mcp-project-b": {
136
+ "smart-coding-mcp-backend": {
89
137
  "command": "smart-coding-mcp",
90
- "args": ["--workspace", "/path/to/project-b"]
138
+ "args": ["--workspace", "/path/to/backend"]
91
139
  }
92
140
  }
93
141
  }
@@ -131,27 +179,6 @@ Override configuration settings via environment variables in your MCP config:
131
179
 
132
180
  **Note**: The server starts instantly and indexes in the background, so your IDE won't be blocked waiting for indexing to complete.
133
181
 
134
- ## Available Tools
135
-
136
- **semantic_search** - Find code by meaning
137
-
138
- ```
139
- Query: "Where do we validate user input?"
140
- Returns: Relevant validation code with file paths and line numbers
141
- ```
142
-
143
- **index_codebase** - Manually trigger reindexing
144
-
145
- ```
146
- Use after major refactoring or branch switches
147
- ```
148
-
149
- **clear_cache** - Reset the embeddings cache
150
-
151
- ```
152
- Useful when cache becomes corrupted or outdated
153
- ```
154
-
155
182
  ## How It Works
156
183
 
157
184
  The server indexes your code in four steps:
package/config.json CHANGED
@@ -47,7 +47,8 @@
47
47
  "**/.next/**",
48
48
  "**/target/**",
49
49
  "**/vendor/**",
50
- "**/.smart-coding-cache/**"
50
+ "**/.smart-coding-cache/**",
51
+ "**/*.rdb"
51
52
  ],
52
53
  "smartIndexing": true,
53
54
  "chunkSize": 25,
@@ -0,0 +1,503 @@
1
+ // Using native global fetch (Node.js 18+)
2
+
3
+ /**
4
+ * Version checker for package registries across multiple ecosystems.
5
+ * Supports caching and automatic retry for transient failures.
6
+ */
7
+ export class VersionChecker {
8
+ /**
9
+ * Creates a new VersionChecker instance.
10
+ * @param {Object} config - Configuration object
11
+ * @param {number} [config.versionCheckTimeout=5000] - Timeout for API requests in ms
12
+ * @param {number} [config.versionCacheTTL=300000] - Cache TTL in ms (default: 5 minutes)
13
+ * @param {number} [config.retryAttempts=1] - Number of retry attempts for failed requests
14
+ * @param {number} [config.retryDelay=500] - Delay between retries in ms
15
+ */
16
+ constructor(config) {
17
+ this.config = config;
18
+ this.timeout = config.versionCheckTimeout || 5000;
19
+ this.cacheTTL = config.versionCacheTTL || 300000; // 5 minutes
20
+ this.retryAttempts = config.retryAttempts ?? 1;
21
+ this.retryDelay = config.retryDelay || 500;
22
+
23
+ // Simple in-memory cache: Map<cacheKey, { data, timestamp }>
24
+ this.cache = new Map();
25
+ }
26
+
27
+ /**
28
+ * Checks the latest version of a package from its registry.
29
+ * @param {string} packageName - Package name (may include ecosystem prefix like "npm:")
30
+ * @param {string} [ecosystem] - Ecosystem identifier (auto-detected if not provided)
31
+ * @returns {Promise<Object>} Result object with package, ecosystem, version, found, source, or error
32
+ */
33
+ async checkVersion(packageName, ecosystem) {
34
+ try {
35
+ // 1. Sanitization
36
+ if (!packageName || typeof packageName !== 'string') {
37
+ throw new Error("Invalid package name");
38
+ }
39
+ packageName = packageName.trim();
40
+
41
+ // 2. Auto-detect
42
+ if (!ecosystem) {
43
+ ecosystem = this._detectEcosystem(packageName);
44
+ } else {
45
+ ecosystem = ecosystem.toLowerCase().trim();
46
+ }
47
+
48
+ // 3. Check cache
49
+ const cacheKey = `${ecosystem || 'unknown'}:${packageName}`;
50
+ const cached = this._getFromCache(cacheKey);
51
+ if (cached) {
52
+ return { ...cached, fromCache: true };
53
+ }
54
+
55
+ // 4. Fetch
56
+ const result = await this._fetchVersion(packageName, ecosystem);
57
+ const response = {
58
+ package: packageName,
59
+ ecosystem: ecosystem || "unknown",
60
+ ...result
61
+ };
62
+
63
+ // 5. Cache successful results
64
+ if (result.found) {
65
+ this._setCache(cacheKey, response);
66
+ }
67
+
68
+ return response;
69
+ } catch (error) {
70
+ return {
71
+ package: packageName,
72
+ ecosystem: ecosystem || "unknown",
73
+ error: error.message,
74
+ found: false,
75
+ message: `Failed to check version: ${error.message}`
76
+ };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Gets a value from cache if it exists and hasn't expired.
82
+ * @param {string} key - Cache key
83
+ * @returns {Object|null} Cached data or null
84
+ * @private
85
+ */
86
+ _getFromCache(key) {
87
+ const entry = this.cache.get(key);
88
+ if (!entry) return null;
89
+
90
+ if (Date.now() - entry.timestamp > this.cacheTTL) {
91
+ this.cache.delete(key);
92
+ return null;
93
+ }
94
+ return entry.data;
95
+ }
96
+
97
+ /**
98
+ * Sets a value in the cache.
99
+ * @param {string} key - Cache key
100
+ * @param {Object} data - Data to cache
101
+ * @private
102
+ */
103
+ _setCache(key, data) {
104
+ this.cache.set(key, { data, timestamp: Date.now() });
105
+ }
106
+
107
+ /**
108
+ * Clears the version cache.
109
+ */
110
+ clearCache() {
111
+ this.cache.clear();
112
+ }
113
+
114
+ /**
115
+ * Detects the ecosystem from package name prefixes.
116
+ * @param {string} packageName - Package name with optional prefix
117
+ * @returns {string|null} Detected ecosystem or null
118
+ * @private
119
+ */
120
+ _detectEcosystem(packageName) {
121
+ // Check specific prefixes first (order matters - more specific before generic)
122
+ if (packageName.startsWith("npm:")) return "npm";
123
+ if (packageName.startsWith("pip:")) return "pypi";
124
+ if (packageName.startsWith("go:")) return "go";
125
+ if (packageName.startsWith("cargo:")) return "crates";
126
+ if (packageName.startsWith("gem:")) return "rubygems";
127
+ if (packageName.startsWith("composer:")) return "packagist";
128
+ if (packageName.startsWith("nuget:")) return "nuget";
129
+ if (packageName.startsWith("pod:")) return "cocoapods";
130
+ if (packageName.startsWith("hex:")) return "hex";
131
+
132
+ // R ecosystem
133
+ if (packageName.startsWith("cran:") || packageName.startsWith("R:")) return "cran";
134
+ // Perl ecosystem
135
+ if (packageName.startsWith("cpan:") || packageName.startsWith("perl:")) return "cpan";
136
+ // Dart ecosystem
137
+ if (packageName.startsWith("dart:") || packageName.startsWith("pub:")) return "pub";
138
+
139
+ // Homebrew
140
+ if (packageName.startsWith("brew:")) return "homebrew";
141
+ // Conda
142
+ if (packageName.startsWith("conda:")) return "conda";
143
+ // Clojars (Clojure)
144
+ if (packageName.startsWith("clojars:") || packageName.startsWith("clj:")) return "clojars";
145
+ // Hackage (Haskell)
146
+ if (packageName.startsWith("hackage:") || packageName.startsWith("haskell:")) return "hackage";
147
+ // Julia
148
+ if (packageName.startsWith("julia:") || packageName.startsWith("jl:")) return "julia";
149
+ // Swift Package Manager
150
+ if (packageName.startsWith("swift:") || packageName.startsWith("spm:")) return "swift";
151
+ // Chocolatey (Windows)
152
+ if (packageName.startsWith("choco:")) return "chocolatey";
153
+
154
+ // Check for Maven patterns last (generic colon for group:artifact or explicit mvn:)
155
+ if (packageName.startsWith("mvn:") || packageName.includes(":")) return "maven";
156
+
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Fetches with automatic retry for transient failures.
162
+ * @param {string} url - URL to fetch
163
+ * @param {Object} options - Fetch options
164
+ * @param {number} [retriesLeft] - Number of retries remaining
165
+ * @returns {Promise<Response>} Fetch response
166
+ * @private
167
+ */
168
+ async _fetchWithRetry(url, options, retriesLeft = this.retryAttempts) {
169
+ try {
170
+ return await fetch(url, options);
171
+ } catch (err) {
172
+ if (retriesLeft > 0) {
173
+ await new Promise(resolve => setTimeout(resolve, this.retryDelay));
174
+ return this._fetchWithRetry(url, options, retriesLeft - 1);
175
+ }
176
+ throw err;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Fetches the latest version from the appropriate registry.
182
+ * @param {string} pkgInput - Package name (with or without prefix)
183
+ * @param {string} ecosystem - Target ecosystem
184
+ * @returns {Promise<Object>} Result with version info or error
185
+ * @private
186
+ */
187
+ async _fetchVersion(pkgInput, ecosystem) {
188
+ const signal = AbortSignal.timeout(this.timeout);
189
+ let url, version;
190
+
191
+ // Helper to handle simple JSON fetch with specific 404 message
192
+ const fetchJson = async (u, errorPrefix) => {
193
+ const res = await this._fetchWithRetry(u, { signal });
194
+ if (res.status === 404) throw new Error(`${errorPrefix} not found (404)`);
195
+ if (!res.ok) throw new Error(`${errorPrefix} registry error: ${res.status} ${res.statusText}`);
196
+ return res.json();
197
+ };
198
+
199
+ switch (ecosystem) {
200
+ case "npm":
201
+ let npmPkg = pkgInput.replace("npm:", "");
202
+ if (npmPkg.startsWith("@") && npmPkg.includes("/")) {
203
+ npmPkg = npmPkg.replace("/", "%2f");
204
+ }
205
+ url = `https://registry.npmjs.org/${npmPkg}`;
206
+ const npmData = await fetchJson(url, "npm package");
207
+ version = npmData["dist-tags"]?.latest;
208
+ break;
209
+
210
+ case "pypi":
211
+ url = `https://pypi.org/pypi/${pkgInput.replace("pip:", "")}/json`;
212
+ const pypiData = await fetchJson(url, "PyPI package");
213
+ version = pypiData.info?.version;
214
+ break;
215
+
216
+ case "packagist":
217
+ const packPkg = pkgInput.replace("composer:", "");
218
+ const parts = packPkg.split("/");
219
+ if (parts.length !== 2) throw new Error("Packagist package must be in 'vendor/package' format");
220
+ url = `https://repo.packagist.org/p2/${packPkg}.json`;
221
+ const packData = await fetchJson(url, "Packagist package");
222
+ const versions = packData.packages[packPkg];
223
+ if (!versions || versions.length === 0) throw new Error("No versions found in registry response");
224
+ version = versions[0].version;
225
+ break;
226
+
227
+ case "crates":
228
+ url = `https://crates.io/api/v1/crates/${pkgInput.replace("cargo:", "")}`;
229
+ const crateRes = await this._fetchWithRetry(url, { headers: { "User-Agent": "Smart-Coding-MCP" }, signal });
230
+ if (crateRes.status === 404) throw new Error("Crate not found (404)");
231
+ if (!crateRes.ok) throw new Error(`Crates.io error: ${crateRes.status}`);
232
+ const crateData = await crateRes.json();
233
+ version = crateData.crate?.max_stable_version || crateData.crate?.newest_version;
234
+ break;
235
+
236
+ case "maven":
237
+ const mvnPkg = pkgInput.replace("mvn:", "");
238
+ const [g, a] = mvnPkg.split(":");
239
+ if (!g || !a) throw new Error("Maven artifact must be in 'group:artifact' format");
240
+ url = `https://search.maven.org/solrsearch/select?q=g:"${g}"+AND+a:"${a}"&wt=json`;
241
+ const mvnRes = await this._fetchWithRetry(url, { signal });
242
+ if (!mvnRes.ok) throw new Error(`Maven Central error: ${mvnRes.status}`);
243
+ const mvnData = await mvnRes.json();
244
+ if (mvnData.response?.docs?.length > 0) {
245
+ version = mvnData.response.docs[0].latestVersion;
246
+ } else {
247
+ throw new Error("Maven artifact not found");
248
+ }
249
+ break;
250
+
251
+ case "go":
252
+ url = `https://proxy.golang.org/${pkgInput.replace("go:", "")}/@latest`;
253
+ const goData = await fetchJson(url, "Go module");
254
+ version = goData.Version;
255
+ break;
256
+
257
+ case "rubygems":
258
+ url = `https://rubygems.org/api/v1/versions/${pkgInput.replace("gem:", "")}/latest.json`;
259
+ const gemData = await fetchJson(url, "Gem");
260
+ version = gemData.version;
261
+ break;
262
+
263
+ case "nuget":
264
+ const nugetPkg = pkgInput.replace("nuget:", "").toLowerCase();
265
+ url = `https://api.nuget.org/v3-flatcontainer/${nugetPkg}/index.json`;
266
+ const nugetData = await fetchJson(url, "NuGet package");
267
+ if (nugetData.versions && nugetData.versions.length > 0) {
268
+ version = nugetData.versions[nugetData.versions.length - 1];
269
+ } else {
270
+ throw new Error("No versions found");
271
+ }
272
+ break;
273
+
274
+ case "cocoapods":
275
+ // CocoaPods trunk API requires special client authentication.
276
+ // The API returns pod metadata but version info requires parsing specs repo.
277
+ // We intentionally fall back to suggesting a web search for reliable results.
278
+ url = `https://trunk.cocoapods.org/api/v1/pods/${pkgInput.replace("pod:", "")}`;
279
+ const podRes = await this._fetchWithRetry(url, { signal });
280
+ if (podRes.ok) {
281
+ throw new Error("CocoaPods API requires client; please perform a web search.");
282
+ }
283
+ throw new Error("CocoaPods lookup not supported directly; please perform a web search.");
284
+
285
+ case "hex":
286
+ url = `https://hex.pm/api/packages/${pkgInput.replace("hex:", "")}`;
287
+ const hexData = await fetchJson(url, "Hex package");
288
+ if (hexData.releases && hexData.releases.length > 0) {
289
+ version = hexData.releases[0].version;
290
+ } else {
291
+ throw new Error("No releases found");
292
+ }
293
+ break;
294
+
295
+ case "cran":
296
+ // R packages from CRAN
297
+ const cranPkg = pkgInput.replace(/^R:|^cran:/i, "");
298
+ url = `https://crandb.r-pkg.org/${cranPkg}`;
299
+ const cranData = await fetchJson(url, "CRAN package");
300
+ version = cranData.Version;
301
+ break;
302
+
303
+ case "cpan":
304
+ // Perl modules from CPAN/MetaCPAN
305
+ const cpanPkg = pkgInput.replace(/^perl:|^cpan:/i, "");
306
+ url = `https://fastapi.metacpan.org/v1/module/${cpanPkg}`;
307
+ const cpanData = await fetchJson(url, "CPAN module");
308
+ version = cpanData.version;
309
+ break;
310
+
311
+ case "pub":
312
+ // Dart packages from pub.dev
313
+ const pubPkg = pkgInput.replace(/^dart:|^pub:/i, "");
314
+ url = `https://pub.dev/api/packages/${pubPkg}`;
315
+ const pubData = await fetchJson(url, "pub.dev package");
316
+ version = pubData.latest?.version;
317
+ break;
318
+
319
+ case "homebrew":
320
+ // Homebrew formulae (macOS/Linux)
321
+ const brewPkg = pkgInput.replace(/^brew:/i, "");
322
+ url = `https://formulae.brew.sh/api/formula/${brewPkg}.json`;
323
+ const brewData = await fetchJson(url, "Homebrew formula");
324
+ version = brewData.versions?.stable;
325
+ break;
326
+
327
+ case "conda":
328
+ // Conda packages (defaults channel via anaconda.org)
329
+ const condaPkg = pkgInput.replace(/^conda:/i, "");
330
+ // Try conda-forge first, then defaults
331
+ url = `https://api.anaconda.org/package/conda-forge/${condaPkg}`;
332
+ try {
333
+ const condaData = await fetchJson(url, "Conda package");
334
+ version = condaData.latest_version;
335
+ } catch {
336
+ // Fallback to main channel
337
+ url = `https://api.anaconda.org/package/anaconda/${condaPkg}`;
338
+ const condaData2 = await fetchJson(url, "Conda package");
339
+ version = condaData2.latest_version;
340
+ }
341
+ break;
342
+
343
+ case "clojars":
344
+ // Clojure packages from Clojars
345
+ const cljPkg = pkgInput.replace(/^clojars:|^clj:/i, "");
346
+ // Format: group/artifact or just artifact (implies same group)
347
+ const cljParts = cljPkg.includes("/") ? cljPkg.split("/") : [cljPkg, cljPkg];
348
+ url = `https://clojars.org/api/artifacts/${cljParts[0]}/${cljParts[1]}`;
349
+ const cljData = await fetchJson(url, "Clojars artifact");
350
+ version = cljData.latest_version;
351
+ break;
352
+
353
+ case "hackage":
354
+ // Haskell packages from Hackage
355
+ const hackPkg = pkgInput.replace(/^hackage:|^haskell:/i, "");
356
+ url = `https://hackage.haskell.org/package/${hackPkg}/preferred.json`;
357
+ const hackData = await fetchJson(url, "Hackage package");
358
+ // preferred.json returns array of version ranges, get the latest normal version
359
+ if (hackData["normal-version"] && hackData["normal-version"].length > 0) {
360
+ version = hackData["normal-version"][0];
361
+ } else {
362
+ throw new Error("No versions found");
363
+ }
364
+ break;
365
+
366
+ case "julia":
367
+ // Julia packages from JuliaHub/General registry
368
+ const juliaPkg = pkgInput.replace(/^julia:|^jl:/i, "");
369
+ url = `https://juliahub.com/ui/Packages/General/${juliaPkg}`;
370
+ // JuliaHub doesn't have a simple JSON API, use package registry
371
+ const juliaUrl = `https://raw.githubusercontent.com/JuliaRegistries/General/master/${juliaPkg[0].toUpperCase()}/${juliaPkg}/Versions.toml`;
372
+ const juliaRes = await this._fetchWithRetry(juliaUrl, { signal });
373
+ if (!juliaRes.ok) throw new Error("Julia package not found");
374
+ const juliaText = await juliaRes.text();
375
+ // Parse TOML to find latest version (versions are in ["x.y.z"] sections)
376
+ const juliaVersions = juliaText.match(/\["([\d.]+)"\]/g);
377
+ if (juliaVersions && juliaVersions.length > 0) {
378
+ version = juliaVersions[juliaVersions.length - 1].replace(/[\["\]]/g, "");
379
+ url = juliaUrl;
380
+ } else {
381
+ throw new Error("No versions found in Julia registry");
382
+ }
383
+ break;
384
+
385
+ case "swift":
386
+ // Swift Package Index
387
+ const swiftPkg = pkgInput.replace(/^swift:|^spm:/i, "");
388
+ // Format: owner/repo
389
+ if (!swiftPkg.includes("/")) {
390
+ throw new Error("Swift package must be in 'owner/repo' format");
391
+ }
392
+ url = `https://swiftpackageindex.com/api/packages/${swiftPkg}`;
393
+ const swiftRes = await this._fetchWithRetry(url, {
394
+ headers: { "Accept": "application/json" },
395
+ signal
396
+ });
397
+ if (swiftRes.status === 404) throw new Error("Swift package not found (404)");
398
+ if (!swiftRes.ok) throw new Error(`Swift Package Index error: ${swiftRes.status}`);
399
+ const swiftData = await swiftRes.json();
400
+ // Get latest stable release
401
+ if (swiftData.releases && swiftData.releases.length > 0) {
402
+ const stable = swiftData.releases.find(r => !r.preRelease) || swiftData.releases[0];
403
+ version = stable.tagName?.replace(/^v/, "");
404
+ } else {
405
+ throw new Error("No releases found");
406
+ }
407
+ break;
408
+
409
+ case "chocolatey":
410
+ // Chocolatey (Windows) packages
411
+ const chocoPkg = pkgInput.replace(/^choco:/i, "").toLowerCase();
412
+ url = `https://community.chocolatey.org/api/v2/package-versions/${chocoPkg}`;
413
+ const chocoRes = await this._fetchWithRetry(url, { signal });
414
+ if (chocoRes.status === 404) throw new Error("Chocolatey package not found (404)");
415
+ if (!chocoRes.ok) throw new Error(`Chocolatey error: ${chocoRes.status}`);
416
+ const chocoVersions = await chocoRes.json();
417
+ if (chocoVersions && chocoVersions.length > 0) {
418
+ version = chocoVersions[chocoVersions.length - 1];
419
+ } else {
420
+ throw new Error("No versions found");
421
+ }
422
+ break;
423
+
424
+ default:
425
+ return {
426
+ found: false,
427
+ message: `Ecosystem '${ecosystem || "unknown"}' not directly supported. Please perform a web search for '${pkgInput} latest version'.`
428
+ };
429
+ }
430
+
431
+ if (version) {
432
+ return {
433
+ found: true,
434
+ version: version,
435
+ source: url
436
+ };
437
+ }
438
+
439
+ throw new Error("Version not found in response (parsing failed)");
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Returns the MCP tool definition for version checking.
445
+ * @returns {Object} Tool definition object
446
+ */
447
+ export function getToolDefinition() {
448
+ return {
449
+ name: "d_check_last_version",
450
+ description: "Get the latest version of a library/package from its official registry. Supported ecosystems: npm (JS/TS), PyPI (Python), Packagist (PHP), Crates.io (Rust), Maven (Java/Kotlin), Go, RubyGems, NuGet (.NET), Hex (Elixir), CRAN (R), CPAN (Perl), pub.dev (Dart), Homebrew (macOS), Conda (Python/R), Clojars (Clojure), Hackage (Haskell), Julia, Swift PM, Chocolatey (Windows). Returns the version string to help you avoid using outdated dependencies.",
451
+ inputSchema: {
452
+ type: "object",
453
+ properties: {
454
+ package: {
455
+ type: "string",
456
+ description: "Package name (e.g., 'express', 'requests', 'flutter', 'brew:wget', 'conda:numpy', 'swift:apple/swift-nio'). Use prefixes for explicit ecosystem detection."
457
+ },
458
+ ecosystem: {
459
+ type: "string",
460
+ enum: ["npm", "pypi", "packagist", "crates", "maven", "go", "rubygems", "nuget", "cocoapods", "hex", "cran", "cpan", "pub", "homebrew", "conda", "clojars", "hackage", "julia", "swift", "chocolatey"],
461
+ description: "Package ecosystem (optional - auto-detected from prefix)"
462
+ }
463
+ },
464
+ required: ["package"]
465
+ },
466
+ annotations: {
467
+ title: "Check Latest Package Version",
468
+ readOnlyHint: true,
469
+ idempotentHint: true
470
+ }
471
+ };
472
+ }
473
+
474
+ /**
475
+ * Handles an MCP tool call for version checking.
476
+ * @param {Object} request - MCP request object
477
+ * @param {VersionChecker} checker - VersionChecker instance
478
+ * @returns {Promise<Object>} MCP response object
479
+ */
480
+ export async function handleToolCall(request, checker) {
481
+ const pkg = request.params.arguments.package;
482
+ const ecosystem = request.params.arguments.ecosystem;
483
+
484
+ const result = await checker.checkVersion(pkg, ecosystem);
485
+
486
+ if (result.found) {
487
+ const cacheNote = result.fromCache ? " (cached)" : "";
488
+ return {
489
+ content: [{
490
+ type: "text",
491
+ text: `Latest version of \`${result.package}\` (${result.ecosystem}): **${result.version}**${cacheNote}\n\nSource: ${result.source}`
492
+ }]
493
+ };
494
+ } else {
495
+ return {
496
+ content: [{
497
+ type: "text",
498
+ text: result.message || `Could not find version for \`${pkg}\`. Error: ${result.error}`
499
+ }],
500
+ isError: true
501
+ };
502
+ }
503
+ }