@nekzus/mcp-server 1.18.3 → 1.19.0-alpha.10
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 +162 -283
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +60 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +488 -785
- package/dist/index.js.map +1 -1
- package/dist/src/cache.d.ts +22 -0
- package/dist/src/cache.d.ts.map +1 -0
- package/dist/src/cache.js +68 -0
- package/dist/src/cache.js.map +1 -0
- package/dist/src/config.d.ts +11 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +45 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/handlers/index.d.ts +2 -0
- package/dist/src/handlers/index.d.ts.map +1 -0
- package/dist/src/handlers/index.js +2 -0
- package/dist/src/handlers/index.js.map +1 -0
- package/dist/src/icons.d.ts +21 -0
- package/dist/src/icons.d.ts.map +1 -0
- package/dist/src/icons.js +29 -0
- package/dist/src/icons.js.map +1 -0
- package/dist/src/prompts/index.d.ts +3 -0
- package/dist/src/prompts/index.d.ts.map +1 -0
- package/dist/src/prompts/index.js +22 -0
- package/dist/src/prompts/index.js.map +1 -0
- package/dist/src/resources/index.d.ts +3 -0
- package/dist/src/resources/index.d.ts.map +1 -0
- package/dist/src/resources/index.js +62 -0
- package/dist/src/resources/index.js.map +1 -0
- package/dist/src/schemas.d.ts +143 -0
- package/dist/src/schemas.d.ts.map +1 -0
- package/dist/src/schemas.js +189 -0
- package/dist/src/schemas.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +21 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tools/index.d.ts +3 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +411 -0
- package/dist/src/tools/index.js.map +1 -0
- package/dist/src/utils/fetch-retry.d.ts +5 -0
- package/dist/src/utils/fetch-retry.d.ts.map +1 -0
- package/dist/src/utils/fetch-retry.js +69 -0
- package/dist/src/utils/fetch-retry.js.map +1 -0
- package/llms-full.txt +312 -422
- package/package.json +16 -17
- package/smithery.yaml +10 -2
package/dist/index.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
import * as crypto from 'node:crypto';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
8
6
|
import fetch from 'node-fetch';
|
|
9
7
|
import { z } from 'zod';
|
|
8
|
+
import createServer from './src/server.js';
|
|
10
9
|
// Cache configuration
|
|
11
|
-
let NPM_REGISTRY_URL = (process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org').replace(/\/$/, '');
|
|
10
|
+
export let NPM_REGISTRY_URL = (process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org').replace(/\/$/, '');
|
|
11
|
+
export function setNpmRegistryUrl(url) {
|
|
12
|
+
NPM_REGISTRY_URL = url.replace(/\/$/, '');
|
|
13
|
+
}
|
|
12
14
|
// Cache configuration
|
|
13
|
-
const CACHE_TTL_SHORT = 15 * 60 * 1000; // 15 minutes
|
|
14
15
|
const CACHE_TTL_MEDIUM = 60 * 60 * 1000; // 1 hour
|
|
15
16
|
const CACHE_TTL_LONG = 6 * 60 * 60 * 1000; // 6 hours
|
|
16
17
|
const CACHE_TTL_VERY_LONG = 24 * 60 * 60 * 1000; // 24 hours
|
|
@@ -73,6 +74,76 @@ function cacheSet(key, value, ttlMilliseconds) {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
// HTTP wrapper configuration
|
|
78
|
+
const HTTP_MAX_RETRIES = 3;
|
|
79
|
+
const HTTP_INITIAL_BACKOFF_MS = 1000;
|
|
80
|
+
const HTTP_MAX_CONCURRENT_REQUESTS = 5;
|
|
81
|
+
const DEFAULT_HEADERS = {
|
|
82
|
+
Accept: 'application/json',
|
|
83
|
+
'User-Agent': 'NPM-Sentinel-MCP/1.x',
|
|
84
|
+
};
|
|
85
|
+
// Semaphore for concurrency control
|
|
86
|
+
let activeRequests = 0;
|
|
87
|
+
const requestQueue = [];
|
|
88
|
+
function acquireSlot() {
|
|
89
|
+
if (activeRequests < HTTP_MAX_CONCURRENT_REQUESTS) {
|
|
90
|
+
activeRequests++;
|
|
91
|
+
return Promise.resolve();
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
requestQueue.push(() => {
|
|
95
|
+
activeRequests++;
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function releaseSlot() {
|
|
101
|
+
activeRequests--;
|
|
102
|
+
const next = requestQueue.shift();
|
|
103
|
+
if (next)
|
|
104
|
+
next();
|
|
105
|
+
}
|
|
106
|
+
export async function fetchWithRetry(url, options = {}, config = {}) {
|
|
107
|
+
const maxRetries = config.maxRetries ?? HTTP_MAX_RETRIES;
|
|
108
|
+
const mergedHeaders = { ...DEFAULT_HEADERS, ...(options.headers || {}) };
|
|
109
|
+
const mergedOptions = { ...options, headers: mergedHeaders };
|
|
110
|
+
if (!config.skipThrottle) {
|
|
111
|
+
await acquireSlot();
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(url, mergedOptions);
|
|
117
|
+
if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
|
|
118
|
+
if (attempt < maxRetries) {
|
|
119
|
+
const retryAfter = response.headers.get('retry-after');
|
|
120
|
+
const waitMs = retryAfter
|
|
121
|
+
? parseInt(retryAfter, 10) * 1000
|
|
122
|
+
: HTTP_INITIAL_BACKOFF_MS * 2 ** attempt;
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
if (attempt < maxRetries) {
|
|
131
|
+
const waitMs = HTTP_INITIAL_BACKOFF_MS * 2 ** attempt;
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Fallback
|
|
139
|
+
return await fetch(url, mergedOptions);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (!config.skipThrottle) {
|
|
143
|
+
releaseSlot();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
76
147
|
// Zod schemas for npm package data
|
|
77
148
|
export const NpmMaintainerSchema = z
|
|
78
149
|
.object({
|
|
@@ -171,64 +242,7 @@ export const NpmDownloadsDataSchema = z.object({
|
|
|
171
242
|
end: z.string(),
|
|
172
243
|
package: z.string(),
|
|
173
244
|
});
|
|
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
|
-
}
|
|
245
|
+
// Schema for search results from npm registry v1 search api
|
|
232
246
|
export const NpmSearchResultSchema = z
|
|
233
247
|
.object({
|
|
234
248
|
objects: z.array(z.object({
|
|
@@ -266,15 +280,6 @@ export const NpmSearchResultSchema = z
|
|
|
266
280
|
total: z.number(), // total is a sibling of objects
|
|
267
281
|
})
|
|
268
282
|
.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
283
|
// Type guards for API responses
|
|
279
284
|
function isNpmPackageInfo(data) {
|
|
280
285
|
return (typeof data === 'object' &&
|
|
@@ -331,11 +336,24 @@ function isValidNpmPackageName(name) {
|
|
|
331
336
|
!name.startsWith('_') &&
|
|
332
337
|
!name.startsWith('.'));
|
|
333
338
|
}
|
|
339
|
+
export function createEmptyArrayErrorResponse(toolName) {
|
|
340
|
+
const errorResponse = JSON.stringify({
|
|
341
|
+
queryPackages: [],
|
|
342
|
+
results: [],
|
|
343
|
+
status: 'error',
|
|
344
|
+
error: 'No package names provided in request',
|
|
345
|
+
message: `The packages parameter for ${toolName} must contain at least one package name.`,
|
|
346
|
+
}, null, 2);
|
|
347
|
+
return {
|
|
348
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
349
|
+
isError: true,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
334
352
|
export async function handleNpmVersions(args) {
|
|
335
353
|
try {
|
|
336
354
|
const packagesToProcess = args.packages || [];
|
|
337
355
|
if (packagesToProcess.length === 0) {
|
|
338
|
-
|
|
356
|
+
return createEmptyArrayErrorResponse('npmVersions');
|
|
339
357
|
}
|
|
340
358
|
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
341
359
|
let name = '';
|
|
@@ -391,12 +409,7 @@ export async function handleNpmVersions(args) {
|
|
|
391
409
|
};
|
|
392
410
|
}
|
|
393
411
|
try {
|
|
394
|
-
const response = await
|
|
395
|
-
headers: {
|
|
396
|
-
Accept: 'application/json',
|
|
397
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
398
|
-
},
|
|
399
|
-
});
|
|
412
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
400
413
|
if (!response.ok) {
|
|
401
414
|
return {
|
|
402
415
|
packageInput: pkgInput,
|
|
@@ -465,7 +478,7 @@ export async function handleNpmLatest(args) {
|
|
|
465
478
|
try {
|
|
466
479
|
const packagesToProcess = args.packages || [];
|
|
467
480
|
if (packagesToProcess.length === 0) {
|
|
468
|
-
|
|
481
|
+
return createEmptyArrayErrorResponse('npmLatest');
|
|
469
482
|
}
|
|
470
483
|
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
471
484
|
let name = '';
|
|
@@ -527,12 +540,7 @@ export async function handleNpmLatest(args) {
|
|
|
527
540
|
};
|
|
528
541
|
}
|
|
529
542
|
try {
|
|
530
|
-
const response = await
|
|
531
|
-
headers: {
|
|
532
|
-
Accept: 'application/json',
|
|
533
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
534
|
-
},
|
|
535
|
-
});
|
|
543
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
|
|
536
544
|
if (!response.ok) {
|
|
537
545
|
let errorMsg = `Failed to fetch package version: ${response.status} ${response.statusText}`;
|
|
538
546
|
if (response.status === 404) {
|
|
@@ -666,12 +674,7 @@ export async function handleNpmDeps(args) {
|
|
|
666
674
|
};
|
|
667
675
|
}
|
|
668
676
|
try {
|
|
669
|
-
const response = await
|
|
670
|
-
headers: {
|
|
671
|
-
Accept: 'application/json',
|
|
672
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
673
|
-
},
|
|
674
|
-
});
|
|
677
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
|
|
675
678
|
if (!response.ok) {
|
|
676
679
|
return {
|
|
677
680
|
package: packageNameForOutput,
|
|
@@ -797,12 +800,7 @@ export async function handleNpmTypes(args) {
|
|
|
797
800
|
};
|
|
798
801
|
}
|
|
799
802
|
try {
|
|
800
|
-
const response = await
|
|
801
|
-
headers: {
|
|
802
|
-
Accept: 'application/json',
|
|
803
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
804
|
-
},
|
|
805
|
-
});
|
|
803
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
|
|
806
804
|
if (!response.ok) {
|
|
807
805
|
return {
|
|
808
806
|
package: packageNameForOutput,
|
|
@@ -824,12 +822,7 @@ export async function handleNpmTypes(args) {
|
|
|
824
822
|
isAvailable: false,
|
|
825
823
|
};
|
|
826
824
|
try {
|
|
827
|
-
const typesResponse = await
|
|
828
|
-
headers: {
|
|
829
|
-
Accept: 'application/json',
|
|
830
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
831
|
-
},
|
|
832
|
-
});
|
|
825
|
+
const typesResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${typesPackageName}/latest`);
|
|
833
826
|
if (typesResponse.ok) {
|
|
834
827
|
const typesData = (await typesResponse.json());
|
|
835
828
|
typesPackageInfo = {
|
|
@@ -936,12 +929,7 @@ export async function handleNpmSize(args) {
|
|
|
936
929
|
};
|
|
937
930
|
}
|
|
938
931
|
try {
|
|
939
|
-
const response = await
|
|
940
|
-
headers: {
|
|
941
|
-
Accept: 'application/json',
|
|
942
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
943
|
-
},
|
|
944
|
-
});
|
|
932
|
+
const response = await fetchWithRetry(`https://bundlephobia.com/api/size?package=${bundlephobiaQuery}`);
|
|
945
933
|
if (!response.ok) {
|
|
946
934
|
let errorMsg = `Failed to fetch package size: ${response.status} ${response.statusText}`;
|
|
947
935
|
if (response.status === 404) {
|
|
@@ -1021,9 +1009,7 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
|
|
|
1021
1009
|
const encodedName = encodeURIComponent(pkgName);
|
|
1022
1010
|
const encodedVersion = encodeURIComponent(version);
|
|
1023
1011
|
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
|
-
});
|
|
1012
|
+
const response = await fetchWithRetry(url);
|
|
1027
1013
|
if (!response.ok) {
|
|
1028
1014
|
console.warn(`deps.dev API returned ${response.status} for ${pkgName}@${version}`);
|
|
1029
1015
|
return [];
|
|
@@ -1043,12 +1029,46 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
|
|
|
1043
1029
|
return [];
|
|
1044
1030
|
}
|
|
1045
1031
|
}
|
|
1032
|
+
export async function fetchRepoStatsFromDepsDev(owner, repo, ignoreCache = false) {
|
|
1033
|
+
const projectKey = encodeURIComponent(`github.com/${owner}/${repo}`);
|
|
1034
|
+
const cacheKey = generateCacheKey('depsDevProject', owner, repo);
|
|
1035
|
+
const cached = ignoreCache ? undefined : cacheGet(cacheKey);
|
|
1036
|
+
if (cached)
|
|
1037
|
+
return cached;
|
|
1038
|
+
try {
|
|
1039
|
+
const response = await fetchWithRetry(`https://api.deps.dev/v3alpha/projects/${projectKey}`, {}, { maxRetries: 1 });
|
|
1040
|
+
if (!response.ok)
|
|
1041
|
+
return null;
|
|
1042
|
+
const data = (await response.json());
|
|
1043
|
+
const result = {
|
|
1044
|
+
starsCount: data.starsCount ?? 0,
|
|
1045
|
+
forksCount: data.forksCount ?? 0,
|
|
1046
|
+
openIssuesCount: data.openIssuesCount ?? 0,
|
|
1047
|
+
license: data.license ?? 'unknown',
|
|
1048
|
+
description: data.description ?? '',
|
|
1049
|
+
homepage: data.homepage ?? '',
|
|
1050
|
+
scorecard: data.scorecard
|
|
1051
|
+
? {
|
|
1052
|
+
overallScore: data.scorecard.overallScore,
|
|
1053
|
+
checks: (data.scorecard.checks || []).map((c) => ({
|
|
1054
|
+
name: c.name,
|
|
1055
|
+
score: c.score,
|
|
1056
|
+
reason: c.reason,
|
|
1057
|
+
})),
|
|
1058
|
+
}
|
|
1059
|
+
: undefined,
|
|
1060
|
+
};
|
|
1061
|
+
cacheSet(cacheKey, result, CACHE_TTL_VERY_LONG);
|
|
1062
|
+
return result;
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1046
1068
|
// Helper to resolve 'latest' tag to actual version number
|
|
1047
1069
|
async function resolveLatestVersion(packageName) {
|
|
1048
1070
|
try {
|
|
1049
|
-
const response = await
|
|
1050
|
-
headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
|
|
1051
|
-
});
|
|
1071
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${packageName}/latest`);
|
|
1052
1072
|
if (!response.ok)
|
|
1053
1073
|
return null;
|
|
1054
1074
|
const data = (await response.json());
|
|
@@ -1069,9 +1089,7 @@ async function enrichVulnerabilityData(vulnId, ignoreCache = false) {
|
|
|
1069
1089
|
if (cached)
|
|
1070
1090
|
return cached;
|
|
1071
1091
|
try {
|
|
1072
|
-
const response = await
|
|
1073
|
-
headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
|
|
1074
|
-
});
|
|
1092
|
+
const response = await fetchWithRetry(`https://api.osv.dev/v1/vulns/${vulnId}`);
|
|
1075
1093
|
if (!response.ok)
|
|
1076
1094
|
return null;
|
|
1077
1095
|
const data = await response.json();
|
|
@@ -1218,7 +1236,7 @@ export async function handleNpmVulnerabilities(args) {
|
|
|
1218
1236
|
let apiResults = [];
|
|
1219
1237
|
if (finalBatchQueries.length > 0) {
|
|
1220
1238
|
// Perform Batch API call to OSV for non-cached items
|
|
1221
|
-
const response = await
|
|
1239
|
+
const response = await fetchWithRetry('https://api.osv.dev/v1/querybatch', {
|
|
1222
1240
|
method: 'POST',
|
|
1223
1241
|
headers: { 'Content-Type': 'application/json' },
|
|
1224
1242
|
body: JSON.stringify({ queries: finalBatchQueries }),
|
|
@@ -1370,12 +1388,7 @@ export async function handleNpmTrends(args) {
|
|
|
1370
1388
|
};
|
|
1371
1389
|
}
|
|
1372
1390
|
try {
|
|
1373
|
-
const response = await
|
|
1374
|
-
headers: {
|
|
1375
|
-
Accept: 'application/json',
|
|
1376
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1377
|
-
},
|
|
1378
|
-
});
|
|
1391
|
+
const response = await fetchWithRetry(`https://api.npmjs.org/downloads/point/${period}/${name}`);
|
|
1379
1392
|
if (!response.ok) {
|
|
1380
1393
|
let errorMsg = `Failed to fetch download trends: ${response.status} ${response.statusText}`;
|
|
1381
1394
|
if (response.status === 404) {
|
|
@@ -1532,7 +1545,7 @@ export async function handleNpmCompare(args) {
|
|
|
1532
1545
|
}
|
|
1533
1546
|
try {
|
|
1534
1547
|
// Fetch package version details from registry
|
|
1535
|
-
const pkgResponse = await
|
|
1548
|
+
const pkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
|
|
1536
1549
|
if (!pkgResponse.ok) {
|
|
1537
1550
|
throw new Error(`Failed to fetch package info for ${name}@${versionTag}: ${pkgResponse.status} ${pkgResponse.statusText}`);
|
|
1538
1551
|
}
|
|
@@ -1543,7 +1556,7 @@ export async function handleNpmCompare(args) {
|
|
|
1543
1556
|
// Fetch monthly downloads
|
|
1544
1557
|
let monthlyDownloads = null;
|
|
1545
1558
|
try {
|
|
1546
|
-
const downloadsResponse = await
|
|
1559
|
+
const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
|
|
1547
1560
|
if (downloadsResponse.ok) {
|
|
1548
1561
|
const downloadsData = await downloadsResponse.json();
|
|
1549
1562
|
if (isNpmDownloadsData(downloadsData)) {
|
|
@@ -1558,7 +1571,7 @@ export async function handleNpmCompare(args) {
|
|
|
1558
1571
|
// Need to fetch the full package info to get to the 'time' field for specific version
|
|
1559
1572
|
let publishDate = null;
|
|
1560
1573
|
try {
|
|
1561
|
-
const fullPkgInfoResponse = await
|
|
1574
|
+
const fullPkgInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
1562
1575
|
if (fullPkgInfoResponse.ok) {
|
|
1563
1576
|
const fullPkgInfo = await fullPkgInfoResponse.json();
|
|
1564
1577
|
if (isNpmPackageInfo(fullPkgInfo) && fullPkgInfo.time) {
|
|
@@ -1623,6 +1636,122 @@ export async function handleNpmCompare(args) {
|
|
|
1623
1636
|
};
|
|
1624
1637
|
}
|
|
1625
1638
|
}
|
|
1639
|
+
export async function getLocalPackageMetrics(name, ignoreCache = false) {
|
|
1640
|
+
const cacheKey = generateCacheKey('localMetrics', name);
|
|
1641
|
+
const cached = ignoreCache ? undefined : cacheGet(cacheKey);
|
|
1642
|
+
if (cached)
|
|
1643
|
+
return cached;
|
|
1644
|
+
// 1. Fetch full packument from registry
|
|
1645
|
+
const registryResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
1646
|
+
if (!registryResponse.ok) {
|
|
1647
|
+
throw new Error(`Package ${name} not found on registry.`);
|
|
1648
|
+
}
|
|
1649
|
+
const packageInfo = await registryResponse.json();
|
|
1650
|
+
if (!isNpmPackageInfo(packageInfo)) {
|
|
1651
|
+
throw new Error(`Invalid registry data received for ${name}.`);
|
|
1652
|
+
}
|
|
1653
|
+
const latestVersion = packageInfo['dist-tags']?.latest;
|
|
1654
|
+
if (!latestVersion || !packageInfo.versions?.[latestVersion]) {
|
|
1655
|
+
throw new Error(`No latest version found for ${name}.`);
|
|
1656
|
+
}
|
|
1657
|
+
const versionData = packageInfo.versions[latestVersion];
|
|
1658
|
+
const hasTypes = Boolean(versionData.types || versionData.typings || versionData.dependencies?.[`@types/${name}`]);
|
|
1659
|
+
const hasReadme = Boolean(versionData.readme || packageInfo.readme);
|
|
1660
|
+
const dependencyCount = Object.keys(versionData.dependencies || {}).length;
|
|
1661
|
+
// 2. Fetch downloads for last month
|
|
1662
|
+
let downloadsLastMonth = 0;
|
|
1663
|
+
try {
|
|
1664
|
+
const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
|
|
1665
|
+
if (downloadsResponse.ok) {
|
|
1666
|
+
const dlData = await downloadsResponse.json();
|
|
1667
|
+
downloadsLastMonth = dlData.downloads || 0;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
catch (dlError) {
|
|
1671
|
+
console.debug(`Could not fetch downloads for ${name}: ${dlError}`);
|
|
1672
|
+
}
|
|
1673
|
+
// 3. Fetch GitHub stats via deps.dev (optional, graceful degradation)
|
|
1674
|
+
let github = {
|
|
1675
|
+
starsCount: 0,
|
|
1676
|
+
forksCount: 0,
|
|
1677
|
+
subscribersCount: 0,
|
|
1678
|
+
openIssuesCount: 0,
|
|
1679
|
+
hasGitHubData: false,
|
|
1680
|
+
};
|
|
1681
|
+
let scorecard;
|
|
1682
|
+
const repoUrl = versionData.repository?.url || packageInfo.repository?.url;
|
|
1683
|
+
if (repoUrl?.includes('github.com')) {
|
|
1684
|
+
const githubMatch = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
1685
|
+
if (githubMatch) {
|
|
1686
|
+
const [, owner, repo] = githubMatch;
|
|
1687
|
+
const cleanRepo = repo.replace(/\.git$/, '');
|
|
1688
|
+
try {
|
|
1689
|
+
const depsDevData = await fetchRepoStatsFromDepsDev(owner, cleanRepo, ignoreCache);
|
|
1690
|
+
if (depsDevData) {
|
|
1691
|
+
github = {
|
|
1692
|
+
starsCount: depsDevData.starsCount,
|
|
1693
|
+
forksCount: depsDevData.forksCount,
|
|
1694
|
+
subscribersCount: 0,
|
|
1695
|
+
openIssuesCount: depsDevData.openIssuesCount,
|
|
1696
|
+
hasGitHubData: true,
|
|
1697
|
+
};
|
|
1698
|
+
scorecard = depsDevData.scorecard;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
catch (ghError) {
|
|
1702
|
+
console.debug(`Could not fetch deps.dev project data for ${owner}/${cleanRepo}: ${ghError}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
// 4. Calculate publish age
|
|
1707
|
+
let lastPublishDaysAgo = 365;
|
|
1708
|
+
const timeObj = packageInfo.time;
|
|
1709
|
+
if (timeObj?.[latestVersion]) {
|
|
1710
|
+
const publishDate = new Date(timeObj[latestVersion]);
|
|
1711
|
+
const diffTime = Math.abs(Date.now() - publishDate.getTime());
|
|
1712
|
+
lastPublishDaysAgo = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
1713
|
+
}
|
|
1714
|
+
// 5. Scoring Algorithm
|
|
1715
|
+
const popularityNPM = Math.min(1, Math.log10(Math.max(1, downloadsLastMonth)) / 8);
|
|
1716
|
+
let popularity = popularityNPM;
|
|
1717
|
+
if (github.hasGitHubData) {
|
|
1718
|
+
const popularityGH = Math.min(1, github.starsCount / 50000);
|
|
1719
|
+
popularity = popularityNPM * 0.7 + popularityGH * 0.3;
|
|
1720
|
+
}
|
|
1721
|
+
const quality = (hasTypes ? 0.3 : 0) + (hasReadme ? 0.2 : 0) + Math.max(0, 0.5 - dependencyCount * 0.005);
|
|
1722
|
+
const maintenancePublish = lastPublishDaysAgo < 30
|
|
1723
|
+
? 1.0
|
|
1724
|
+
: lastPublishDaysAgo < 90
|
|
1725
|
+
? 0.8
|
|
1726
|
+
: lastPublishDaysAgo < 365
|
|
1727
|
+
? 0.5
|
|
1728
|
+
: 0.2;
|
|
1729
|
+
let maintenance = maintenancePublish;
|
|
1730
|
+
if (github.hasGitHubData) {
|
|
1731
|
+
const issuesScore = github.openIssuesCount < 50 ? 0.3 : github.openIssuesCount < 200 ? 0.2 : 0.1;
|
|
1732
|
+
maintenance = maintenancePublish * 0.7 + issuesScore;
|
|
1733
|
+
}
|
|
1734
|
+
const finalScore = popularity * 0.4 + quality * 0.3 + maintenance * 0.3;
|
|
1735
|
+
const result = {
|
|
1736
|
+
packageName: name,
|
|
1737
|
+
version: latestVersion,
|
|
1738
|
+
description: versionData.description || null,
|
|
1739
|
+
analyzedAt: new Date().toISOString(),
|
|
1740
|
+
downloadsLastMonth,
|
|
1741
|
+
github,
|
|
1742
|
+
score: {
|
|
1743
|
+
final: finalScore,
|
|
1744
|
+
detail: {
|
|
1745
|
+
quality,
|
|
1746
|
+
popularity,
|
|
1747
|
+
maintenance,
|
|
1748
|
+
},
|
|
1749
|
+
},
|
|
1750
|
+
scorecard,
|
|
1751
|
+
};
|
|
1752
|
+
cacheSet(cacheKey, result, CACHE_TTL_LONG);
|
|
1753
|
+
return result;
|
|
1754
|
+
}
|
|
1626
1755
|
// Function to get package quality metrics
|
|
1627
1756
|
export async function handleNpmQuality(args) {
|
|
1628
1757
|
try {
|
|
@@ -1634,7 +1763,7 @@ export async function handleNpmQuality(args) {
|
|
|
1634
1763
|
let name = '';
|
|
1635
1764
|
if (typeof pkgInput === 'string') {
|
|
1636
1765
|
const atIdx = pkgInput.lastIndexOf('@');
|
|
1637
|
-
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
|
|
1766
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
|
|
1638
1767
|
}
|
|
1639
1768
|
else {
|
|
1640
1769
|
return {
|
|
@@ -1679,57 +1808,20 @@ export async function handleNpmQuality(args) {
|
|
|
1679
1808
|
};
|
|
1680
1809
|
}
|
|
1681
1810
|
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;
|
|
1811
|
+
const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
|
|
1715
1812
|
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.
|
|
1813
|
+
analyzedAt: metrics.analyzedAt,
|
|
1814
|
+
versionInScore: metrics.version,
|
|
1815
|
+
qualityScore: metrics.score.detail.quality,
|
|
1721
1816
|
};
|
|
1722
|
-
|
|
1723
|
-
? CACHE_TTL_SHORT
|
|
1724
|
-
: CACHE_TTL_LONG;
|
|
1725
|
-
cacheSet(cacheKey, qualityData, ttl);
|
|
1817
|
+
cacheSet(cacheKey, qualityData, CACHE_TTL_LONG);
|
|
1726
1818
|
return {
|
|
1727
1819
|
packageInput: pkgInput,
|
|
1728
1820
|
packageName: name,
|
|
1729
1821
|
status: 'success',
|
|
1730
1822
|
error: null,
|
|
1731
1823
|
data: qualityData,
|
|
1732
|
-
message: `Successfully fetched quality score for ${name} (version analyzed: ${
|
|
1824
|
+
message: `Successfully fetched quality score for ${name} (version analyzed: ${metrics.version}).`,
|
|
1733
1825
|
};
|
|
1734
1826
|
}
|
|
1735
1827
|
catch (error) {
|
|
@@ -1762,6 +1854,7 @@ export async function handleNpmQuality(args) {
|
|
|
1762
1854
|
};
|
|
1763
1855
|
}
|
|
1764
1856
|
}
|
|
1857
|
+
// Function to get package maintenance metrics
|
|
1765
1858
|
export async function handleNpmMaintenance(args) {
|
|
1766
1859
|
try {
|
|
1767
1860
|
const packagesToProcess = args.packages || [];
|
|
@@ -1817,55 +1910,20 @@ export async function handleNpmMaintenance(args) {
|
|
|
1817
1910
|
};
|
|
1818
1911
|
}
|
|
1819
1912
|
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;
|
|
1913
|
+
const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
|
|
1853
1914
|
const maintenanceData = {
|
|
1854
|
-
analyzedAt: analyzedAt,
|
|
1855
|
-
versionInScore:
|
|
1856
|
-
maintenanceScore:
|
|
1915
|
+
analyzedAt: metrics.analyzedAt,
|
|
1916
|
+
versionInScore: metrics.version,
|
|
1917
|
+
maintenanceScore: metrics.score.detail.maintenance,
|
|
1857
1918
|
};
|
|
1858
|
-
|
|
1859
|
-
? CACHE_TTL_SHORT
|
|
1860
|
-
: CACHE_TTL_LONG;
|
|
1861
|
-
cacheSet(cacheKey, maintenanceData, ttl);
|
|
1919
|
+
cacheSet(cacheKey, maintenanceData, CACHE_TTL_LONG);
|
|
1862
1920
|
return {
|
|
1863
1921
|
packageInput: pkgInput,
|
|
1864
1922
|
packageName: name,
|
|
1865
1923
|
status: 'success',
|
|
1866
1924
|
error: null,
|
|
1867
1925
|
data: maintenanceData,
|
|
1868
|
-
message: `Successfully fetched maintenance score for ${name} (version analyzed: ${
|
|
1926
|
+
message: `Successfully fetched maintenance score for ${name} (version analyzed: ${metrics.version}).`,
|
|
1869
1927
|
};
|
|
1870
1928
|
}
|
|
1871
1929
|
catch (error) {
|
|
@@ -2025,6 +2083,7 @@ export async function handleNpmMaintainers(args) {
|
|
|
2025
2083
|
};
|
|
2026
2084
|
}
|
|
2027
2085
|
}
|
|
2086
|
+
// Function to get package score
|
|
2028
2087
|
export async function handleNpmScore(args) {
|
|
2029
2088
|
try {
|
|
2030
2089
|
const packagesToProcess = args.packages || [];
|
|
@@ -2078,86 +2137,41 @@ export async function handleNpmScore(args) {
|
|
|
2078
2137
|
};
|
|
2079
2138
|
}
|
|
2080
2139
|
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;
|
|
2140
|
+
const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
|
|
2118
2141
|
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
|
-
},
|
|
2142
|
+
analyzedAt: metrics.analyzedAt,
|
|
2143
|
+
versionInScore: metrics.version,
|
|
2144
|
+
score: metrics.score,
|
|
2129
2145
|
packageInfoFromScore: {
|
|
2130
|
-
name:
|
|
2131
|
-
version:
|
|
2132
|
-
description:
|
|
2146
|
+
name: metrics.packageName,
|
|
2147
|
+
version: metrics.version,
|
|
2148
|
+
description: metrics.description,
|
|
2133
2149
|
},
|
|
2134
2150
|
npmStats: {
|
|
2135
|
-
downloadsLastMonth:
|
|
2136
|
-
starsCount:
|
|
2151
|
+
downloadsLastMonth: metrics.downloadsLastMonth,
|
|
2152
|
+
starsCount: metrics.github.hasGitHubData ? metrics.github.starsCount : 0, // Fallback best effort
|
|
2137
2153
|
},
|
|
2138
|
-
githubStats:
|
|
2154
|
+
githubStats: metrics.github.hasGitHubData
|
|
2139
2155
|
? {
|
|
2140
|
-
starsCount:
|
|
2141
|
-
forksCount:
|
|
2142
|
-
subscribersCount:
|
|
2156
|
+
starsCount: metrics.github.starsCount,
|
|
2157
|
+
forksCount: metrics.github.forksCount,
|
|
2158
|
+
subscribersCount: metrics.github.subscribersCount,
|
|
2143
2159
|
issues: {
|
|
2144
|
-
count:
|
|
2145
|
-
openCount:
|
|
2160
|
+
count: metrics.github.openIssuesCount, // Heuristic default count = open issues
|
|
2161
|
+
openCount: metrics.github.openIssuesCount,
|
|
2146
2162
|
},
|
|
2147
2163
|
}
|
|
2148
2164
|
: null,
|
|
2165
|
+
scorecard: metrics.scorecard,
|
|
2149
2166
|
};
|
|
2150
|
-
|
|
2151
|
-
? CACHE_TTL_SHORT
|
|
2152
|
-
: CACHE_TTL_LONG;
|
|
2153
|
-
cacheSet(cacheKey, scoreData, ttl);
|
|
2167
|
+
cacheSet(cacheKey, scoreData, CACHE_TTL_LONG);
|
|
2154
2168
|
return {
|
|
2155
2169
|
packageInput: pkgInput,
|
|
2156
2170
|
packageName: name,
|
|
2157
2171
|
status: 'success',
|
|
2158
2172
|
error: null,
|
|
2159
2173
|
data: scoreData,
|
|
2160
|
-
message: `Successfully fetched score data for ${name} (version analyzed: ${
|
|
2174
|
+
message: `Successfully fetched score data for ${name} (version analyzed: ${metrics.version}).`,
|
|
2161
2175
|
};
|
|
2162
2176
|
}
|
|
2163
2177
|
catch (error) {
|
|
@@ -2190,6 +2204,27 @@ export async function handleNpmScore(args) {
|
|
|
2190
2204
|
};
|
|
2191
2205
|
}
|
|
2192
2206
|
}
|
|
2207
|
+
export async function fetchReadmeFromCDN(packageName, version) {
|
|
2208
|
+
const cdnUrls = [
|
|
2209
|
+
`https://cdn.jsdelivr.net/npm/${packageName}@${version}/README.md`,
|
|
2210
|
+
`https://unpkg.com/${packageName}@${version}/README.md`,
|
|
2211
|
+
];
|
|
2212
|
+
for (const cdnUrl of cdnUrls) {
|
|
2213
|
+
try {
|
|
2214
|
+
const response = await fetchWithRetry(cdnUrl, { headers: { Accept: 'text/plain' } }, { maxRetries: 1, skipThrottle: true });
|
|
2215
|
+
if (response.ok) {
|
|
2216
|
+
const text = await response.text();
|
|
2217
|
+
if (text && text.length > 10) {
|
|
2218
|
+
return text;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
catch {
|
|
2223
|
+
// Sigue intentando con la próxima URL
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return null;
|
|
2227
|
+
}
|
|
2193
2228
|
export async function handleNpmPackageReadme(args) {
|
|
2194
2229
|
try {
|
|
2195
2230
|
const packagesToProcess = args.packages || [];
|
|
@@ -2259,7 +2294,7 @@ export async function handleNpmPackageReadme(args) {
|
|
|
2259
2294
|
};
|
|
2260
2295
|
}
|
|
2261
2296
|
try {
|
|
2262
|
-
const response = await
|
|
2297
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
2263
2298
|
if (!response.ok) {
|
|
2264
2299
|
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
2265
2300
|
if (response.status === 404) {
|
|
@@ -2288,7 +2323,7 @@ export async function handleNpmPackageReadme(args) {
|
|
|
2288
2323
|
};
|
|
2289
2324
|
}
|
|
2290
2325
|
const versionToUse = versionTag === 'latest' ? packageInfo['dist-tags']?.latest : versionTag;
|
|
2291
|
-
if (!versionToUse || !packageInfo.versions
|
|
2326
|
+
if (!versionToUse || !packageInfo.versions?.[versionToUse]) {
|
|
2292
2327
|
return {
|
|
2293
2328
|
packageInput: pkgInput,
|
|
2294
2329
|
packageName: name,
|
|
@@ -2301,11 +2336,19 @@ export async function handleNpmPackageReadme(args) {
|
|
|
2301
2336
|
}
|
|
2302
2337
|
const versionData = packageInfo.versions[versionToUse];
|
|
2303
2338
|
// README can be in version-specific data or at the root of packageInfo
|
|
2304
|
-
|
|
2339
|
+
let readmeContent = versionData.readme || packageInfo.readme || null;
|
|
2340
|
+
let readmeSource = readmeContent ? 'registry' : null;
|
|
2341
|
+
if (!readmeContent && versionToUse) {
|
|
2342
|
+
readmeContent = await fetchReadmeFromCDN(name, versionToUse);
|
|
2343
|
+
if (readmeContent) {
|
|
2344
|
+
readmeSource = 'cdn';
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2305
2347
|
const hasReadme = !!readmeContent;
|
|
2306
2348
|
const readmeResultData = {
|
|
2307
2349
|
readme: readmeContent,
|
|
2308
2350
|
hasReadme: hasReadme,
|
|
2351
|
+
readmeSource: readmeSource,
|
|
2309
2352
|
versionFetched: versionToUse, // Store the actually fetched version
|
|
2310
2353
|
};
|
|
2311
2354
|
cacheSet(cacheKey, readmeResultData, CACHE_TTL_LONG);
|
|
@@ -2316,7 +2359,7 @@ export async function handleNpmPackageReadme(args) {
|
|
|
2316
2359
|
versionFetched: versionToUse,
|
|
2317
2360
|
status: 'success',
|
|
2318
2361
|
error: null,
|
|
2319
|
-
data: { readme: readmeContent, hasReadme: hasReadme }, // Return only readme and hasReadme in data field for consistency
|
|
2362
|
+
data: { readme: readmeContent, hasReadme: hasReadme, readmeSource: readmeSource }, // Return only readme and hasReadme in data field for consistency
|
|
2320
2363
|
message: `Successfully fetched README for ${name}@${versionToUse}.`,
|
|
2321
2364
|
};
|
|
2322
2365
|
}
|
|
@@ -2370,7 +2413,7 @@ export async function handleNpmSearch(args) {
|
|
|
2370
2413
|
message: `Search results for query '${query}' with limit ${limit} from cache.`,
|
|
2371
2414
|
};
|
|
2372
2415
|
}
|
|
2373
|
-
const response = await
|
|
2416
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
|
|
2374
2417
|
if (!response.ok) {
|
|
2375
2418
|
throw new Error(`Failed to search packages: ${response.status} ${response.statusText}`);
|
|
2376
2419
|
}
|
|
@@ -2508,7 +2551,7 @@ export async function handleNpmLicenseCompatibility(args) {
|
|
|
2508
2551
|
};
|
|
2509
2552
|
}
|
|
2510
2553
|
try {
|
|
2511
|
-
const response = await
|
|
2554
|
+
const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
|
|
2512
2555
|
if (!response.ok) {
|
|
2513
2556
|
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
2514
2557
|
if (response.status === 404) {
|
|
@@ -2683,7 +2726,7 @@ export async function handleNpmRepoStats(args) {
|
|
|
2683
2726
|
};
|
|
2684
2727
|
}
|
|
2685
2728
|
try {
|
|
2686
|
-
const npmResponse = await
|
|
2729
|
+
const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/latest`);
|
|
2687
2730
|
if (!npmResponse.ok) {
|
|
2688
2731
|
const errorData = {
|
|
2689
2732
|
packageInput: pkgInput,
|
|
@@ -2735,44 +2778,38 @@ export async function handleNpmRepoStats(args) {
|
|
|
2735
2778
|
return resultNotGitHub;
|
|
2736
2779
|
}
|
|
2737
2780
|
const [, owner, repo] = githubMatch;
|
|
2738
|
-
const
|
|
2739
|
-
const
|
|
2740
|
-
|
|
2741
|
-
Accept: 'application/vnd.github.v3+json',
|
|
2742
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
2743
|
-
},
|
|
2744
|
-
});
|
|
2745
|
-
if (!githubResponse.ok) {
|
|
2781
|
+
const cleanRepo = repo.replace(/\.git$/, '');
|
|
2782
|
+
const depsDevData = await fetchRepoStatsFromDepsDev(owner, cleanRepo, args.ignoreCache);
|
|
2783
|
+
if (!depsDevData) {
|
|
2746
2784
|
const errorData = {
|
|
2747
2785
|
packageInput: pkgInput,
|
|
2748
2786
|
packageName: name,
|
|
2749
2787
|
status: 'error',
|
|
2750
|
-
error: `Failed to fetch
|
|
2751
|
-
data: {
|
|
2752
|
-
message: `Could not retrieve
|
|
2788
|
+
error: `Failed to fetch project stats for github.com/${owner}/${cleanRepo} from deps.dev`,
|
|
2789
|
+
data: { repositoryUrl: repoUrl },
|
|
2790
|
+
message: `Could not retrieve repository statistics from deps.dev.`,
|
|
2753
2791
|
};
|
|
2754
|
-
// Do not cache GitHub API call failures for now
|
|
2755
2792
|
return errorData;
|
|
2756
2793
|
}
|
|
2757
|
-
const githubData = (await githubResponse.json());
|
|
2758
2794
|
const successResult = {
|
|
2759
2795
|
packageInput: pkgInput,
|
|
2760
2796
|
packageName: name,
|
|
2761
2797
|
status: 'success',
|
|
2762
2798
|
error: null,
|
|
2763
2799
|
data: {
|
|
2764
|
-
githubRepoUrl: `https://github.com/${owner}/${
|
|
2765
|
-
stars:
|
|
2766
|
-
forks:
|
|
2767
|
-
openIssues:
|
|
2768
|
-
watchers:
|
|
2769
|
-
createdAt:
|
|
2770
|
-
updatedAt:
|
|
2771
|
-
defaultBranch:
|
|
2772
|
-
hasWiki:
|
|
2773
|
-
topics:
|
|
2800
|
+
githubRepoUrl: `https://github.com/${owner}/${cleanRepo}`,
|
|
2801
|
+
stars: depsDevData.starsCount,
|
|
2802
|
+
forks: depsDevData.forksCount,
|
|
2803
|
+
openIssues: depsDevData.openIssuesCount,
|
|
2804
|
+
watchers: 0,
|
|
2805
|
+
createdAt: null,
|
|
2806
|
+
updatedAt: null,
|
|
2807
|
+
defaultBranch: null,
|
|
2808
|
+
hasWiki: null,
|
|
2809
|
+
topics: [],
|
|
2810
|
+
scorecard: depsDevData.scorecard,
|
|
2774
2811
|
},
|
|
2775
|
-
message: '
|
|
2812
|
+
message: 'Repository statistics fetched successfully from deps.dev.',
|
|
2776
2813
|
};
|
|
2777
2814
|
cacheSet(cacheKey, successResult, CACHE_TTL_LONG);
|
|
2778
2815
|
return successResult;
|
|
@@ -2860,7 +2897,7 @@ export async function handleNpmDeprecated(args) {
|
|
|
2860
2897
|
}
|
|
2861
2898
|
// console.debug(`[handleNpmDeprecated] Cache miss for ${cacheKey}`);
|
|
2862
2899
|
try {
|
|
2863
|
-
const mainPkgResponse = await
|
|
2900
|
+
const mainPkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
2864
2901
|
if (!mainPkgResponse.ok) {
|
|
2865
2902
|
return {
|
|
2866
2903
|
package: initialPackageNameForOutput,
|
|
@@ -2902,7 +2939,7 @@ export async function handleNpmDeprecated(args) {
|
|
|
2902
2939
|
let statusMessage = '';
|
|
2903
2940
|
try {
|
|
2904
2941
|
// console.debug(`[handleNpmDeprecated] Checking dependency: ${depName}@${depSemVer}`);
|
|
2905
|
-
const depInfoResponse = await
|
|
2942
|
+
const depInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${encodeURIComponent(depName)}`);
|
|
2906
2943
|
if (!depInfoResponse.ok) {
|
|
2907
2944
|
statusMessage = `Could not fetch dependency info for '${depName}' (status: ${depInfoResponse.status}). Deprecation status unknown.`;
|
|
2908
2945
|
// console.warn(`[handleNpmDeprecated] ${statusMessage}`);
|
|
@@ -3079,7 +3116,7 @@ export async function handleNpmChangelogAnalysis(args) {
|
|
|
3079
3116
|
};
|
|
3080
3117
|
}
|
|
3081
3118
|
try {
|
|
3082
|
-
const npmResponse = await
|
|
3119
|
+
const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
|
|
3083
3120
|
if (!npmResponse.ok) {
|
|
3084
3121
|
const errorResult = {
|
|
3085
3122
|
packageInput: pkgInput,
|
|
@@ -3150,41 +3187,59 @@ export async function handleNpmChangelogAnalysis(args) {
|
|
|
3150
3187
|
let changelogContent = null;
|
|
3151
3188
|
let changelogSourceUrl = null;
|
|
3152
3189
|
let hasChangelogFile = false;
|
|
3153
|
-
for (const
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3190
|
+
for (const branch of ['main', 'master']) {
|
|
3191
|
+
for (const file of changelogFiles) {
|
|
3192
|
+
try {
|
|
3193
|
+
const rawChangelogUrl = `https://raw.githubusercontent.com/${owner}/${repoNameForUrl}/${branch}/${file}`;
|
|
3194
|
+
const response = await fetchWithRetry(rawChangelogUrl, {}, { maxRetries: 0 });
|
|
3195
|
+
if (response.ok) {
|
|
3196
|
+
changelogContent = await response.text();
|
|
3197
|
+
changelogSourceUrl = rawChangelogUrl;
|
|
3198
|
+
hasChangelogFile = true;
|
|
3199
|
+
break;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
catch {
|
|
3203
|
+
// Sigue intentando
|
|
3162
3204
|
}
|
|
3163
3205
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
}
|
|
3206
|
+
if (hasChangelogFile)
|
|
3207
|
+
break;
|
|
3167
3208
|
}
|
|
3168
3209
|
let githubReleases = [];
|
|
3169
3210
|
try {
|
|
3170
|
-
const githubApiResponse = await
|
|
3211
|
+
const githubApiResponse = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
|
|
3171
3212
|
headers: {
|
|
3172
3213
|
Accept: 'application/vnd.github.v3+json',
|
|
3173
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
3174
3214
|
},
|
|
3175
|
-
});
|
|
3215
|
+
}, { maxRetries: 0 });
|
|
3176
3216
|
if (githubApiResponse.ok) {
|
|
3177
3217
|
const releasesData = (await githubApiResponse.json());
|
|
3178
3218
|
githubReleases = releasesData.map((r) => ({
|
|
3179
3219
|
tag_name: r.tag_name || null,
|
|
3180
3220
|
name: r.name || null,
|
|
3181
3221
|
published_at: r.published_at || null,
|
|
3222
|
+
source: 'github-api',
|
|
3182
3223
|
}));
|
|
3183
3224
|
}
|
|
3184
3225
|
}
|
|
3185
3226
|
catch (error) {
|
|
3186
3227
|
console.debug(`Error fetching GitHub releases for ${name}: ${error}`);
|
|
3187
3228
|
}
|
|
3229
|
+
// Fallback to NPM Registry time/publish history if GitHub releases couldn't be fetched
|
|
3230
|
+
const publishTime = npmData.time;
|
|
3231
|
+
if (githubReleases.length === 0 && publishTime) {
|
|
3232
|
+
const timeKeys = Object.keys(publishTime).filter((k) => k !== 'modified' && k !== 'created');
|
|
3233
|
+
const sortedVersions = timeKeys.sort((a, b) => {
|
|
3234
|
+
return new Date(publishTime[b]).getTime() - new Date(publishTime[a]).getTime();
|
|
3235
|
+
});
|
|
3236
|
+
githubReleases = sortedVersions.slice(0, 5).map((v) => ({
|
|
3237
|
+
tag_name: `v${v}`,
|
|
3238
|
+
name: `Release ${v} (NPM Publish)`,
|
|
3239
|
+
published_at: publishTime[v],
|
|
3240
|
+
source: 'npm-registry',
|
|
3241
|
+
}));
|
|
3242
|
+
}
|
|
3188
3243
|
const versions = Object.keys(npmData.versions || {});
|
|
3189
3244
|
const npmVersionHistory = {
|
|
3190
3245
|
totalVersions: versions.length,
|
|
@@ -3198,7 +3253,7 @@ export async function handleNpmChangelogAnalysis(args) {
|
|
|
3198
3253
|
? `No changelog file or GitHub releases found for ${name}.`
|
|
3199
3254
|
: `Changelog analysis for ${name}.`;
|
|
3200
3255
|
const resultToCache = {
|
|
3201
|
-
packageInput: pkgInput,
|
|
3256
|
+
packageInput: pkgInput,
|
|
3202
3257
|
packageName: name,
|
|
3203
3258
|
versionQueried: versionQueried,
|
|
3204
3259
|
status: status,
|
|
@@ -3250,20 +3305,45 @@ export async function handleNpmChangelogAnalysis(args) {
|
|
|
3250
3305
|
};
|
|
3251
3306
|
}
|
|
3252
3307
|
}
|
|
3308
|
+
function isAlternativeCandidate(candidateName, candidateDesc, originalName) {
|
|
3309
|
+
const lowerCandidate = candidateName.toLowerCase();
|
|
3310
|
+
const lowerOriginal = originalName.toLowerCase();
|
|
3311
|
+
const lowerDesc = (candidateDesc || '').toLowerCase();
|
|
3312
|
+
if (lowerCandidate === lowerOriginal)
|
|
3313
|
+
return false;
|
|
3314
|
+
if (lowerCandidate.startsWith('@types/'))
|
|
3315
|
+
return false;
|
|
3316
|
+
if (lowerCandidate.startsWith(`${lowerOriginal}-`) ||
|
|
3317
|
+
lowerCandidate.startsWith(`${lowerOriginal}_`) ||
|
|
3318
|
+
lowerCandidate.startsWith(`${lowerOriginal}.`) ||
|
|
3319
|
+
lowerCandidate.endsWith(`-${lowerOriginal}`) ||
|
|
3320
|
+
lowerCandidate.endsWith(`_${lowerOriginal}`) ||
|
|
3321
|
+
lowerCandidate.includes(lowerOriginal)) {
|
|
3322
|
+
return false;
|
|
3323
|
+
}
|
|
3324
|
+
if (lowerDesc.includes(`middleware for ${lowerOriginal}`) ||
|
|
3325
|
+
lowerDesc.includes(`plugin for ${lowerOriginal}`) ||
|
|
3326
|
+
lowerDesc.includes(`adapter for ${lowerOriginal}`) ||
|
|
3327
|
+
lowerDesc.includes(`wrapper for ${lowerOriginal}`) ||
|
|
3328
|
+
lowerDesc.includes(`extension for ${lowerOriginal}`) ||
|
|
3329
|
+
lowerDesc.includes(`${lowerOriginal} middleware`) ||
|
|
3330
|
+
lowerDesc.includes(`${lowerOriginal} plugin`)) {
|
|
3331
|
+
return false;
|
|
3332
|
+
}
|
|
3333
|
+
return true;
|
|
3334
|
+
}
|
|
3253
3335
|
export async function handleNpmAlternatives(args) {
|
|
3254
3336
|
try {
|
|
3255
3337
|
const packagesToProcess = args.packages || [];
|
|
3256
3338
|
if (packagesToProcess.length === 0) {
|
|
3257
|
-
|
|
3339
|
+
return createEmptyArrayErrorResponse('npmAlternatives');
|
|
3258
3340
|
}
|
|
3259
3341
|
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
3260
3342
|
let originalPackageName = '';
|
|
3261
|
-
let versionQueried;
|
|
3262
3343
|
if (typeof pkgInput === 'string') {
|
|
3263
3344
|
const atIdx = pkgInput.lastIndexOf('@');
|
|
3264
3345
|
if (atIdx > 0) {
|
|
3265
3346
|
originalPackageName = pkgInput.slice(0, atIdx);
|
|
3266
|
-
versionQueried = pkgInput.slice(atIdx + 1);
|
|
3267
3347
|
}
|
|
3268
3348
|
else {
|
|
3269
3349
|
originalPackageName = pkgInput;
|
|
@@ -3300,35 +3380,34 @@ export async function handleNpmAlternatives(args) {
|
|
|
3300
3380
|
};
|
|
3301
3381
|
}
|
|
3302
3382
|
const cacheKey = generateCacheKey('handleNpmAlternatives', originalPackageName);
|
|
3303
|
-
const cachedResult = args.ignoreCache ? undefined : cacheGet(cacheKey);
|
|
3383
|
+
const cachedResult = args.ignoreCache ? undefined : cacheGet(cacheKey);
|
|
3304
3384
|
if (cachedResult) {
|
|
3305
3385
|
return {
|
|
3306
3386
|
...cachedResult,
|
|
3307
|
-
packageInput: pkgInput,
|
|
3308
|
-
packageName: originalPackageName,
|
|
3309
|
-
// versionQueried is part of cachedResult.data or similar if stored, or add if needed
|
|
3387
|
+
packageInput: pkgInput,
|
|
3388
|
+
packageName: originalPackageName,
|
|
3310
3389
|
status: `${cachedResult.status}_cache`,
|
|
3311
3390
|
message: `${cachedResult.message} (from cache)`,
|
|
3312
3391
|
};
|
|
3313
3392
|
}
|
|
3314
3393
|
try {
|
|
3315
|
-
|
|
3316
|
-
if (!searchResponse.ok) {
|
|
3317
|
-
const errorResult = {
|
|
3318
|
-
packageInput: pkgInput,
|
|
3319
|
-
packageName: originalPackageName,
|
|
3320
|
-
status: 'error',
|
|
3321
|
-
error: `Failed to search for alternatives: ${searchResponse.status} ${searchResponse.statusText}`,
|
|
3322
|
-
data: null,
|
|
3323
|
-
message: 'Could not perform search for alternatives.',
|
|
3324
|
-
};
|
|
3325
|
-
return errorResult; // Do not cache API errors for search
|
|
3326
|
-
}
|
|
3327
|
-
const searchData = (await searchResponse.json());
|
|
3328
|
-
const alternativePackagesRaw = searchData.objects || [];
|
|
3394
|
+
let originalPackageKeywords = [];
|
|
3329
3395
|
let originalPackageDownloads = 0;
|
|
3330
3396
|
try {
|
|
3331
|
-
const
|
|
3397
|
+
const initSearchResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=5`);
|
|
3398
|
+
if (initSearchResponse.ok) {
|
|
3399
|
+
const initSearchData = (await initSearchResponse.json());
|
|
3400
|
+
const match = (initSearchData.objects || []).find((p) => p.package.name === originalPackageName);
|
|
3401
|
+
if (match?.package?.keywords) {
|
|
3402
|
+
originalPackageKeywords = match.package.keywords;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
catch {
|
|
3407
|
+
// Optional keyword retrieval failure
|
|
3408
|
+
}
|
|
3409
|
+
try {
|
|
3410
|
+
const dlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
|
|
3332
3411
|
if (dlResponse.ok) {
|
|
3333
3412
|
originalPackageDownloads =
|
|
3334
3413
|
(await dlResponse.json()).downloads || 0;
|
|
@@ -3337,34 +3416,80 @@ export async function handleNpmAlternatives(args) {
|
|
|
3337
3416
|
catch (e) {
|
|
3338
3417
|
console.debug(`Failed to fetch downloads for original package ${originalPackageName}: ${e}`);
|
|
3339
3418
|
}
|
|
3340
|
-
const originalPackageKeywords = alternativePackagesRaw.find((p) => p.package.name === originalPackageName)?.package
|
|
3341
|
-
.keywords || [];
|
|
3342
3419
|
const originalPackageStats = {
|
|
3343
3420
|
name: originalPackageName,
|
|
3344
3421
|
monthlyDownloads: originalPackageDownloads,
|
|
3345
3422
|
keywords: originalPackageKeywords,
|
|
3346
3423
|
};
|
|
3347
|
-
|
|
3348
|
-
(
|
|
3349
|
-
|
|
3424
|
+
const genericKeywords = originalPackageKeywords.filter((kw) => kw.toLowerCase() !== originalPackageName.toLowerCase() &&
|
|
3425
|
+
!kw.toLowerCase().includes(originalPackageName.toLowerCase()) &&
|
|
3426
|
+
kw.length > 2);
|
|
3427
|
+
const domainKeywords = genericKeywords.filter((kw) => ![
|
|
3428
|
+
'mobile',
|
|
3429
|
+
'ionic',
|
|
3430
|
+
'component',
|
|
3431
|
+
'components',
|
|
3432
|
+
'ui',
|
|
3433
|
+
'css',
|
|
3434
|
+
'react',
|
|
3435
|
+
'vue',
|
|
3436
|
+
'angular',
|
|
3437
|
+
'stencil',
|
|
3438
|
+
'storybook',
|
|
3439
|
+
'icon',
|
|
3440
|
+
'icons',
|
|
3441
|
+
].includes(kw.toLowerCase()));
|
|
3442
|
+
const selectedKeywords = domainKeywords.length > 0 ? domainKeywords : genericKeywords;
|
|
3443
|
+
let searchQuery = originalPackageName;
|
|
3444
|
+
if (selectedKeywords.length >= 2) {
|
|
3445
|
+
searchQuery = selectedKeywords.slice(0, 3).join(' ');
|
|
3446
|
+
}
|
|
3447
|
+
else if (selectedKeywords.length === 1) {
|
|
3448
|
+
searchQuery = selectedKeywords[0];
|
|
3449
|
+
}
|
|
3450
|
+
const searchResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(searchQuery)}&size=30`);
|
|
3451
|
+
if (!searchResponse.ok) {
|
|
3452
|
+
const errorResult = {
|
|
3453
|
+
packageInput: pkgInput,
|
|
3454
|
+
packageName: originalPackageName,
|
|
3455
|
+
status: 'error',
|
|
3456
|
+
error: `Failed to search for alternatives: ${searchResponse.status} ${searchResponse.statusText}`,
|
|
3457
|
+
data: null,
|
|
3458
|
+
message: 'Could not perform search for alternatives.',
|
|
3459
|
+
};
|
|
3460
|
+
return errorResult;
|
|
3461
|
+
}
|
|
3462
|
+
const searchData = (await searchResponse.json());
|
|
3463
|
+
const alternativePackagesRaw = searchData.objects || [];
|
|
3464
|
+
let validAlternativesRaw = alternativePackagesRaw.filter((alt) => isAlternativeCandidate(alt.package.name, alt.package.description || '', originalPackageName));
|
|
3465
|
+
if (validAlternativesRaw.length === 0 && searchQuery !== originalPackageName) {
|
|
3466
|
+
try {
|
|
3467
|
+
const fallbackResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(originalPackageName)}&size=30`);
|
|
3468
|
+
if (fallbackResponse.ok) {
|
|
3469
|
+
const fallbackData = (await fallbackResponse.json());
|
|
3470
|
+
validAlternativesRaw = (fallbackData.objects || []).filter((alt) => isAlternativeCandidate(alt.package.name, alt.package.description || '', originalPackageName));
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
catch {
|
|
3474
|
+
// Fallback failure
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
if (validAlternativesRaw.length === 0) {
|
|
3350
3478
|
const resultNoAlternatives = {
|
|
3351
3479
|
packageInput: pkgInput,
|
|
3352
3480
|
packageName: originalPackageName,
|
|
3353
3481
|
status: 'no_alternatives_found',
|
|
3354
3482
|
error: null,
|
|
3355
3483
|
data: { originalPackageStats, alternatives: [] },
|
|
3356
|
-
message: `No significant alternatives found for ${originalPackageName} based on
|
|
3484
|
+
message: `No significant alternatives found for ${originalPackageName} based on search.`,
|
|
3357
3485
|
};
|
|
3358
3486
|
cacheSet(cacheKey, resultNoAlternatives, CACHE_TTL_MEDIUM);
|
|
3359
3487
|
return resultNoAlternatives;
|
|
3360
3488
|
}
|
|
3361
|
-
const alternativesData = await Promise.all(
|
|
3362
|
-
.filter((alt) => alt.package.name !== originalPackageName)
|
|
3363
|
-
.slice(0, 5)
|
|
3364
|
-
.map(async (alt) => {
|
|
3489
|
+
const alternativesData = await Promise.all(validAlternativesRaw.slice(0, 5).map(async (alt) => {
|
|
3365
3490
|
let altDownloads = 0;
|
|
3366
3491
|
try {
|
|
3367
|
-
const altDlResponse = await
|
|
3492
|
+
const altDlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
|
|
3368
3493
|
if (altDlResponse.ok) {
|
|
3369
3494
|
altDownloads = (await altDlResponse.json()).downloads || 0;
|
|
3370
3495
|
}
|
|
@@ -3427,430 +3552,8 @@ export async function handleNpmAlternatives(args) {
|
|
|
3427
3552
|
};
|
|
3428
3553
|
}
|
|
3429
3554
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
export const configSchema = z.object({
|
|
3433
|
-
NPM_REGISTRY_URL: z.string().optional().describe('URL of the NPM registry to use'),
|
|
3434
|
-
});
|
|
3435
|
-
// Create server function for Smithery CLI
|
|
3436
|
-
export default function createServer({ config }) {
|
|
3437
|
-
// Apply config overrides
|
|
3438
|
-
if (config.NPM_REGISTRY_URL) {
|
|
3439
|
-
NPM_REGISTRY_URL = config.NPM_REGISTRY_URL.replace(/\/$/, '');
|
|
3440
|
-
}
|
|
3441
|
-
// Handle both ESM and CJS environments
|
|
3442
|
-
let __filename;
|
|
3443
|
-
let __dirname;
|
|
3444
|
-
try {
|
|
3445
|
-
__filename = fileURLToPath(import.meta.url);
|
|
3446
|
-
__dirname = path.dirname(__filename);
|
|
3447
|
-
}
|
|
3448
|
-
catch (error) {
|
|
3449
|
-
// Fallback for CJS environment (Smithery HTTP build)
|
|
3450
|
-
__filename = process.argv[1] || '.';
|
|
3451
|
-
__dirname = path.dirname(__filename);
|
|
3452
|
-
}
|
|
3453
|
-
// Determine package root
|
|
3454
|
-
let packageRoot = __dirname;
|
|
3455
|
-
if (fs.existsSync(path.join(__dirname, 'package.json'))) {
|
|
3456
|
-
packageRoot = __dirname;
|
|
3457
|
-
}
|
|
3458
|
-
else if (fs.existsSync(path.join(__dirname, '..', 'package.json'))) {
|
|
3459
|
-
packageRoot = path.join(__dirname, '..');
|
|
3460
|
-
}
|
|
3461
|
-
// Read version from package.json
|
|
3462
|
-
let serverVersion = '1.0.0';
|
|
3463
|
-
try {
|
|
3464
|
-
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
3465
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
3466
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
3467
|
-
serverVersion = packageJson.version;
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
catch (error) {
|
|
3471
|
-
console.error('Error reading package.json version:', error);
|
|
3472
|
-
}
|
|
3473
|
-
// Create server instance
|
|
3474
|
-
const server = new McpServer({
|
|
3475
|
-
name: 'npm-sentinel-mcp',
|
|
3476
|
-
version: serverVersion,
|
|
3477
|
-
});
|
|
3478
|
-
// Update paths to be relative to the package
|
|
3479
|
-
const README_PATH = path.join(packageRoot, 'README.md');
|
|
3480
|
-
const LLMS_FULL_TEXT_PATH = path.join(packageRoot, 'llms-full.txt');
|
|
3481
|
-
// Register README.md resource
|
|
3482
|
-
server.registerResource('serverReadme', 'doc://server/readme', {
|
|
3483
|
-
description: 'Main documentation and usage guide for this NPM Info Server.',
|
|
3484
|
-
mimeType: 'text/markdown',
|
|
3485
|
-
}, async (uri) => {
|
|
3486
|
-
try {
|
|
3487
|
-
const readmeContent = fs.readFileSync(README_PATH, 'utf-8');
|
|
3488
|
-
return {
|
|
3489
|
-
contents: [
|
|
3490
|
-
{
|
|
3491
|
-
uri: uri.href,
|
|
3492
|
-
text: readmeContent,
|
|
3493
|
-
mimeType: 'text/markdown',
|
|
3494
|
-
},
|
|
3495
|
-
],
|
|
3496
|
-
};
|
|
3497
|
-
}
|
|
3498
|
-
catch (error) {
|
|
3499
|
-
console.error(`Error reading README.md for resource ${uri.href}:`, error.message);
|
|
3500
|
-
throw {
|
|
3501
|
-
code: -32002,
|
|
3502
|
-
message: `Resource not found or unreadable: ${uri.href}`,
|
|
3503
|
-
data: { uri: uri.href, cause: error.message },
|
|
3504
|
-
};
|
|
3505
|
-
}
|
|
3506
|
-
});
|
|
3507
|
-
// Register llms-full.txt resource (MCP Specification)
|
|
3508
|
-
server.registerResource('mcpSpecification', 'doc://mcp/specification', {
|
|
3509
|
-
description: 'The llms-full.txt content providing a comprehensive overview of the Model Context Protocol.',
|
|
3510
|
-
mimeType: 'text/plain',
|
|
3511
|
-
}, async (uri) => {
|
|
3512
|
-
try {
|
|
3513
|
-
const specContent = fs.readFileSync(LLMS_FULL_TEXT_PATH, 'utf-8');
|
|
3514
|
-
return {
|
|
3515
|
-
contents: [
|
|
3516
|
-
{
|
|
3517
|
-
uri: uri.href,
|
|
3518
|
-
text: specContent,
|
|
3519
|
-
mimeType: 'text/plain',
|
|
3520
|
-
},
|
|
3521
|
-
],
|
|
3522
|
-
};
|
|
3523
|
-
}
|
|
3524
|
-
catch (error) {
|
|
3525
|
-
console.error(`Error reading llms-full.txt for resource ${uri.href}:`, error.message);
|
|
3526
|
-
throw {
|
|
3527
|
-
code: -32002,
|
|
3528
|
-
message: `Resource not found or unreadable: ${uri.href}`,
|
|
3529
|
-
data: { uri: uri.href, cause: error.message },
|
|
3530
|
-
};
|
|
3531
|
-
}
|
|
3532
|
-
});
|
|
3533
|
-
// Register prompts
|
|
3534
|
-
server.registerPrompt('analyze-package', {
|
|
3535
|
-
description: 'Analyze an NPM package for security and quality',
|
|
3536
|
-
argsSchema: {
|
|
3537
|
-
package: z.string().describe('Name of the npm package to analyze'),
|
|
3538
|
-
},
|
|
3539
|
-
}, ({ package: pkgName }) => ({
|
|
3540
|
-
messages: [
|
|
3541
|
-
{
|
|
3542
|
-
role: 'user',
|
|
3543
|
-
content: {
|
|
3544
|
-
type: 'text',
|
|
3545
|
-
text: `Please analyze the npm package "${pkgName}". Check for vulnerabilities, maintenance status, and recent issues. Use the available tools to gather information.`,
|
|
3546
|
-
},
|
|
3547
|
-
},
|
|
3548
|
-
],
|
|
3549
|
-
}));
|
|
3550
|
-
// Add NPM tools - Ensuring each tool registration is complete and correct
|
|
3551
|
-
server.registerTool('npmVersions', {
|
|
3552
|
-
description: 'Get all available versions of an NPM package',
|
|
3553
|
-
inputSchema: {
|
|
3554
|
-
packages: z.array(z.string()).describe('List of package names to get versions for'),
|
|
3555
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3556
|
-
},
|
|
3557
|
-
annotations: {
|
|
3558
|
-
title: 'Get All Package Versions',
|
|
3559
|
-
readOnlyHint: true,
|
|
3560
|
-
openWorldHint: true,
|
|
3561
|
-
idempotentHint: true,
|
|
3562
|
-
},
|
|
3563
|
-
}, async (args) => {
|
|
3564
|
-
return await handleNpmVersions(args);
|
|
3565
|
-
});
|
|
3566
|
-
server.registerTool('npmLatest', {
|
|
3567
|
-
description: 'Get the latest version and changelog of an NPM package',
|
|
3568
|
-
inputSchema: {
|
|
3569
|
-
packages: z.array(z.string()).describe('List of package names to get latest versions for'),
|
|
3570
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3571
|
-
},
|
|
3572
|
-
annotations: {
|
|
3573
|
-
title: 'Get Latest Package Information',
|
|
3574
|
-
readOnlyHint: true,
|
|
3575
|
-
openWorldHint: true,
|
|
3576
|
-
idempotentHint: true, // Result for 'latest' tag can change, but call itself is idempotent
|
|
3577
|
-
},
|
|
3578
|
-
}, async (args) => {
|
|
3579
|
-
return await handleNpmLatest(args);
|
|
3580
|
-
});
|
|
3581
|
-
server.registerTool('npmDeps', {
|
|
3582
|
-
description: 'Analyze dependencies and devDependencies of an NPM package',
|
|
3583
|
-
inputSchema: {
|
|
3584
|
-
packages: z.array(z.string()).describe('List of package names to analyze dependencies for'),
|
|
3585
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3586
|
-
},
|
|
3587
|
-
annotations: {
|
|
3588
|
-
title: 'Get Package Dependencies',
|
|
3589
|
-
readOnlyHint: true,
|
|
3590
|
-
openWorldHint: true,
|
|
3591
|
-
idempotentHint: true,
|
|
3592
|
-
},
|
|
3593
|
-
}, async (args) => {
|
|
3594
|
-
return await handleNpmDeps(args);
|
|
3595
|
-
});
|
|
3596
|
-
server.registerTool('npmTypes', {
|
|
3597
|
-
description: 'Check TypeScript types availability and version for a package',
|
|
3598
|
-
inputSchema: {
|
|
3599
|
-
packages: z.array(z.string()).describe('List of package names to check types for'),
|
|
3600
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3601
|
-
},
|
|
3602
|
-
annotations: {
|
|
3603
|
-
title: 'Check TypeScript Type Availability',
|
|
3604
|
-
readOnlyHint: true,
|
|
3605
|
-
openWorldHint: true,
|
|
3606
|
-
idempotentHint: true,
|
|
3607
|
-
},
|
|
3608
|
-
}, async (args) => {
|
|
3609
|
-
return await handleNpmTypes(args);
|
|
3610
|
-
});
|
|
3611
|
-
server.registerTool('npmSize', {
|
|
3612
|
-
description: 'Get package size information including dependencies and bundle size',
|
|
3613
|
-
inputSchema: {
|
|
3614
|
-
packages: z.array(z.string()).describe('List of package names to get size information for'),
|
|
3615
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3616
|
-
},
|
|
3617
|
-
annotations: {
|
|
3618
|
-
title: 'Get Package Size (Bundlephobia)',
|
|
3619
|
-
readOnlyHint: true,
|
|
3620
|
-
openWorldHint: true,
|
|
3621
|
-
idempotentHint: true,
|
|
3622
|
-
},
|
|
3623
|
-
}, async (args) => {
|
|
3624
|
-
return await handleNpmSize(args);
|
|
3625
|
-
});
|
|
3626
|
-
server.registerTool('npmVulnerabilities', {
|
|
3627
|
-
description: 'Check for known vulnerabilities in packages',
|
|
3628
|
-
inputSchema: {
|
|
3629
|
-
packages: z
|
|
3630
|
-
.array(z.string())
|
|
3631
|
-
.describe('List of package names to check for vulnerabilities'),
|
|
3632
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3633
|
-
},
|
|
3634
|
-
annotations: {
|
|
3635
|
-
title: 'Check Package Vulnerabilities (OSV.dev)',
|
|
3636
|
-
readOnlyHint: true,
|
|
3637
|
-
openWorldHint: true,
|
|
3638
|
-
idempotentHint: false, // Vulnerability data can change frequently
|
|
3639
|
-
},
|
|
3640
|
-
}, async (args) => {
|
|
3641
|
-
return await handleNpmVulnerabilities(args);
|
|
3642
|
-
});
|
|
3643
|
-
server.registerTool('npmTrends', {
|
|
3644
|
-
description: 'Get download trends and popularity metrics for packages',
|
|
3645
|
-
inputSchema: {
|
|
3646
|
-
packages: z.array(z.string()).describe('List of package names to get trends for'),
|
|
3647
|
-
period: z
|
|
3648
|
-
.enum(['last-week', 'last-month', 'last-year'])
|
|
3649
|
-
.describe('Time period for trends. Options: "last-week", "last-month", "last-year"')
|
|
3650
|
-
.optional()
|
|
3651
|
-
.default('last-month'),
|
|
3652
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3653
|
-
},
|
|
3654
|
-
annotations: {
|
|
3655
|
-
title: 'Get NPM Package Download Trends',
|
|
3656
|
-
readOnlyHint: true,
|
|
3657
|
-
openWorldHint: true,
|
|
3658
|
-
idempotentHint: true, // Trends for a fixed past period are idempotent
|
|
3659
|
-
},
|
|
3660
|
-
}, async (args) => {
|
|
3661
|
-
return await handleNpmTrends(args);
|
|
3662
|
-
});
|
|
3663
|
-
server.registerTool('npmCompare', {
|
|
3664
|
-
description: 'Compare multiple NPM packages based on various metrics',
|
|
3665
|
-
inputSchema: {
|
|
3666
|
-
packages: z.array(z.string()).describe('List of package names to compare'),
|
|
3667
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3668
|
-
},
|
|
3669
|
-
annotations: {
|
|
3670
|
-
title: 'Compare NPM Packages',
|
|
3671
|
-
readOnlyHint: true,
|
|
3672
|
-
openWorldHint: true,
|
|
3673
|
-
idempotentHint: true,
|
|
3674
|
-
},
|
|
3675
|
-
}, async (args) => {
|
|
3676
|
-
return await handleNpmCompare(args);
|
|
3677
|
-
});
|
|
3678
|
-
server.registerTool('npmMaintainers', {
|
|
3679
|
-
description: 'Get maintainers information for NPM packages',
|
|
3680
|
-
inputSchema: {
|
|
3681
|
-
packages: z.array(z.string()).describe('List of package names to get maintainers for'),
|
|
3682
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3683
|
-
},
|
|
3684
|
-
annotations: {
|
|
3685
|
-
title: 'Get NPM Package Maintainers',
|
|
3686
|
-
readOnlyHint: true,
|
|
3687
|
-
openWorldHint: true,
|
|
3688
|
-
idempotentHint: true,
|
|
3689
|
-
},
|
|
3690
|
-
}, async (args) => {
|
|
3691
|
-
return await handleNpmMaintainers(args);
|
|
3692
|
-
});
|
|
3693
|
-
server.registerTool('npmScore', {
|
|
3694
|
-
description: 'Get consolidated package score based on quality, maintenance, and popularity metrics',
|
|
3695
|
-
inputSchema: {
|
|
3696
|
-
packages: z.array(z.string()).describe('List of package names to get scores for'),
|
|
3697
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3698
|
-
},
|
|
3699
|
-
annotations: {
|
|
3700
|
-
title: 'Get NPM Package Score (NPMS.io)',
|
|
3701
|
-
readOnlyHint: true,
|
|
3702
|
-
openWorldHint: true,
|
|
3703
|
-
idempotentHint: true, // Score for a version is stable, for 'latest' can change
|
|
3704
|
-
},
|
|
3705
|
-
}, async (args) => {
|
|
3706
|
-
return await handleNpmScore(args);
|
|
3707
|
-
});
|
|
3708
|
-
server.registerTool('npmPackageReadme', {
|
|
3709
|
-
description: 'Get the README content for NPM packages',
|
|
3710
|
-
inputSchema: {
|
|
3711
|
-
packages: z.array(z.string()).describe('List of package names to get READMEs for'),
|
|
3712
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3713
|
-
},
|
|
3714
|
-
annotations: {
|
|
3715
|
-
title: 'Get NPM Package README',
|
|
3716
|
-
readOnlyHint: true,
|
|
3717
|
-
openWorldHint: true,
|
|
3718
|
-
idempotentHint: true,
|
|
3719
|
-
},
|
|
3720
|
-
}, async (args) => {
|
|
3721
|
-
return await handleNpmPackageReadme(args);
|
|
3722
|
-
});
|
|
3723
|
-
server.registerTool('npmSearch', {
|
|
3724
|
-
description: 'Search for NPM packages with optional limit',
|
|
3725
|
-
inputSchema: {
|
|
3726
|
-
query: z.string().describe('Search query for packages'),
|
|
3727
|
-
limit: z
|
|
3728
|
-
.number()
|
|
3729
|
-
.min(1)
|
|
3730
|
-
.max(50)
|
|
3731
|
-
.optional()
|
|
3732
|
-
.describe('Maximum number of results to return (default: 10)'),
|
|
3733
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3734
|
-
},
|
|
3735
|
-
annotations: {
|
|
3736
|
-
title: 'Search NPM Packages',
|
|
3737
|
-
readOnlyHint: true,
|
|
3738
|
-
openWorldHint: true,
|
|
3739
|
-
idempotentHint: false, // Search results can change
|
|
3740
|
-
},
|
|
3741
|
-
}, async (args) => {
|
|
3742
|
-
return await handleNpmSearch(args);
|
|
3743
|
-
});
|
|
3744
|
-
server.registerTool('npmLicenseCompatibility', {
|
|
3745
|
-
description: 'Check license compatibility between multiple packages',
|
|
3746
|
-
inputSchema: {
|
|
3747
|
-
packages: z
|
|
3748
|
-
.array(z.string())
|
|
3749
|
-
.min(1)
|
|
3750
|
-
.describe('List of package names to check for license compatibility'),
|
|
3751
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3752
|
-
},
|
|
3753
|
-
annotations: {
|
|
3754
|
-
title: 'Check NPM License Compatibility',
|
|
3755
|
-
readOnlyHint: true,
|
|
3756
|
-
openWorldHint: true,
|
|
3757
|
-
idempotentHint: true,
|
|
3758
|
-
},
|
|
3759
|
-
}, async (args) => {
|
|
3760
|
-
return await handleNpmLicenseCompatibility(args);
|
|
3761
|
-
});
|
|
3762
|
-
server.registerTool('npmRepoStats', {
|
|
3763
|
-
description: 'Get repository statistics for NPM packages',
|
|
3764
|
-
inputSchema: {
|
|
3765
|
-
packages: z.array(z.string()).describe('List of package names to get repository stats for'),
|
|
3766
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3767
|
-
},
|
|
3768
|
-
annotations: {
|
|
3769
|
-
title: 'Get NPM Package Repository Stats (GitHub)',
|
|
3770
|
-
readOnlyHint: true,
|
|
3771
|
-
openWorldHint: true,
|
|
3772
|
-
idempotentHint: true, // Stats for a repo at a point in time, though they change over time
|
|
3773
|
-
},
|
|
3774
|
-
}, async (args) => {
|
|
3775
|
-
return await handleNpmRepoStats(args);
|
|
3776
|
-
});
|
|
3777
|
-
server.registerTool('npmDeprecated', {
|
|
3778
|
-
description: 'Check if packages are deprecated',
|
|
3779
|
-
inputSchema: {
|
|
3780
|
-
packages: z.array(z.string()).describe('List of package names to check for deprecation'),
|
|
3781
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3782
|
-
},
|
|
3783
|
-
annotations: {
|
|
3784
|
-
title: 'Check NPM Package Deprecation Status',
|
|
3785
|
-
readOnlyHint: true,
|
|
3786
|
-
openWorldHint: true,
|
|
3787
|
-
idempotentHint: true, // Deprecation status is generally stable for a version
|
|
3788
|
-
},
|
|
3789
|
-
}, async (args) => {
|
|
3790
|
-
return await handleNpmDeprecated(args);
|
|
3791
|
-
});
|
|
3792
|
-
server.registerTool('npmChangelogAnalysis', {
|
|
3793
|
-
description: 'Analyze changelog and release history of packages',
|
|
3794
|
-
inputSchema: {
|
|
3795
|
-
packages: z.array(z.string()).describe('List of package names to analyze changelogs for'),
|
|
3796
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3797
|
-
},
|
|
3798
|
-
annotations: {
|
|
3799
|
-
title: 'Analyze NPM Package Changelog (GitHub)',
|
|
3800
|
-
readOnlyHint: true,
|
|
3801
|
-
openWorldHint: true,
|
|
3802
|
-
idempotentHint: true,
|
|
3803
|
-
},
|
|
3804
|
-
}, async (args) => {
|
|
3805
|
-
return await handleNpmChangelogAnalysis(args);
|
|
3806
|
-
});
|
|
3807
|
-
server.registerTool('npmAlternatives', {
|
|
3808
|
-
description: 'Find alternative packages with similar functionality',
|
|
3809
|
-
inputSchema: {
|
|
3810
|
-
packages: z.array(z.string()).describe('List of package names to find alternatives for'),
|
|
3811
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3812
|
-
},
|
|
3813
|
-
annotations: {
|
|
3814
|
-
title: 'Find NPM Package Alternatives',
|
|
3815
|
-
readOnlyHint: true,
|
|
3816
|
-
openWorldHint: true,
|
|
3817
|
-
idempotentHint: false, // Search-based, results can change
|
|
3818
|
-
},
|
|
3819
|
-
}, async (args) => {
|
|
3820
|
-
return await handleNpmAlternatives(args);
|
|
3821
|
-
});
|
|
3822
|
-
server.registerTool('npmQuality', {
|
|
3823
|
-
description: 'Analyze package quality metrics',
|
|
3824
|
-
inputSchema: {
|
|
3825
|
-
packages: z.array(z.string()).describe('List of package names to analyze'),
|
|
3826
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3827
|
-
},
|
|
3828
|
-
annotations: {
|
|
3829
|
-
title: 'Analyze NPM Package Quality (NPMS.io)',
|
|
3830
|
-
readOnlyHint: true,
|
|
3831
|
-
openWorldHint: true,
|
|
3832
|
-
idempotentHint: true, // Score for a version is stable, for 'latest' can change
|
|
3833
|
-
},
|
|
3834
|
-
}, async (args) => {
|
|
3835
|
-
return await handleNpmQuality(args);
|
|
3836
|
-
});
|
|
3837
|
-
server.registerTool('npmMaintenance', {
|
|
3838
|
-
description: 'Analyze package maintenance metrics',
|
|
3839
|
-
inputSchema: {
|
|
3840
|
-
packages: z.array(z.string()).describe('List of package names to analyze'),
|
|
3841
|
-
ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
|
|
3842
|
-
},
|
|
3843
|
-
annotations: {
|
|
3844
|
-
title: 'Analyze NPM Package Maintenance (NPMS.io)',
|
|
3845
|
-
readOnlyHint: true,
|
|
3846
|
-
openWorldHint: true,
|
|
3847
|
-
idempotentHint: true, // Score for a version is stable, for 'latest' can change
|
|
3848
|
-
},
|
|
3849
|
-
}, async (args) => {
|
|
3850
|
-
return await handleNpmMaintenance(args);
|
|
3851
|
-
});
|
|
3852
|
-
return server.server;
|
|
3853
|
-
}
|
|
3555
|
+
export { createServer };
|
|
3556
|
+
export default createServer;
|
|
3854
3557
|
// STDIO compatibility for backward compatibility
|
|
3855
3558
|
async function main() {
|
|
3856
3559
|
// Create server with empty configuration
|
|
@@ -3884,7 +3587,7 @@ function isNpmPackageVersionData(data) {
|
|
|
3884
3587
|
}
|
|
3885
3588
|
return result.success;
|
|
3886
3589
|
}
|
|
3887
|
-
catch
|
|
3590
|
+
catch {
|
|
3888
3591
|
// This catch block might not be strictly necessary with safeParse but kept for safety
|
|
3889
3592
|
// console.error("isNpmPackageVersionData validation failed unexpectedly:", e);
|
|
3890
3593
|
return false;
|