@nekzus/mcp-server 1.18.1 → 1.19.0-alpha.1
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/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +276 -292
- package/dist/index.js.map +1 -1
- package/package.json +9 -6
package/dist/index.js
CHANGED
|
@@ -10,7 +10,6 @@ import { z } from 'zod';
|
|
|
10
10
|
// Cache configuration
|
|
11
11
|
let NPM_REGISTRY_URL = (process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org').replace(/\/$/, '');
|
|
12
12
|
// Cache configuration
|
|
13
|
-
const CACHE_TTL_SHORT = 15 * 60 * 1000; // 15 minutes
|
|
14
13
|
const CACHE_TTL_MEDIUM = 60 * 60 * 1000; // 1 hour
|
|
15
14
|
const CACHE_TTL_LONG = 6 * 60 * 60 * 1000; // 6 hours
|
|
16
15
|
const CACHE_TTL_VERY_LONG = 24 * 60 * 60 * 1000; // 24 hours
|
|
@@ -73,6 +72,76 @@ function cacheSet(key, value, ttlMilliseconds) {
|
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
}
|
|
75
|
+
// HTTP wrapper configuration
|
|
76
|
+
const HTTP_MAX_RETRIES = 3;
|
|
77
|
+
const HTTP_INITIAL_BACKOFF_MS = 1000;
|
|
78
|
+
const HTTP_MAX_CONCURRENT_REQUESTS = 5;
|
|
79
|
+
const DEFAULT_HEADERS = {
|
|
80
|
+
Accept: 'application/json',
|
|
81
|
+
'User-Agent': 'NPM-Sentinel-MCP/1.x',
|
|
82
|
+
};
|
|
83
|
+
// Semaphore for concurrency control
|
|
84
|
+
let activeRequests = 0;
|
|
85
|
+
const requestQueue = [];
|
|
86
|
+
function acquireSlot() {
|
|
87
|
+
if (activeRequests < HTTP_MAX_CONCURRENT_REQUESTS) {
|
|
88
|
+
activeRequests++;
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
requestQueue.push(() => {
|
|
93
|
+
activeRequests++;
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function releaseSlot() {
|
|
99
|
+
activeRequests--;
|
|
100
|
+
const next = requestQueue.shift();
|
|
101
|
+
if (next)
|
|
102
|
+
next();
|
|
103
|
+
}
|
|
104
|
+
export async function fetchWithRetry(url, options = {}, config = {}) {
|
|
105
|
+
const maxRetries = config.maxRetries ?? HTTP_MAX_RETRIES;
|
|
106
|
+
const mergedHeaders = { ...DEFAULT_HEADERS, ...(options.headers || {}) };
|
|
107
|
+
const mergedOptions = { ...options, headers: mergedHeaders };
|
|
108
|
+
if (!config.skipThrottle) {
|
|
109
|
+
await acquireSlot();
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(url, mergedOptions);
|
|
115
|
+
if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
|
|
116
|
+
if (attempt < maxRetries) {
|
|
117
|
+
const retryAfter = response.headers.get('retry-after');
|
|
118
|
+
const waitMs = retryAfter
|
|
119
|
+
? parseInt(retryAfter, 10) * 1000
|
|
120
|
+
: HTTP_INITIAL_BACKOFF_MS * (2 ** attempt);
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return response;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (attempt < maxRetries) {
|
|
129
|
+
const waitMs = HTTP_INITIAL_BACKOFF_MS * (2 ** attempt);
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Fallback
|
|
137
|
+
return await fetch(url, mergedOptions);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (!config.skipThrottle) {
|
|
141
|
+
releaseSlot();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
76
145
|
// Zod schemas for npm package data
|
|
77
146
|
export const NpmMaintainerSchema = z
|
|
78
147
|
.object({
|
|
@@ -171,64 +240,7 @@ export const NpmDownloadsDataSchema = z.object({
|
|
|
171
240
|
end: z.string(),
|
|
172
241
|
package: z.string(),
|
|
173
242
|
});
|
|
174
|
-
|
|
175
|
-
if (typeof data !== 'object' || data === null) {
|
|
176
|
-
console.debug('NpmsApiResponse validation: Response is not an object or is null');
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
const response = data;
|
|
180
|
-
// Check score structure
|
|
181
|
-
if (!response.score ||
|
|
182
|
-
typeof response.score !== 'object' ||
|
|
183
|
-
!('final' in response.score) ||
|
|
184
|
-
typeof response.score.final !== 'number' ||
|
|
185
|
-
!('detail' in response.score) ||
|
|
186
|
-
typeof response.score.detail !== 'object') {
|
|
187
|
-
console.debug('NpmsApiResponse validation: Invalid score structure');
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
// Check score detail metrics
|
|
191
|
-
const detail = response.score.detail;
|
|
192
|
-
if (typeof detail.quality !== 'number' ||
|
|
193
|
-
typeof detail.popularity !== 'number' ||
|
|
194
|
-
typeof detail.maintenance !== 'number') {
|
|
195
|
-
console.debug('NpmsApiResponse validation: Invalid score detail metrics');
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
// Check collected data structure
|
|
199
|
-
if (!response.collected ||
|
|
200
|
-
typeof response.collected !== 'object' ||
|
|
201
|
-
!response.collected.metadata ||
|
|
202
|
-
typeof response.collected.metadata !== 'object' ||
|
|
203
|
-
typeof response.collected.metadata.name !== 'string' ||
|
|
204
|
-
typeof response.collected.metadata.version !== 'string') {
|
|
205
|
-
console.debug('NpmsApiResponse validation: Invalid collected data structure');
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
// Check npm data
|
|
209
|
-
if (!response.collected.npm ||
|
|
210
|
-
typeof response.collected.npm !== 'object' ||
|
|
211
|
-
!Array.isArray(response.collected.npm.downloads) ||
|
|
212
|
-
typeof response.collected.npm.starsCount !== 'number') {
|
|
213
|
-
console.debug('NpmsApiResponse validation: Invalid npm data structure');
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
// Optional github data check
|
|
217
|
-
if (response.collected.github) {
|
|
218
|
-
if (typeof response.collected.github !== 'object' ||
|
|
219
|
-
typeof response.collected.github.starsCount !== 'number' ||
|
|
220
|
-
typeof response.collected.github.forksCount !== 'number' ||
|
|
221
|
-
typeof response.collected.github.subscribersCount !== 'number' ||
|
|
222
|
-
!response.collected.github.issues ||
|
|
223
|
-
typeof response.collected.github.issues !== 'object' ||
|
|
224
|
-
typeof response.collected.github.issues.count !== 'number' ||
|
|
225
|
-
typeof response.collected.github.issues.openCount !== 'number') {
|
|
226
|
-
console.debug('NpmsApiResponse validation: Invalid github data structure');
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
243
|
+
// Schema for search results from npm registry v1 search api
|
|
232
244
|
export const NpmSearchResultSchema = z
|
|
233
245
|
.object({
|
|
234
246
|
objects: z.array(z.object({
|
|
@@ -266,15 +278,6 @@ export const NpmSearchResultSchema = z
|
|
|
266
278
|
total: z.number(), // total is a sibling of objects
|
|
267
279
|
})
|
|
268
280
|
.loose();
|
|
269
|
-
// Logger function that uses stderr - only for critical errors
|
|
270
|
-
const log = (...args) => {
|
|
271
|
-
// Filter out server status messages
|
|
272
|
-
const message = args[0];
|
|
273
|
-
if (typeof message === 'string' &&
|
|
274
|
-
(!message.startsWith('[Server]') || message.includes('error') || message.includes('Error'))) {
|
|
275
|
-
console.error(...args);
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
281
|
// Type guards for API responses
|
|
279
282
|
function isNpmPackageInfo(data) {
|
|
280
283
|
return (typeof data === 'object' &&
|
|
@@ -391,12 +394,7 @@ export async function handleNpmVersions(args) {
|
|
|
391
394
|
};
|
|
392
395
|
}
|
|
393
396
|
try {
|
|
394
|
-
const response = await
|
|
395
|
-
headers: {
|
|
396
|
-
Accept: 'application/json',
|
|
397
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
398
|
-
},
|
|
399
|
-
});
|
|
397
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
400
398
|
if (!response.ok) {
|
|
401
399
|
return {
|
|
402
400
|
packageInput: pkgInput,
|
|
@@ -527,12 +525,7 @@ export async function handleNpmLatest(args) {
|
|
|
527
525
|
};
|
|
528
526
|
}
|
|
529
527
|
try {
|
|
530
|
-
const response = await
|
|
531
|
-
headers: {
|
|
532
|
-
Accept: 'application/json',
|
|
533
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
534
|
-
},
|
|
535
|
-
});
|
|
528
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
|
|
536
529
|
if (!response.ok) {
|
|
537
530
|
let errorMsg = `Failed to fetch package version: ${response.status} ${response.statusText}`;
|
|
538
531
|
if (response.status === 404) {
|
|
@@ -666,12 +659,7 @@ export async function handleNpmDeps(args) {
|
|
|
666
659
|
};
|
|
667
660
|
}
|
|
668
661
|
try {
|
|
669
|
-
const response = await
|
|
670
|
-
headers: {
|
|
671
|
-
Accept: 'application/json',
|
|
672
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
673
|
-
},
|
|
674
|
-
});
|
|
662
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
|
|
675
663
|
if (!response.ok) {
|
|
676
664
|
return {
|
|
677
665
|
package: packageNameForOutput,
|
|
@@ -797,12 +785,7 @@ export async function handleNpmTypes(args) {
|
|
|
797
785
|
};
|
|
798
786
|
}
|
|
799
787
|
try {
|
|
800
|
-
const response = await
|
|
801
|
-
headers: {
|
|
802
|
-
Accept: 'application/json',
|
|
803
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
804
|
-
},
|
|
805
|
-
});
|
|
788
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
|
|
806
789
|
if (!response.ok) {
|
|
807
790
|
return {
|
|
808
791
|
package: packageNameForOutput,
|
|
@@ -824,12 +807,7 @@ export async function handleNpmTypes(args) {
|
|
|
824
807
|
isAvailable: false,
|
|
825
808
|
};
|
|
826
809
|
try {
|
|
827
|
-
const typesResponse = await
|
|
828
|
-
headers: {
|
|
829
|
-
Accept: 'application/json',
|
|
830
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
831
|
-
},
|
|
832
|
-
});
|
|
810
|
+
const typesResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${typesPackageName}/latest`);
|
|
833
811
|
if (typesResponse.ok) {
|
|
834
812
|
const typesData = (await typesResponse.json());
|
|
835
813
|
typesPackageInfo = {
|
|
@@ -936,12 +914,7 @@ export async function handleNpmSize(args) {
|
|
|
936
914
|
};
|
|
937
915
|
}
|
|
938
916
|
try {
|
|
939
|
-
const response = await
|
|
940
|
-
headers: {
|
|
941
|
-
Accept: 'application/json',
|
|
942
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
943
|
-
},
|
|
944
|
-
});
|
|
917
|
+
const response = await fetchWithRetry(`https://bundlephobia.com/api/size?package=${bundlephobiaQuery}`);
|
|
945
918
|
if (!response.ok) {
|
|
946
919
|
let errorMsg = `Failed to fetch package size: ${response.status} ${response.statusText}`;
|
|
947
920
|
if (response.status === 404) {
|
|
@@ -1021,9 +994,7 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
|
|
|
1021
994
|
const encodedName = encodeURIComponent(pkgName);
|
|
1022
995
|
const encodedVersion = encodeURIComponent(version);
|
|
1023
996
|
const url = `https://api.deps.dev/v3/systems/npm/packages/${encodedName}/versions/${encodedVersion}:dependencies`;
|
|
1024
|
-
const response = await
|
|
1025
|
-
headers: { Accept: 'application/json', 'User-Agent': 'NPM-Sentinel-MCP' },
|
|
1026
|
-
});
|
|
997
|
+
const response = await fetchWithRetry(url);
|
|
1027
998
|
if (!response.ok) {
|
|
1028
999
|
console.warn(`deps.dev API returned ${response.status} for ${pkgName}@${version}`);
|
|
1029
1000
|
return [];
|
|
@@ -1046,9 +1017,7 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
|
|
|
1046
1017
|
// Helper to resolve 'latest' tag to actual version number
|
|
1047
1018
|
async function resolveLatestVersion(packageName) {
|
|
1048
1019
|
try {
|
|
1049
|
-
const response = await
|
|
1050
|
-
headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
|
|
1051
|
-
});
|
|
1020
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${packageName}/latest`);
|
|
1052
1021
|
if (!response.ok)
|
|
1053
1022
|
return null;
|
|
1054
1023
|
const data = (await response.json());
|
|
@@ -1069,9 +1038,7 @@ async function enrichVulnerabilityData(vulnId, ignoreCache = false) {
|
|
|
1069
1038
|
if (cached)
|
|
1070
1039
|
return cached;
|
|
1071
1040
|
try {
|
|
1072
|
-
const response = await
|
|
1073
|
-
headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
|
|
1074
|
-
});
|
|
1041
|
+
const response = await fetchWithRetry(`https://api.osv.dev/v1/vulns/${vulnId}`);
|
|
1075
1042
|
if (!response.ok)
|
|
1076
1043
|
return null;
|
|
1077
1044
|
const data = await response.json();
|
|
@@ -1218,7 +1185,7 @@ export async function handleNpmVulnerabilities(args) {
|
|
|
1218
1185
|
let apiResults = [];
|
|
1219
1186
|
if (finalBatchQueries.length > 0) {
|
|
1220
1187
|
// Perform Batch API call to OSV for non-cached items
|
|
1221
|
-
const response = await
|
|
1188
|
+
const response = await fetchWithRetry('https://api.osv.dev/v1/querybatch', {
|
|
1222
1189
|
method: 'POST',
|
|
1223
1190
|
headers: { 'Content-Type': 'application/json' },
|
|
1224
1191
|
body: JSON.stringify({ queries: finalBatchQueries }),
|
|
@@ -1370,12 +1337,7 @@ export async function handleNpmTrends(args) {
|
|
|
1370
1337
|
};
|
|
1371
1338
|
}
|
|
1372
1339
|
try {
|
|
1373
|
-
const response = await
|
|
1374
|
-
headers: {
|
|
1375
|
-
Accept: 'application/json',
|
|
1376
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1377
|
-
},
|
|
1378
|
-
});
|
|
1340
|
+
const response = await fetchWithRetry(`https://api.npmjs.org/downloads/point/${period}/${name}`);
|
|
1379
1341
|
if (!response.ok) {
|
|
1380
1342
|
let errorMsg = `Failed to fetch download trends: ${response.status} ${response.statusText}`;
|
|
1381
1343
|
if (response.status === 404) {
|
|
@@ -1532,7 +1494,7 @@ export async function handleNpmCompare(args) {
|
|
|
1532
1494
|
}
|
|
1533
1495
|
try {
|
|
1534
1496
|
// Fetch package version details from registry
|
|
1535
|
-
const pkgResponse = await
|
|
1497
|
+
const pkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
|
|
1536
1498
|
if (!pkgResponse.ok) {
|
|
1537
1499
|
throw new Error(`Failed to fetch package info for ${name}@${versionTag}: ${pkgResponse.status} ${pkgResponse.statusText}`);
|
|
1538
1500
|
}
|
|
@@ -1543,7 +1505,7 @@ export async function handleNpmCompare(args) {
|
|
|
1543
1505
|
// Fetch monthly downloads
|
|
1544
1506
|
let monthlyDownloads = null;
|
|
1545
1507
|
try {
|
|
1546
|
-
const downloadsResponse = await
|
|
1508
|
+
const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
|
|
1547
1509
|
if (downloadsResponse.ok) {
|
|
1548
1510
|
const downloadsData = await downloadsResponse.json();
|
|
1549
1511
|
if (isNpmDownloadsData(downloadsData)) {
|
|
@@ -1558,7 +1520,7 @@ export async function handleNpmCompare(args) {
|
|
|
1558
1520
|
// Need to fetch the full package info to get to the 'time' field for specific version
|
|
1559
1521
|
let publishDate = null;
|
|
1560
1522
|
try {
|
|
1561
|
-
const fullPkgInfoResponse = await
|
|
1523
|
+
const fullPkgInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
1562
1524
|
if (fullPkgInfoResponse.ok) {
|
|
1563
1525
|
const fullPkgInfo = await fullPkgInfoResponse.json();
|
|
1564
1526
|
if (isNpmPackageInfo(fullPkgInfo) && fullPkgInfo.time) {
|
|
@@ -1623,6 +1585,119 @@ export async function handleNpmCompare(args) {
|
|
|
1623
1585
|
};
|
|
1624
1586
|
}
|
|
1625
1587
|
}
|
|
1588
|
+
export async function getLocalPackageMetrics(name, ignoreCache = false) {
|
|
1589
|
+
const cacheKey = generateCacheKey('localMetrics', name);
|
|
1590
|
+
const cached = ignoreCache ? undefined : cacheGet(cacheKey);
|
|
1591
|
+
if (cached)
|
|
1592
|
+
return cached;
|
|
1593
|
+
// 1. Fetch full packument from registry
|
|
1594
|
+
const registryResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
1595
|
+
if (!registryResponse.ok) {
|
|
1596
|
+
throw new Error(`Package ${name} not found on registry.`);
|
|
1597
|
+
}
|
|
1598
|
+
const packageInfo = await registryResponse.json();
|
|
1599
|
+
if (!isNpmPackageInfo(packageInfo)) {
|
|
1600
|
+
throw new Error(`Invalid registry data received for ${name}.`);
|
|
1601
|
+
}
|
|
1602
|
+
const latestVersion = packageInfo['dist-tags']?.latest;
|
|
1603
|
+
if (!latestVersion || !packageInfo.versions?.[latestVersion]) {
|
|
1604
|
+
throw new Error(`No latest version found for ${name}.`);
|
|
1605
|
+
}
|
|
1606
|
+
const versionData = packageInfo.versions[latestVersion];
|
|
1607
|
+
const hasTypes = Boolean(versionData.types || versionData.typings || versionData.dependencies?.[`@types/${name}`]);
|
|
1608
|
+
const hasReadme = Boolean(versionData.readme || packageInfo.readme);
|
|
1609
|
+
const dependencyCount = Object.keys(versionData.dependencies || {}).length;
|
|
1610
|
+
// 2. Fetch downloads for last month
|
|
1611
|
+
let downloadsLastMonth = 0;
|
|
1612
|
+
try {
|
|
1613
|
+
const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
|
|
1614
|
+
if (downloadsResponse.ok) {
|
|
1615
|
+
const dlData = await downloadsResponse.json();
|
|
1616
|
+
downloadsLastMonth = dlData.downloads || 0;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
catch (dlError) {
|
|
1620
|
+
console.debug(`Could not fetch downloads for ${name}: ${dlError}`);
|
|
1621
|
+
}
|
|
1622
|
+
// 3. Fetch GitHub stats (optional, graceful degradation)
|
|
1623
|
+
let github = {
|
|
1624
|
+
starsCount: 0,
|
|
1625
|
+
forksCount: 0,
|
|
1626
|
+
subscribersCount: 0,
|
|
1627
|
+
openIssuesCount: 0,
|
|
1628
|
+
hasGitHubData: false,
|
|
1629
|
+
};
|
|
1630
|
+
const repoUrl = versionData.repository?.url || packageInfo.repository?.url;
|
|
1631
|
+
if (repoUrl?.includes('github.com')) {
|
|
1632
|
+
const githubMatch = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
1633
|
+
if (githubMatch) {
|
|
1634
|
+
const [, owner, repo] = githubMatch;
|
|
1635
|
+
const cleanRepo = repo.replace(/\.git$/, '');
|
|
1636
|
+
try {
|
|
1637
|
+
const ghResponse = await fetchWithRetry(`https://api.github.com/repos/${owner}/${cleanRepo}`, {}, { maxRetries: 0 });
|
|
1638
|
+
if (ghResponse.ok) {
|
|
1639
|
+
const ghData = await ghResponse.json();
|
|
1640
|
+
github = {
|
|
1641
|
+
starsCount: ghData.stargazers_count || 0,
|
|
1642
|
+
forksCount: ghData.forks_count || 0,
|
|
1643
|
+
subscribersCount: ghData.subscribers_count || 0,
|
|
1644
|
+
openIssuesCount: ghData.open_issues_count || 0,
|
|
1645
|
+
hasGitHubData: true,
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
catch (ghError) {
|
|
1650
|
+
console.debug(`Could not fetch GitHub data for ${owner}/${cleanRepo}: ${ghError}`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
// 4. Calculate publish age
|
|
1655
|
+
let lastPublishDaysAgo = 365;
|
|
1656
|
+
const timeObj = packageInfo.time;
|
|
1657
|
+
if (timeObj?.[latestVersion]) {
|
|
1658
|
+
const publishDate = new Date(timeObj[latestVersion]);
|
|
1659
|
+
const diffTime = Math.abs(Date.now() - publishDate.getTime());
|
|
1660
|
+
lastPublishDaysAgo = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
1661
|
+
}
|
|
1662
|
+
// 5. Scoring Algorithm
|
|
1663
|
+
const popularityNPM = Math.min(1, Math.log10(Math.max(1, downloadsLastMonth)) / 8);
|
|
1664
|
+
let popularity = popularityNPM;
|
|
1665
|
+
if (github.hasGitHubData) {
|
|
1666
|
+
const popularityGH = Math.min(1, github.starsCount / 50000);
|
|
1667
|
+
popularity = (popularityNPM * 0.7) + (popularityGH * 0.3);
|
|
1668
|
+
}
|
|
1669
|
+
const quality = ((hasTypes ? 0.3 : 0) +
|
|
1670
|
+
(hasReadme ? 0.2 : 0) +
|
|
1671
|
+
Math.max(0, 0.5 - (dependencyCount * 0.005)));
|
|
1672
|
+
const maintenancePublish = lastPublishDaysAgo < 30 ? 1.0
|
|
1673
|
+
: lastPublishDaysAgo < 90 ? 0.8
|
|
1674
|
+
: lastPublishDaysAgo < 365 ? 0.5
|
|
1675
|
+
: 0.2;
|
|
1676
|
+
let maintenance = maintenancePublish;
|
|
1677
|
+
if (github.hasGitHubData) {
|
|
1678
|
+
const issuesScore = github.openIssuesCount < 50 ? 0.3 : github.openIssuesCount < 200 ? 0.2 : 0.1;
|
|
1679
|
+
maintenance = (maintenancePublish * 0.7) + issuesScore;
|
|
1680
|
+
}
|
|
1681
|
+
const finalScore = popularity * 0.4 + quality * 0.3 + maintenance * 0.3;
|
|
1682
|
+
const result = {
|
|
1683
|
+
packageName: name,
|
|
1684
|
+
version: latestVersion,
|
|
1685
|
+
description: versionData.description || null,
|
|
1686
|
+
analyzedAt: new Date().toISOString(),
|
|
1687
|
+
downloadsLastMonth,
|
|
1688
|
+
github,
|
|
1689
|
+
score: {
|
|
1690
|
+
final: finalScore,
|
|
1691
|
+
detail: {
|
|
1692
|
+
quality,
|
|
1693
|
+
popularity,
|
|
1694
|
+
maintenance,
|
|
1695
|
+
},
|
|
1696
|
+
},
|
|
1697
|
+
};
|
|
1698
|
+
cacheSet(cacheKey, result, CACHE_TTL_LONG);
|
|
1699
|
+
return result;
|
|
1700
|
+
}
|
|
1626
1701
|
// Function to get package quality metrics
|
|
1627
1702
|
export async function handleNpmQuality(args) {
|
|
1628
1703
|
try {
|
|
@@ -1634,7 +1709,7 @@ export async function handleNpmQuality(args) {
|
|
|
1634
1709
|
let name = '';
|
|
1635
1710
|
if (typeof pkgInput === 'string') {
|
|
1636
1711
|
const atIdx = pkgInput.lastIndexOf('@');
|
|
1637
|
-
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
|
|
1712
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
|
|
1638
1713
|
}
|
|
1639
1714
|
else {
|
|
1640
1715
|
return {
|
|
@@ -1679,57 +1754,20 @@ export async function handleNpmQuality(args) {
|
|
|
1679
1754
|
};
|
|
1680
1755
|
}
|
|
1681
1756
|
try {
|
|
1682
|
-
const
|
|
1683
|
-
headers: {
|
|
1684
|
-
Accept: 'application/json',
|
|
1685
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1686
|
-
},
|
|
1687
|
-
});
|
|
1688
|
-
if (!response.ok) {
|
|
1689
|
-
let errorMsg = `Failed to fetch quality data: ${response.status} ${response.statusText}`;
|
|
1690
|
-
if (response.status === 404) {
|
|
1691
|
-
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1692
|
-
}
|
|
1693
|
-
return {
|
|
1694
|
-
packageInput: pkgInput,
|
|
1695
|
-
packageName: name,
|
|
1696
|
-
status: 'error',
|
|
1697
|
-
error: errorMsg,
|
|
1698
|
-
data: null,
|
|
1699
|
-
message: `Could not retrieve quality information for ${name}.`,
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
|
-
const rawData = await response.json();
|
|
1703
|
-
if (!isValidNpmsResponse(rawData)) {
|
|
1704
|
-
return {
|
|
1705
|
-
packageInput: pkgInput,
|
|
1706
|
-
packageName: name,
|
|
1707
|
-
status: 'error',
|
|
1708
|
-
error: 'Invalid or incomplete response from npms.io API for quality data',
|
|
1709
|
-
data: null,
|
|
1710
|
-
message: `Received malformed quality data for ${name}.`,
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
|
-
const { score, collected, analyzedAt } = rawData;
|
|
1714
|
-
const qualityScore = score.detail.quality;
|
|
1757
|
+
const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
|
|
1715
1758
|
const qualityData = {
|
|
1716
|
-
analyzedAt: analyzedAt,
|
|
1717
|
-
versionInScore:
|
|
1718
|
-
qualityScore:
|
|
1719
|
-
// Detailed sub-metrics like tests, coverage, linting, types are no longer directly provided
|
|
1720
|
-
// by the npms.io v2 API in the same way. The overall quality score is the primary metric.
|
|
1759
|
+
analyzedAt: metrics.analyzedAt,
|
|
1760
|
+
versionInScore: metrics.version,
|
|
1761
|
+
qualityScore: metrics.score.detail.quality,
|
|
1721
1762
|
};
|
|
1722
|
-
|
|
1723
|
-
? CACHE_TTL_SHORT
|
|
1724
|
-
: CACHE_TTL_LONG;
|
|
1725
|
-
cacheSet(cacheKey, qualityData, ttl);
|
|
1763
|
+
cacheSet(cacheKey, qualityData, CACHE_TTL_LONG);
|
|
1726
1764
|
return {
|
|
1727
1765
|
packageInput: pkgInput,
|
|
1728
1766
|
packageName: name,
|
|
1729
1767
|
status: 'success',
|
|
1730
1768
|
error: null,
|
|
1731
1769
|
data: qualityData,
|
|
1732
|
-
message: `Successfully fetched quality score for ${name} (version analyzed: ${
|
|
1770
|
+
message: `Successfully fetched quality score for ${name} (version analyzed: ${metrics.version}).`,
|
|
1733
1771
|
};
|
|
1734
1772
|
}
|
|
1735
1773
|
catch (error) {
|
|
@@ -1762,6 +1800,7 @@ export async function handleNpmQuality(args) {
|
|
|
1762
1800
|
};
|
|
1763
1801
|
}
|
|
1764
1802
|
}
|
|
1803
|
+
// Function to get package maintenance metrics
|
|
1765
1804
|
export async function handleNpmMaintenance(args) {
|
|
1766
1805
|
try {
|
|
1767
1806
|
const packagesToProcess = args.packages || [];
|
|
@@ -1817,55 +1856,20 @@ export async function handleNpmMaintenance(args) {
|
|
|
1817
1856
|
};
|
|
1818
1857
|
}
|
|
1819
1858
|
try {
|
|
1820
|
-
const
|
|
1821
|
-
headers: {
|
|
1822
|
-
Accept: 'application/json',
|
|
1823
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1824
|
-
},
|
|
1825
|
-
});
|
|
1826
|
-
if (!response.ok) {
|
|
1827
|
-
let errorMsg = `Failed to fetch maintenance data: ${response.status} ${response.statusText}`;
|
|
1828
|
-
if (response.status === 404) {
|
|
1829
|
-
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1830
|
-
}
|
|
1831
|
-
return {
|
|
1832
|
-
packageInput: pkgInput,
|
|
1833
|
-
packageName: name,
|
|
1834
|
-
status: 'error',
|
|
1835
|
-
error: errorMsg,
|
|
1836
|
-
data: null,
|
|
1837
|
-
message: `Could not retrieve maintenance information for ${name}.`,
|
|
1838
|
-
};
|
|
1839
|
-
}
|
|
1840
|
-
const rawData = await response.json();
|
|
1841
|
-
if (!isValidNpmsResponse(rawData)) {
|
|
1842
|
-
return {
|
|
1843
|
-
packageInput: pkgInput,
|
|
1844
|
-
packageName: name,
|
|
1845
|
-
status: 'error',
|
|
1846
|
-
error: 'Invalid or incomplete response from npms.io API for maintenance data',
|
|
1847
|
-
data: null,
|
|
1848
|
-
message: `Received malformed maintenance data for ${name}.`,
|
|
1849
|
-
};
|
|
1850
|
-
}
|
|
1851
|
-
const { score, collected, analyzedAt } = rawData;
|
|
1852
|
-
const maintenanceScoreValue = score.detail.maintenance;
|
|
1859
|
+
const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
|
|
1853
1860
|
const maintenanceData = {
|
|
1854
|
-
analyzedAt: analyzedAt,
|
|
1855
|
-
versionInScore:
|
|
1856
|
-
maintenanceScore:
|
|
1861
|
+
analyzedAt: metrics.analyzedAt,
|
|
1862
|
+
versionInScore: metrics.version,
|
|
1863
|
+
maintenanceScore: metrics.score.detail.maintenance,
|
|
1857
1864
|
};
|
|
1858
|
-
|
|
1859
|
-
? CACHE_TTL_SHORT
|
|
1860
|
-
: CACHE_TTL_LONG;
|
|
1861
|
-
cacheSet(cacheKey, maintenanceData, ttl);
|
|
1865
|
+
cacheSet(cacheKey, maintenanceData, CACHE_TTL_LONG);
|
|
1862
1866
|
return {
|
|
1863
1867
|
packageInput: pkgInput,
|
|
1864
1868
|
packageName: name,
|
|
1865
1869
|
status: 'success',
|
|
1866
1870
|
error: null,
|
|
1867
1871
|
data: maintenanceData,
|
|
1868
|
-
message: `Successfully fetched maintenance score for ${name} (version analyzed: ${
|
|
1872
|
+
message: `Successfully fetched maintenance score for ${name} (version analyzed: ${metrics.version}).`,
|
|
1869
1873
|
};
|
|
1870
1874
|
}
|
|
1871
1875
|
catch (error) {
|
|
@@ -2025,6 +2029,7 @@ export async function handleNpmMaintainers(args) {
|
|
|
2025
2029
|
};
|
|
2026
2030
|
}
|
|
2027
2031
|
}
|
|
2032
|
+
// Function to get package score
|
|
2028
2033
|
export async function handleNpmScore(args) {
|
|
2029
2034
|
try {
|
|
2030
2035
|
const packagesToProcess = args.packages || [];
|
|
@@ -2078,86 +2083,40 @@ export async function handleNpmScore(args) {
|
|
|
2078
2083
|
};
|
|
2079
2084
|
}
|
|
2080
2085
|
try {
|
|
2081
|
-
const
|
|
2082
|
-
if (!response.ok) {
|
|
2083
|
-
let errorMsg = `Failed to fetch package score: ${response.status} ${response.statusText}`;
|
|
2084
|
-
if (response.status === 404) {
|
|
2085
|
-
errorMsg = `Package ${name} not found on npms.io.`;
|
|
2086
|
-
}
|
|
2087
|
-
return {
|
|
2088
|
-
packageInput: pkgInput,
|
|
2089
|
-
packageName: name,
|
|
2090
|
-
status: 'error',
|
|
2091
|
-
error: errorMsg,
|
|
2092
|
-
data: null,
|
|
2093
|
-
};
|
|
2094
|
-
}
|
|
2095
|
-
const rawData = await response.json();
|
|
2096
|
-
if (!isValidNpmsResponse(rawData)) {
|
|
2097
|
-
return {
|
|
2098
|
-
packageInput: pkgInput,
|
|
2099
|
-
packageName: name,
|
|
2100
|
-
status: 'error',
|
|
2101
|
-
error: 'Invalid or incomplete response from npms.io API',
|
|
2102
|
-
data: null,
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
const { score, collected, analyzedAt } = rawData;
|
|
2106
|
-
const { detail } = score;
|
|
2107
|
-
// Calculate total downloads for the last month from the typically first entry in downloads array
|
|
2108
|
-
const lastMonthDownloads = collected.npm?.downloads?.find((d) => {
|
|
2109
|
-
// Heuristic: find a download period that is roughly 30 days
|
|
2110
|
-
const from = new Date(d.from);
|
|
2111
|
-
const to = new Date(d.to);
|
|
2112
|
-
const diffTime = Math.abs(to.getTime() - from.getTime());
|
|
2113
|
-
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
2114
|
-
return diffDays >= 28 && diffDays <= 31; // Common range for monthly data
|
|
2115
|
-
})?.count ||
|
|
2116
|
-
collected.npm?.downloads?.[0]?.count ||
|
|
2117
|
-
0;
|
|
2086
|
+
const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
|
|
2118
2087
|
const scoreData = {
|
|
2119
|
-
analyzedAt: analyzedAt,
|
|
2120
|
-
versionInScore:
|
|
2121
|
-
score:
|
|
2122
|
-
final: score.final,
|
|
2123
|
-
detail: {
|
|
2124
|
-
quality: detail.quality,
|
|
2125
|
-
popularity: detail.popularity,
|
|
2126
|
-
maintenance: detail.maintenance,
|
|
2127
|
-
},
|
|
2128
|
-
},
|
|
2088
|
+
analyzedAt: metrics.analyzedAt,
|
|
2089
|
+
versionInScore: metrics.version,
|
|
2090
|
+
score: metrics.score,
|
|
2129
2091
|
packageInfoFromScore: {
|
|
2130
|
-
name:
|
|
2131
|
-
version:
|
|
2132
|
-
description:
|
|
2092
|
+
name: metrics.packageName,
|
|
2093
|
+
version: metrics.version,
|
|
2094
|
+
description: metrics.description,
|
|
2133
2095
|
},
|
|
2134
2096
|
npmStats: {
|
|
2135
|
-
downloadsLastMonth:
|
|
2136
|
-
starsCount:
|
|
2097
|
+
downloadsLastMonth: metrics.downloadsLastMonth,
|
|
2098
|
+
starsCount: metrics.github.hasGitHubData ? metrics.github.starsCount : 0, // Fallback best effort
|
|
2137
2099
|
},
|
|
2138
|
-
githubStats:
|
|
2100
|
+
githubStats: metrics.github.hasGitHubData
|
|
2139
2101
|
? {
|
|
2140
|
-
starsCount:
|
|
2141
|
-
forksCount:
|
|
2142
|
-
subscribersCount:
|
|
2102
|
+
starsCount: metrics.github.starsCount,
|
|
2103
|
+
forksCount: metrics.github.forksCount,
|
|
2104
|
+
subscribersCount: metrics.github.subscribersCount,
|
|
2143
2105
|
issues: {
|
|
2144
|
-
count:
|
|
2145
|
-
openCount:
|
|
2106
|
+
count: metrics.github.openIssuesCount, // Heuristic default count = open issues
|
|
2107
|
+
openCount: metrics.github.openIssuesCount,
|
|
2146
2108
|
},
|
|
2147
2109
|
}
|
|
2148
2110
|
: null,
|
|
2149
2111
|
};
|
|
2150
|
-
|
|
2151
|
-
? CACHE_TTL_SHORT
|
|
2152
|
-
: CACHE_TTL_LONG;
|
|
2153
|
-
cacheSet(cacheKey, scoreData, ttl);
|
|
2112
|
+
cacheSet(cacheKey, scoreData, CACHE_TTL_LONG);
|
|
2154
2113
|
return {
|
|
2155
2114
|
packageInput: pkgInput,
|
|
2156
2115
|
packageName: name,
|
|
2157
2116
|
status: 'success',
|
|
2158
2117
|
error: null,
|
|
2159
2118
|
data: scoreData,
|
|
2160
|
-
message: `Successfully fetched score data for ${name} (version analyzed: ${
|
|
2119
|
+
message: `Successfully fetched score data for ${name} (version analyzed: ${metrics.version}).`,
|
|
2161
2120
|
};
|
|
2162
2121
|
}
|
|
2163
2122
|
catch (error) {
|
|
@@ -2190,6 +2149,27 @@ export async function handleNpmScore(args) {
|
|
|
2190
2149
|
};
|
|
2191
2150
|
}
|
|
2192
2151
|
}
|
|
2152
|
+
export async function fetchReadmeFromCDN(packageName, version) {
|
|
2153
|
+
const cdnUrls = [
|
|
2154
|
+
`https://cdn.jsdelivr.net/npm/${packageName}@${version}/README.md`,
|
|
2155
|
+
`https://unpkg.com/${packageName}@${version}/README.md`,
|
|
2156
|
+
];
|
|
2157
|
+
for (const cdnUrl of cdnUrls) {
|
|
2158
|
+
try {
|
|
2159
|
+
const response = await fetchWithRetry(cdnUrl, { headers: { Accept: 'text/plain' } }, { maxRetries: 1, skipThrottle: true });
|
|
2160
|
+
if (response.ok) {
|
|
2161
|
+
const text = await response.text();
|
|
2162
|
+
if (text && text.length > 10) {
|
|
2163
|
+
return text;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
catch {
|
|
2168
|
+
// Sigue intentando con la próxima URL
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return null;
|
|
2172
|
+
}
|
|
2193
2173
|
export async function handleNpmPackageReadme(args) {
|
|
2194
2174
|
try {
|
|
2195
2175
|
const packagesToProcess = args.packages || [];
|
|
@@ -2259,7 +2239,7 @@ export async function handleNpmPackageReadme(args) {
|
|
|
2259
2239
|
};
|
|
2260
2240
|
}
|
|
2261
2241
|
try {
|
|
2262
|
-
const response = await
|
|
2242
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
2263
2243
|
if (!response.ok) {
|
|
2264
2244
|
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
2265
2245
|
if (response.status === 404) {
|
|
@@ -2301,11 +2281,19 @@ export async function handleNpmPackageReadme(args) {
|
|
|
2301
2281
|
}
|
|
2302
2282
|
const versionData = packageInfo.versions[versionToUse];
|
|
2303
2283
|
// README can be in version-specific data or at the root of packageInfo
|
|
2304
|
-
|
|
2284
|
+
let readmeContent = versionData.readme || packageInfo.readme || null;
|
|
2285
|
+
let readmeSource = readmeContent ? 'registry' : null;
|
|
2286
|
+
if (!readmeContent && versionToUse) {
|
|
2287
|
+
readmeContent = await fetchReadmeFromCDN(name, versionToUse);
|
|
2288
|
+
if (readmeContent) {
|
|
2289
|
+
readmeSource = 'cdn';
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2305
2292
|
const hasReadme = !!readmeContent;
|
|
2306
2293
|
const readmeResultData = {
|
|
2307
2294
|
readme: readmeContent,
|
|
2308
2295
|
hasReadme: hasReadme,
|
|
2296
|
+
readmeSource: readmeSource,
|
|
2309
2297
|
versionFetched: versionToUse, // Store the actually fetched version
|
|
2310
2298
|
};
|
|
2311
2299
|
cacheSet(cacheKey, readmeResultData, CACHE_TTL_LONG);
|
|
@@ -2316,7 +2304,7 @@ export async function handleNpmPackageReadme(args) {
|
|
|
2316
2304
|
versionFetched: versionToUse,
|
|
2317
2305
|
status: 'success',
|
|
2318
2306
|
error: null,
|
|
2319
|
-
data: { readme: readmeContent, hasReadme: hasReadme }, // Return only readme and hasReadme in data field for consistency
|
|
2307
|
+
data: { readme: readmeContent, hasReadme: hasReadme, readmeSource: readmeSource }, // Return only readme and hasReadme in data field for consistency
|
|
2320
2308
|
message: `Successfully fetched README for ${name}@${versionToUse}.`,
|
|
2321
2309
|
};
|
|
2322
2310
|
}
|
|
@@ -2370,7 +2358,7 @@ export async function handleNpmSearch(args) {
|
|
|
2370
2358
|
message: `Search results for query '${query}' with limit ${limit} from cache.`,
|
|
2371
2359
|
};
|
|
2372
2360
|
}
|
|
2373
|
-
const response = await
|
|
2361
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
|
|
2374
2362
|
if (!response.ok) {
|
|
2375
2363
|
throw new Error(`Failed to search packages: ${response.status} ${response.statusText}`);
|
|
2376
2364
|
}
|
|
@@ -2508,7 +2496,7 @@ export async function handleNpmLicenseCompatibility(args) {
|
|
|
2508
2496
|
};
|
|
2509
2497
|
}
|
|
2510
2498
|
try {
|
|
2511
|
-
const response = await
|
|
2499
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
|
|
2512
2500
|
if (!response.ok) {
|
|
2513
2501
|
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
2514
2502
|
if (response.status === 404) {
|
|
@@ -2683,7 +2671,7 @@ export async function handleNpmRepoStats(args) {
|
|
|
2683
2671
|
};
|
|
2684
2672
|
}
|
|
2685
2673
|
try {
|
|
2686
|
-
const npmResponse = await
|
|
2674
|
+
const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/latest`);
|
|
2687
2675
|
if (!npmResponse.ok) {
|
|
2688
2676
|
const errorData = {
|
|
2689
2677
|
packageInput: pkgInput,
|
|
@@ -2736,10 +2724,9 @@ export async function handleNpmRepoStats(args) {
|
|
|
2736
2724
|
}
|
|
2737
2725
|
const [, owner, repo] = githubMatch;
|
|
2738
2726
|
const githubRepoApiUrl = `https://api.github.com/repos/${owner}/${repo.replace(/\.git$/, '')}`;
|
|
2739
|
-
const githubResponse = await
|
|
2727
|
+
const githubResponse = await fetchWithRetry(githubRepoApiUrl, {
|
|
2740
2728
|
headers: {
|
|
2741
2729
|
Accept: 'application/vnd.github.v3+json',
|
|
2742
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
2743
2730
|
},
|
|
2744
2731
|
});
|
|
2745
2732
|
if (!githubResponse.ok) {
|
|
@@ -2860,7 +2847,7 @@ export async function handleNpmDeprecated(args) {
|
|
|
2860
2847
|
}
|
|
2861
2848
|
// console.debug(`[handleNpmDeprecated] Cache miss for ${cacheKey}`);
|
|
2862
2849
|
try {
|
|
2863
|
-
const mainPkgResponse = await
|
|
2850
|
+
const mainPkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
2864
2851
|
if (!mainPkgResponse.ok) {
|
|
2865
2852
|
return {
|
|
2866
2853
|
package: initialPackageNameForOutput,
|
|
@@ -2902,7 +2889,7 @@ export async function handleNpmDeprecated(args) {
|
|
|
2902
2889
|
let statusMessage = '';
|
|
2903
2890
|
try {
|
|
2904
2891
|
// console.debug(`[handleNpmDeprecated] Checking dependency: ${depName}@${depSemVer}`);
|
|
2905
|
-
const depInfoResponse = await
|
|
2892
|
+
const depInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${encodeURIComponent(depName)}`);
|
|
2906
2893
|
if (!depInfoResponse.ok) {
|
|
2907
2894
|
statusMessage = `Could not fetch dependency info for '${depName}' (status: ${depInfoResponse.status}). Deprecation status unknown.`;
|
|
2908
2895
|
// console.warn(`[handleNpmDeprecated] ${statusMessage}`);
|
|
@@ -3079,7 +3066,7 @@ export async function handleNpmChangelogAnalysis(args) {
|
|
|
3079
3066
|
};
|
|
3080
3067
|
}
|
|
3081
3068
|
try {
|
|
3082
|
-
const npmResponse = await
|
|
3069
|
+
const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
3083
3070
|
if (!npmResponse.ok) {
|
|
3084
3071
|
const errorResult = {
|
|
3085
3072
|
packageInput: pkgInput,
|
|
@@ -3153,7 +3140,7 @@ export async function handleNpmChangelogAnalysis(args) {
|
|
|
3153
3140
|
for (const file of changelogFiles) {
|
|
3154
3141
|
try {
|
|
3155
3142
|
const rawChangelogUrl = `https://raw.githubusercontent.com/${owner}/${repoNameForUrl}/master/${file}`;
|
|
3156
|
-
const response = await
|
|
3143
|
+
const response = await fetchWithRetry(rawChangelogUrl);
|
|
3157
3144
|
if (response.ok) {
|
|
3158
3145
|
changelogContent = await response.text();
|
|
3159
3146
|
changelogSourceUrl = rawChangelogUrl;
|
|
@@ -3167,10 +3154,9 @@ export async function handleNpmChangelogAnalysis(args) {
|
|
|
3167
3154
|
}
|
|
3168
3155
|
let githubReleases = [];
|
|
3169
3156
|
try {
|
|
3170
|
-
const githubApiResponse = await
|
|
3157
|
+
const githubApiResponse = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
|
|
3171
3158
|
headers: {
|
|
3172
3159
|
Accept: 'application/vnd.github.v3+json',
|
|
3173
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
3174
3160
|
},
|
|
3175
3161
|
});
|
|
3176
3162
|
if (githubApiResponse.ok) {
|
|
@@ -3258,12 +3244,10 @@ export async function handleNpmAlternatives(args) {
|
|
|
3258
3244
|
}
|
|
3259
3245
|
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
3260
3246
|
let originalPackageName = '';
|
|
3261
|
-
let versionQueried;
|
|
3262
3247
|
if (typeof pkgInput === 'string') {
|
|
3263
3248
|
const atIdx = pkgInput.lastIndexOf('@');
|
|
3264
3249
|
if (atIdx > 0) {
|
|
3265
3250
|
originalPackageName = pkgInput.slice(0, atIdx);
|
|
3266
|
-
versionQueried = pkgInput.slice(atIdx + 1);
|
|
3267
3251
|
}
|
|
3268
3252
|
else {
|
|
3269
3253
|
originalPackageName = pkgInput;
|
|
@@ -3312,7 +3296,7 @@ export async function handleNpmAlternatives(args) {
|
|
|
3312
3296
|
};
|
|
3313
3297
|
}
|
|
3314
3298
|
try {
|
|
3315
|
-
const searchResponse = await
|
|
3299
|
+
const searchResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=10`);
|
|
3316
3300
|
if (!searchResponse.ok) {
|
|
3317
3301
|
const errorResult = {
|
|
3318
3302
|
packageInput: pkgInput,
|
|
@@ -3328,7 +3312,7 @@ export async function handleNpmAlternatives(args) {
|
|
|
3328
3312
|
const alternativePackagesRaw = searchData.objects || [];
|
|
3329
3313
|
let originalPackageDownloads = 0;
|
|
3330
3314
|
try {
|
|
3331
|
-
const dlResponse = await
|
|
3315
|
+
const dlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
|
|
3332
3316
|
if (dlResponse.ok) {
|
|
3333
3317
|
originalPackageDownloads =
|
|
3334
3318
|
(await dlResponse.json()).downloads || 0;
|
|
@@ -3364,7 +3348,7 @@ export async function handleNpmAlternatives(args) {
|
|
|
3364
3348
|
.map(async (alt) => {
|
|
3365
3349
|
let altDownloads = 0;
|
|
3366
3350
|
try {
|
|
3367
|
-
const altDlResponse = await
|
|
3351
|
+
const altDlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
|
|
3368
3352
|
if (altDlResponse.ok) {
|
|
3369
3353
|
altDownloads = (await altDlResponse.json()).downloads || 0;
|
|
3370
3354
|
}
|
|
@@ -3445,7 +3429,7 @@ export default function createServer({ config }) {
|
|
|
3445
3429
|
__filename = fileURLToPath(import.meta.url);
|
|
3446
3430
|
__dirname = path.dirname(__filename);
|
|
3447
3431
|
}
|
|
3448
|
-
catch
|
|
3432
|
+
catch {
|
|
3449
3433
|
// Fallback for CJS environment (Smithery HTTP build)
|
|
3450
3434
|
__filename = process.argv[1] || '.';
|
|
3451
3435
|
__dirname = path.dirname(__filename);
|
|
@@ -3884,7 +3868,7 @@ function isNpmPackageVersionData(data) {
|
|
|
3884
3868
|
}
|
|
3885
3869
|
return result.success;
|
|
3886
3870
|
}
|
|
3887
|
-
catch
|
|
3871
|
+
catch {
|
|
3888
3872
|
// This catch block might not be strictly necessary with safeParse but kept for safety
|
|
3889
3873
|
// console.error("isNpmPackageVersionData validation failed unexpectedly:", e);
|
|
3890
3874
|
return false;
|