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 +54 -27
- package/config.json +2 -1
- package/features/check-last-version.js +503 -0
- package/index.js +16 -6
- package/lib/ignore-patterns.js +570 -1
- package/package.json +1 -1
- package/test/check-last-version.test.js +577 -0
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:
|
|
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
|
|
127
|
+
### Option 3: Multi-Project Support
|
|
80
128
|
|
|
81
129
|
```json
|
|
82
130
|
{
|
|
83
131
|
"mcpServers": {
|
|
84
|
-
"smart-coding-mcp-
|
|
132
|
+
"smart-coding-mcp-frontend": {
|
|
85
133
|
"command": "smart-coding-mcp",
|
|
86
|
-
"args": ["--workspace", "/path/to/
|
|
134
|
+
"args": ["--workspace", "/path/to/frontend"]
|
|
87
135
|
},
|
|
88
|
-
"smart-coding-mcp-
|
|
136
|
+
"smart-coding-mcp-backend": {
|
|
89
137
|
"command": "smart-coding-mcp",
|
|
90
|
-
"args": ["--workspace", "/path/to/
|
|
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
|
@@ -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
|
+
}
|