@nekzus/mcp-server 1.5.6 → 1.6.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/README.md +27 -9
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +393 -72
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2589 -1246
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -3,6 +3,41 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import fetch from 'node-fetch';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
// Cache configuration
|
|
7
|
+
const CACHE_TTL_SHORT = 15 * 60 * 1000; // 15 minutes
|
|
8
|
+
const CACHE_TTL_MEDIUM = 60 * 60 * 1000; // 1 hour
|
|
9
|
+
const CACHE_TTL_LONG = 6 * 60 * 60 * 1000; // 6 hours
|
|
10
|
+
const CACHE_TTL_VERY_LONG = 24 * 60 * 60 * 1000; // 24 hours
|
|
11
|
+
const MAX_CACHE_SIZE = 500; // Max number of items in cache
|
|
12
|
+
const apiCache = new Map();
|
|
13
|
+
function generateCacheKey(toolName, ...args) {
|
|
14
|
+
// Simple key generation, ensure consistent order and stringification of args
|
|
15
|
+
return `${toolName}:${args.map((arg) => String(arg)).join(':')}`;
|
|
16
|
+
}
|
|
17
|
+
function cacheGet(key) {
|
|
18
|
+
const entry = apiCache.get(key);
|
|
19
|
+
if (entry && entry.expiresAt > Date.now()) {
|
|
20
|
+
return entry.data;
|
|
21
|
+
}
|
|
22
|
+
if (entry && entry.expiresAt <= Date.now()) {
|
|
23
|
+
apiCache.delete(key); // Remove stale entry
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function cacheSet(key, value, ttlMilliseconds) {
|
|
28
|
+
if (ttlMilliseconds <= 0)
|
|
29
|
+
return; // Do not cache if TTL is zero or negative
|
|
30
|
+
const expiresAt = Date.now() + ttlMilliseconds;
|
|
31
|
+
apiCache.set(key, { data: value, expiresAt });
|
|
32
|
+
// Basic FIFO eviction strategy if cache exceeds max size
|
|
33
|
+
if (apiCache.size > MAX_CACHE_SIZE) {
|
|
34
|
+
// To make it FIFO, we need to ensure Map iteration order is insertion order (which it is)
|
|
35
|
+
const oldestKey = apiCache.keys().next().value;
|
|
36
|
+
if (oldestKey) {
|
|
37
|
+
apiCache.delete(oldestKey);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
6
41
|
// Zod schemas for npm package data
|
|
7
42
|
export const NpmMaintainerSchema = z
|
|
8
43
|
.object({
|
|
@@ -16,7 +51,18 @@ export const NpmPackageVersionSchema = z
|
|
|
16
51
|
name: z.string(),
|
|
17
52
|
version: z.string(),
|
|
18
53
|
description: z.string().optional(),
|
|
19
|
-
author: z
|
|
54
|
+
author: z
|
|
55
|
+
.union([
|
|
56
|
+
z.string(),
|
|
57
|
+
z
|
|
58
|
+
.object({
|
|
59
|
+
name: z.string().optional(),
|
|
60
|
+
email: z.string().optional(),
|
|
61
|
+
url: z.string().optional(),
|
|
62
|
+
})
|
|
63
|
+
.passthrough(),
|
|
64
|
+
])
|
|
65
|
+
.optional(),
|
|
20
66
|
license: z.string().optional(),
|
|
21
67
|
repository: z
|
|
22
68
|
.object({
|
|
@@ -32,6 +78,15 @@ export const NpmPackageVersionSchema = z
|
|
|
32
78
|
.passthrough()
|
|
33
79
|
.optional(),
|
|
34
80
|
homepage: z.string().optional(),
|
|
81
|
+
dependencies: z.record(z.string()).optional(),
|
|
82
|
+
devDependencies: z.record(z.string()).optional(),
|
|
83
|
+
peerDependencies: z.record(z.string()).optional(),
|
|
84
|
+
types: z.string().optional(),
|
|
85
|
+
typings: z.string().optional(),
|
|
86
|
+
dist: z
|
|
87
|
+
.object({ shasum: z.string().optional(), tarball: z.string().optional() })
|
|
88
|
+
.passthrough()
|
|
89
|
+
.optional(),
|
|
35
90
|
})
|
|
36
91
|
.passthrough();
|
|
37
92
|
export const NpmPackageInfoSchema = z
|
|
@@ -79,31 +134,9 @@ export const NpmDownloadsDataSchema = z.object({
|
|
|
79
134
|
end: z.string(),
|
|
80
135
|
package: z.string(),
|
|
81
136
|
});
|
|
82
|
-
// Schemas for NPM quality, maintenance and popularity metrics
|
|
83
|
-
export const NpmQualitySchema = z.object({
|
|
84
|
-
score: z.number(),
|
|
85
|
-
tests: z.number(),
|
|
86
|
-
coverage: z.number(),
|
|
87
|
-
linting: z.number(),
|
|
88
|
-
types: z.number(),
|
|
89
|
-
});
|
|
90
|
-
export const NpmMaintenanceSchema = z.object({
|
|
91
|
-
score: z.number(),
|
|
92
|
-
issuesResolutionTime: z.number(),
|
|
93
|
-
commitsFrequency: z.number(),
|
|
94
|
-
releaseFrequency: z.number(),
|
|
95
|
-
lastUpdate: z.string(),
|
|
96
|
-
});
|
|
97
|
-
export const NpmPopularitySchema = z.object({
|
|
98
|
-
score: z.number(),
|
|
99
|
-
stars: z.number(),
|
|
100
|
-
downloads: z.number(),
|
|
101
|
-
dependents: z.number(),
|
|
102
|
-
communityInterest: z.number(),
|
|
103
|
-
});
|
|
104
137
|
function isValidNpmsResponse(data) {
|
|
105
138
|
if (typeof data !== 'object' || data === null) {
|
|
106
|
-
console.debug('Response is not an object or is null');
|
|
139
|
+
console.debug('NpmsApiResponse validation: Response is not an object or is null');
|
|
107
140
|
return false;
|
|
108
141
|
}
|
|
109
142
|
const response = data;
|
|
@@ -114,7 +147,7 @@ function isValidNpmsResponse(data) {
|
|
|
114
147
|
typeof response.score.final !== 'number' ||
|
|
115
148
|
!('detail' in response.score) ||
|
|
116
149
|
typeof response.score.detail !== 'object') {
|
|
117
|
-
console.debug('Invalid score structure');
|
|
150
|
+
console.debug('NpmsApiResponse validation: Invalid score structure');
|
|
118
151
|
return false;
|
|
119
152
|
}
|
|
120
153
|
// Check score detail metrics
|
|
@@ -122,7 +155,7 @@ function isValidNpmsResponse(data) {
|
|
|
122
155
|
if (typeof detail.quality !== 'number' ||
|
|
123
156
|
typeof detail.popularity !== 'number' ||
|
|
124
157
|
typeof detail.maintenance !== 'number') {
|
|
125
|
-
console.debug('Invalid score detail metrics');
|
|
158
|
+
console.debug('NpmsApiResponse validation: Invalid score detail metrics');
|
|
126
159
|
return false;
|
|
127
160
|
}
|
|
128
161
|
// Check collected data structure
|
|
@@ -132,7 +165,7 @@ function isValidNpmsResponse(data) {
|
|
|
132
165
|
typeof response.collected.metadata !== 'object' ||
|
|
133
166
|
typeof response.collected.metadata.name !== 'string' ||
|
|
134
167
|
typeof response.collected.metadata.version !== 'string') {
|
|
135
|
-
console.debug('Invalid collected data structure');
|
|
168
|
+
console.debug('NpmsApiResponse validation: Invalid collected data structure');
|
|
136
169
|
return false;
|
|
137
170
|
}
|
|
138
171
|
// Check npm data
|
|
@@ -140,7 +173,7 @@ function isValidNpmsResponse(data) {
|
|
|
140
173
|
typeof response.collected.npm !== 'object' ||
|
|
141
174
|
!Array.isArray(response.collected.npm.downloads) ||
|
|
142
175
|
typeof response.collected.npm.starsCount !== 'number') {
|
|
143
|
-
console.debug('Invalid npm data structure');
|
|
176
|
+
console.debug('NpmsApiResponse validation: Invalid npm data structure');
|
|
144
177
|
return false;
|
|
145
178
|
}
|
|
146
179
|
// Optional github data check
|
|
@@ -153,7 +186,7 @@ function isValidNpmsResponse(data) {
|
|
|
153
186
|
typeof response.collected.github.issues !== 'object' ||
|
|
154
187
|
typeof response.collected.github.issues.count !== 'number' ||
|
|
155
188
|
typeof response.collected.github.issues.openCount !== 'number') {
|
|
156
|
-
console.debug('Invalid github data structure');
|
|
189
|
+
console.debug('NpmsApiResponse validation: Invalid github data structure');
|
|
157
190
|
return false;
|
|
158
191
|
}
|
|
159
192
|
}
|
|
@@ -170,6 +203,7 @@ export const NpmSearchResultSchema = z
|
|
|
170
203
|
publisher: z
|
|
171
204
|
.object({
|
|
172
205
|
username: z.string(),
|
|
206
|
+
email: z.string().optional(),
|
|
173
207
|
})
|
|
174
208
|
.optional(),
|
|
175
209
|
links: z
|
|
@@ -177,8 +211,10 @@ export const NpmSearchResultSchema = z
|
|
|
177
211
|
npm: z.string().optional(),
|
|
178
212
|
homepage: z.string().optional(),
|
|
179
213
|
repository: z.string().optional(),
|
|
214
|
+
bugs: z.string().optional(),
|
|
180
215
|
})
|
|
181
216
|
.optional(),
|
|
217
|
+
date: z.string().optional(),
|
|
182
218
|
}),
|
|
183
219
|
score: z.object({
|
|
184
220
|
final: z.number(),
|
|
@@ -190,7 +226,7 @@ export const NpmSearchResultSchema = z
|
|
|
190
226
|
}),
|
|
191
227
|
searchScore: z.number(),
|
|
192
228
|
})),
|
|
193
|
-
total: z.number(),
|
|
229
|
+
total: z.number(), // total is a sibling of objects
|
|
194
230
|
})
|
|
195
231
|
.passthrough();
|
|
196
232
|
// Logger function that uses stderr - only for critical errors
|
|
@@ -202,253 +238,6 @@ const log = (...args) => {
|
|
|
202
238
|
console.error(...args);
|
|
203
239
|
}
|
|
204
240
|
};
|
|
205
|
-
// Define tools
|
|
206
|
-
const TOOLS = [
|
|
207
|
-
// NPM Package Analysis Tools
|
|
208
|
-
{
|
|
209
|
-
name: 'npmVersions',
|
|
210
|
-
description: 'Get all available versions of an NPM package',
|
|
211
|
-
parameters: z.union([
|
|
212
|
-
z.object({
|
|
213
|
-
packageName: z.string().describe('The name of the package'),
|
|
214
|
-
}),
|
|
215
|
-
z.object({
|
|
216
|
-
packages: z.array(z.string()).describe('List of package names to get versions for'),
|
|
217
|
-
}),
|
|
218
|
-
]),
|
|
219
|
-
inputSchema: {
|
|
220
|
-
type: 'object',
|
|
221
|
-
properties: {
|
|
222
|
-
packageName: { type: 'string' },
|
|
223
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
224
|
-
},
|
|
225
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
name: 'npmLatest',
|
|
230
|
-
description: 'Get the latest version and changelog of an NPM package',
|
|
231
|
-
parameters: z.union([
|
|
232
|
-
z.object({
|
|
233
|
-
packageName: z.string().describe('The name of the package'),
|
|
234
|
-
}),
|
|
235
|
-
z.object({
|
|
236
|
-
packages: z.array(z.string()).describe('List of package names to get latest versions for'),
|
|
237
|
-
}),
|
|
238
|
-
]),
|
|
239
|
-
inputSchema: {
|
|
240
|
-
type: 'object',
|
|
241
|
-
properties: {
|
|
242
|
-
packageName: { type: 'string' },
|
|
243
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
244
|
-
},
|
|
245
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
name: 'npmDeps',
|
|
250
|
-
description: 'Analyze dependencies and devDependencies of an NPM package',
|
|
251
|
-
parameters: z.union([
|
|
252
|
-
z.object({
|
|
253
|
-
packageName: z.string().describe('The name of the package'),
|
|
254
|
-
}),
|
|
255
|
-
z.object({
|
|
256
|
-
packages: z.array(z.string()).describe('List of package names to analyze dependencies for'),
|
|
257
|
-
}),
|
|
258
|
-
]),
|
|
259
|
-
inputSchema: {
|
|
260
|
-
type: 'object',
|
|
261
|
-
properties: {
|
|
262
|
-
packageName: { type: 'string' },
|
|
263
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
264
|
-
},
|
|
265
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
name: 'npmTypes',
|
|
270
|
-
description: 'Check TypeScript types availability and version for a package',
|
|
271
|
-
parameters: z.union([
|
|
272
|
-
z.object({
|
|
273
|
-
packageName: z.string().describe('The name of the package'),
|
|
274
|
-
}),
|
|
275
|
-
z.object({
|
|
276
|
-
packages: z.array(z.string()).describe('List of package names to check types for'),
|
|
277
|
-
}),
|
|
278
|
-
]),
|
|
279
|
-
inputSchema: {
|
|
280
|
-
type: 'object',
|
|
281
|
-
properties: {
|
|
282
|
-
packageName: { type: 'string' },
|
|
283
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
284
|
-
},
|
|
285
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
{
|
|
289
|
-
name: 'npmSize',
|
|
290
|
-
description: 'Get package size information including dependencies and bundle size',
|
|
291
|
-
parameters: z.union([
|
|
292
|
-
z.object({
|
|
293
|
-
packageName: z.string().describe('The name of the package'),
|
|
294
|
-
}),
|
|
295
|
-
z.object({
|
|
296
|
-
packages: z.array(z.string()).describe('List of package names to get size information for'),
|
|
297
|
-
}),
|
|
298
|
-
]),
|
|
299
|
-
inputSchema: {
|
|
300
|
-
type: 'object',
|
|
301
|
-
properties: {
|
|
302
|
-
packageName: { type: 'string' },
|
|
303
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
304
|
-
},
|
|
305
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
name: 'npmVulnerabilities',
|
|
310
|
-
description: 'Check for known vulnerabilities in packages',
|
|
311
|
-
parameters: z.union([
|
|
312
|
-
z.object({
|
|
313
|
-
packageName: z.string().describe('The name of the package'),
|
|
314
|
-
}),
|
|
315
|
-
z.object({
|
|
316
|
-
packages: z
|
|
317
|
-
.array(z.string())
|
|
318
|
-
.describe('List of package names to check for vulnerabilities'),
|
|
319
|
-
}),
|
|
320
|
-
]),
|
|
321
|
-
inputSchema: {
|
|
322
|
-
type: 'object',
|
|
323
|
-
properties: {
|
|
324
|
-
packageName: { type: 'string' },
|
|
325
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
326
|
-
},
|
|
327
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
name: 'npmTrends',
|
|
332
|
-
description: 'Get download trends and popularity metrics for packages. Available periods: "last-week" (7 days), "last-month" (30 days), or "last-year" (365 days)',
|
|
333
|
-
parameters: z.object({
|
|
334
|
-
packages: z.array(z.string()).describe('List of package names to get trends for'),
|
|
335
|
-
period: z
|
|
336
|
-
.enum(['last-week', 'last-month', 'last-year'])
|
|
337
|
-
.describe('Time period for trends. Options: "last-week", "last-month", "last-year"')
|
|
338
|
-
.optional()
|
|
339
|
-
.default('last-month'),
|
|
340
|
-
}),
|
|
341
|
-
inputSchema: {
|
|
342
|
-
type: 'object',
|
|
343
|
-
properties: {
|
|
344
|
-
packages: {
|
|
345
|
-
type: 'array',
|
|
346
|
-
items: { type: 'string' },
|
|
347
|
-
description: 'List of package names to get trends for',
|
|
348
|
-
},
|
|
349
|
-
period: {
|
|
350
|
-
type: 'string',
|
|
351
|
-
enum: ['last-week', 'last-month', 'last-year'],
|
|
352
|
-
description: 'Time period for trends. Options: "last-week" (7 days), "last-month" (30 days), or "last-year" (365 days)',
|
|
353
|
-
default: 'last-month',
|
|
354
|
-
},
|
|
355
|
-
},
|
|
356
|
-
required: ['packages'],
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
name: 'npmCompare',
|
|
361
|
-
description: 'Compare multiple NPM packages based on various metrics',
|
|
362
|
-
parameters: z.object({
|
|
363
|
-
packages: z.array(z.string()).describe('List of package names to compare'),
|
|
364
|
-
}),
|
|
365
|
-
inputSchema: {
|
|
366
|
-
type: 'object',
|
|
367
|
-
properties: {
|
|
368
|
-
packages: {
|
|
369
|
-
type: 'array',
|
|
370
|
-
items: { type: 'string' },
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
required: ['packages'],
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
name: 'npmMaintainers',
|
|
378
|
-
description: 'Get maintainers for an NPM package',
|
|
379
|
-
parameters: z.object({
|
|
380
|
-
packageName: z.string().describe('The name of the package'),
|
|
381
|
-
}),
|
|
382
|
-
inputSchema: {
|
|
383
|
-
type: 'object',
|
|
384
|
-
properties: {
|
|
385
|
-
packageName: { type: 'string' },
|
|
386
|
-
},
|
|
387
|
-
required: ['packageName'],
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
name: 'npmScore',
|
|
392
|
-
description: 'Get consolidated package score based on quality, maintenance, and popularity metrics',
|
|
393
|
-
parameters: z.union([
|
|
394
|
-
z.object({
|
|
395
|
-
packageName: z.string().describe('The name of the package'),
|
|
396
|
-
}),
|
|
397
|
-
z.object({
|
|
398
|
-
packages: z.array(z.string()).describe('List of package names to get scores for'),
|
|
399
|
-
}),
|
|
400
|
-
]),
|
|
401
|
-
inputSchema: {
|
|
402
|
-
type: 'object',
|
|
403
|
-
properties: {
|
|
404
|
-
packageName: { type: 'string' },
|
|
405
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
406
|
-
},
|
|
407
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
408
|
-
},
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
name: 'npmPackageReadme',
|
|
412
|
-
description: 'Get the README for an NPM package',
|
|
413
|
-
parameters: z.union([
|
|
414
|
-
z.object({
|
|
415
|
-
packageName: z.string().describe('The name of the package'),
|
|
416
|
-
}),
|
|
417
|
-
z.object({
|
|
418
|
-
packages: z.array(z.string()).describe('List of package names to get READMEs for'),
|
|
419
|
-
}),
|
|
420
|
-
]),
|
|
421
|
-
inputSchema: {
|
|
422
|
-
type: 'object',
|
|
423
|
-
properties: {
|
|
424
|
-
packageName: { type: 'string' },
|
|
425
|
-
packages: { type: 'array', items: { type: 'string' } },
|
|
426
|
-
},
|
|
427
|
-
oneOf: [{ required: ['packageName'] }, { required: ['packages'] }],
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
name: 'npmSearch',
|
|
432
|
-
description: 'Search for NPM packages',
|
|
433
|
-
parameters: z.object({
|
|
434
|
-
query: z.string().describe('Search query for packages'),
|
|
435
|
-
limit: z
|
|
436
|
-
.number()
|
|
437
|
-
.min(1)
|
|
438
|
-
.max(50)
|
|
439
|
-
.optional()
|
|
440
|
-
.describe('Maximum number of results to return (default: 10)'),
|
|
441
|
-
}),
|
|
442
|
-
inputSchema: {
|
|
443
|
-
type: 'object',
|
|
444
|
-
properties: {
|
|
445
|
-
query: { type: 'string' },
|
|
446
|
-
limit: { type: 'number', minimum: 1, maximum: 50 },
|
|
447
|
-
},
|
|
448
|
-
required: ['query'],
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
];
|
|
452
241
|
// Type guards for API responses
|
|
453
242
|
function isNpmPackageInfo(data) {
|
|
454
243
|
return (typeof data === 'object' &&
|
|
@@ -488,850 +277,1855 @@ function isNpmDownloadsData(data) {
|
|
|
488
277
|
}
|
|
489
278
|
}
|
|
490
279
|
export async function handleNpmVersions(args) {
|
|
491
|
-
const results = await Promise.all(args.packages.map(async (pkg) => {
|
|
492
|
-
try {
|
|
493
|
-
const response = await fetch(`https://registry.npmjs.org/${pkg}`, {
|
|
494
|
-
headers: {
|
|
495
|
-
Accept: 'application/json',
|
|
496
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
497
|
-
},
|
|
498
|
-
});
|
|
499
|
-
if (!response.ok) {
|
|
500
|
-
throw new Error(`Failed to fetch package info: ${response.statusText}`);
|
|
501
|
-
}
|
|
502
|
-
const data = await response.json();
|
|
503
|
-
if (!isNpmPackageInfo(data)) {
|
|
504
|
-
throw new Error('Invalid package info format');
|
|
505
|
-
}
|
|
506
|
-
const versions = Object.keys(data.versions);
|
|
507
|
-
const latestVersion = data['dist-tags']?.latest;
|
|
508
|
-
return {
|
|
509
|
-
name: pkg,
|
|
510
|
-
versions,
|
|
511
|
-
latest: latestVersion,
|
|
512
|
-
success: true,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
catch (error) {
|
|
516
|
-
return {
|
|
517
|
-
name: pkg,
|
|
518
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
519
|
-
success: false,
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
}));
|
|
523
|
-
const content = results.map((result) => ({
|
|
524
|
-
type: 'text',
|
|
525
|
-
text: result.success
|
|
526
|
-
? `📦 ${result.name}:
|
|
527
|
-
Latest version: ${result.latest}
|
|
528
|
-
Available versions: ${result.versions.join(', ')}`
|
|
529
|
-
: `❌ Error fetching ${result.name}: ${result.error}`,
|
|
530
|
-
}));
|
|
531
|
-
return { content, isError: false };
|
|
532
|
-
}
|
|
533
|
-
export async function handleNpmLatest(args) {
|
|
534
|
-
const results = await Promise.all(args.packages.map(async (pkg) => {
|
|
535
|
-
try {
|
|
536
|
-
const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
|
|
537
|
-
headers: {
|
|
538
|
-
Accept: 'application/json',
|
|
539
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
540
|
-
},
|
|
541
|
-
});
|
|
542
|
-
if (!response.ok) {
|
|
543
|
-
throw new Error(`Failed to fetch latest version: ${response.statusText}`);
|
|
544
|
-
}
|
|
545
|
-
const data = await response.json();
|
|
546
|
-
const latestInfo = data;
|
|
547
|
-
return {
|
|
548
|
-
name: pkg,
|
|
549
|
-
version: latestInfo.version,
|
|
550
|
-
description: latestInfo.description,
|
|
551
|
-
author: latestInfo.author?.name,
|
|
552
|
-
license: latestInfo.license,
|
|
553
|
-
homepage: latestInfo.homepage,
|
|
554
|
-
success: true,
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
catch (err) {
|
|
558
|
-
return {
|
|
559
|
-
name: pkg,
|
|
560
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
561
|
-
success: false,
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
}));
|
|
565
|
-
const content = results.map((result) => ({
|
|
566
|
-
type: 'text',
|
|
567
|
-
text: result.success
|
|
568
|
-
? `📦 Latest version of ${result.name}:
|
|
569
|
-
Version: ${result.version}
|
|
570
|
-
Description: ${result.description || 'No description available'}
|
|
571
|
-
Author: ${result.author || 'Unknown'}
|
|
572
|
-
License: ${result.license || 'Unknown'}
|
|
573
|
-
Homepage: ${result.homepage || 'Not specified'}
|
|
574
|
-
---`
|
|
575
|
-
: `❌ Error fetching latest version for ${result.name}: ${result.error}`,
|
|
576
|
-
}));
|
|
577
|
-
return { content, isError: false };
|
|
578
|
-
}
|
|
579
|
-
export async function handleNpmDeps(args) {
|
|
580
280
|
try {
|
|
581
281
|
const packagesToProcess = args.packages || [];
|
|
582
282
|
if (packagesToProcess.length === 0) {
|
|
583
283
|
throw new Error('No package names provided');
|
|
584
284
|
}
|
|
585
|
-
const
|
|
285
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
286
|
+
let name = '';
|
|
287
|
+
if (typeof pkgInput === 'string') {
|
|
288
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
289
|
+
if (atIdx > 0) {
|
|
290
|
+
name = pkgInput.slice(0, atIdx);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
name = pkgInput;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
return {
|
|
298
|
+
packageInput: JSON.stringify(pkgInput),
|
|
299
|
+
packageName: 'unknown_package_input',
|
|
300
|
+
status: 'error',
|
|
301
|
+
error: 'Invalid package input type',
|
|
302
|
+
data: null,
|
|
303
|
+
message: 'Package input was not a string.',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (!name) {
|
|
307
|
+
return {
|
|
308
|
+
packageInput: pkgInput,
|
|
309
|
+
packageName: 'empty_package_name',
|
|
310
|
+
status: 'error',
|
|
311
|
+
error: 'Empty package name derived from input',
|
|
312
|
+
data: null,
|
|
313
|
+
message: 'Package name could not be determined from input.',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const cacheKey = generateCacheKey('handleNpmVersions', name);
|
|
317
|
+
const cachedData = cacheGet(cacheKey); // Using any for the diverse structure from this endpoint
|
|
318
|
+
if (cachedData) {
|
|
319
|
+
return {
|
|
320
|
+
packageInput: pkgInput,
|
|
321
|
+
packageName: name,
|
|
322
|
+
status: 'success_cache',
|
|
323
|
+
error: null,
|
|
324
|
+
data: cachedData,
|
|
325
|
+
message: `Successfully fetched versions for ${name} from cache.`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
586
328
|
try {
|
|
587
|
-
const response = await fetch(`https://registry.npmjs.org/${
|
|
329
|
+
const response = await fetch(`https://registry.npmjs.org/${name}`, {
|
|
588
330
|
headers: {
|
|
589
331
|
Accept: 'application/json',
|
|
590
332
|
'User-Agent': 'NPM-Sentinel-MCP',
|
|
591
333
|
},
|
|
592
334
|
});
|
|
593
335
|
if (!response.ok) {
|
|
594
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
packageInput: pkgInput,
|
|
338
|
+
packageName: name,
|
|
339
|
+
status: 'error',
|
|
340
|
+
error: `Failed to fetch package info: ${response.status} ${response.statusText}`,
|
|
341
|
+
data: null,
|
|
342
|
+
message: `Could not retrieve information for package ${name}.`,
|
|
343
|
+
};
|
|
595
344
|
}
|
|
596
|
-
const
|
|
597
|
-
if (!
|
|
598
|
-
return {
|
|
345
|
+
const data = await response.json();
|
|
346
|
+
if (!isNpmPackageInfo(data)) {
|
|
347
|
+
return {
|
|
348
|
+
packageInput: pkgInput,
|
|
349
|
+
packageName: name,
|
|
350
|
+
status: 'error',
|
|
351
|
+
error: 'Invalid package info format received from registry',
|
|
352
|
+
data: null,
|
|
353
|
+
message: `Received malformed data for package ${name}.`,
|
|
354
|
+
};
|
|
599
355
|
}
|
|
356
|
+
const allVersions = Object.keys(data.versions || {});
|
|
357
|
+
const tags = data['dist-tags'] || {};
|
|
358
|
+
const latestVersionTag = tags.latest || null;
|
|
359
|
+
const resultData = {
|
|
360
|
+
allVersions,
|
|
361
|
+
tags,
|
|
362
|
+
latestVersionTag,
|
|
363
|
+
};
|
|
364
|
+
cacheSet(cacheKey, resultData, CACHE_TTL_MEDIUM);
|
|
600
365
|
return {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
366
|
+
packageInput: pkgInput,
|
|
367
|
+
packageName: name,
|
|
368
|
+
status: 'success',
|
|
369
|
+
error: null,
|
|
370
|
+
data: resultData,
|
|
371
|
+
message: `Successfully fetched versions for ${name}.`,
|
|
606
372
|
};
|
|
607
373
|
}
|
|
608
374
|
catch (error) {
|
|
609
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
packageInput: pkgInput,
|
|
377
|
+
packageName: name,
|
|
378
|
+
status: 'error',
|
|
379
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
380
|
+
data: null,
|
|
381
|
+
message: `An unexpected error occurred while processing ${name}.`,
|
|
382
|
+
};
|
|
610
383
|
}
|
|
611
384
|
}));
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if ('error' in result) {
|
|
615
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
text += `📦 Dependencies for ${result.name}@${result.version}\n\n`;
|
|
619
|
-
if (Object.keys(result.dependencies).length > 0) {
|
|
620
|
-
text += 'Dependencies:\n';
|
|
621
|
-
for (const [dep, version] of Object.entries(result.dependencies)) {
|
|
622
|
-
text += `• ${dep}: ${version}\n`;
|
|
623
|
-
}
|
|
624
|
-
text += '\n';
|
|
625
|
-
}
|
|
626
|
-
if (Object.keys(result.devDependencies).length > 0) {
|
|
627
|
-
text += 'Dev Dependencies:\n';
|
|
628
|
-
for (const [dep, version] of Object.entries(result.devDependencies)) {
|
|
629
|
-
text += `• ${dep}: ${version}\n`;
|
|
630
|
-
}
|
|
631
|
-
text += '\n';
|
|
632
|
-
}
|
|
633
|
-
if (Object.keys(result.peerDependencies).length > 0) {
|
|
634
|
-
text += 'Peer Dependencies:\n';
|
|
635
|
-
for (const [dep, version] of Object.entries(result.peerDependencies)) {
|
|
636
|
-
text += `• ${dep}: ${version}\n`;
|
|
637
|
-
}
|
|
638
|
-
text += '\n';
|
|
639
|
-
}
|
|
640
|
-
text += '---\n\n';
|
|
641
|
-
}
|
|
642
|
-
return { content: [{ type: 'text', text }], isError: false };
|
|
385
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
386
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
643
387
|
}
|
|
644
388
|
catch (error) {
|
|
389
|
+
const errorResponse = JSON.stringify({
|
|
390
|
+
results: [],
|
|
391
|
+
error: `General error fetching versions: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
392
|
+
}, null, 2);
|
|
645
393
|
return {
|
|
646
|
-
content: [
|
|
647
|
-
{
|
|
648
|
-
type: 'text',
|
|
649
|
-
text: `Error fetching dependencies: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
650
|
-
},
|
|
651
|
-
],
|
|
394
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
652
395
|
isError: true,
|
|
653
396
|
};
|
|
654
397
|
}
|
|
655
398
|
}
|
|
656
|
-
export async function
|
|
399
|
+
export async function handleNpmLatest(args) {
|
|
657
400
|
try {
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
|
|
401
|
+
const packagesToProcess = args.packages || [];
|
|
402
|
+
if (packagesToProcess.length === 0) {
|
|
403
|
+
throw new Error('No package names provided');
|
|
404
|
+
}
|
|
405
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
406
|
+
let name = '';
|
|
407
|
+
let versionTag = 'latest'; // Default to 'latest'
|
|
408
|
+
if (typeof pkgInput === 'string') {
|
|
409
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
410
|
+
if (atIdx > 0) {
|
|
411
|
+
name = pkgInput.slice(0, atIdx);
|
|
412
|
+
versionTag = pkgInput.slice(atIdx + 1);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
name = pkgInput;
|
|
416
|
+
}
|
|
667
417
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
418
|
+
else {
|
|
419
|
+
return {
|
|
420
|
+
packageInput: JSON.stringify(pkgInput),
|
|
421
|
+
packageName: 'unknown_package_input',
|
|
422
|
+
versionQueried: versionTag,
|
|
423
|
+
status: 'error',
|
|
424
|
+
error: 'Invalid package input type',
|
|
425
|
+
data: null,
|
|
426
|
+
message: 'Package input was not a string.',
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (!name) {
|
|
430
|
+
return {
|
|
431
|
+
packageInput: pkgInput,
|
|
432
|
+
packageName: 'empty_package_name',
|
|
433
|
+
versionQueried: versionTag,
|
|
434
|
+
status: 'error',
|
|
435
|
+
error: 'Empty package name derived from input',
|
|
436
|
+
data: null,
|
|
437
|
+
message: 'Package name could not be determined from input.',
|
|
438
|
+
};
|
|
686
439
|
}
|
|
687
|
-
|
|
688
|
-
|
|
440
|
+
const cacheKey = generateCacheKey('handleNpmLatest', name, versionTag);
|
|
441
|
+
const cachedData = cacheGet(cacheKey); // Using any for the diverse structure from this endpoint
|
|
442
|
+
if (cachedData) {
|
|
443
|
+
return {
|
|
444
|
+
packageInput: pkgInput,
|
|
445
|
+
packageName: name,
|
|
446
|
+
versionQueried: versionTag,
|
|
447
|
+
status: 'success_cache',
|
|
448
|
+
error: null,
|
|
449
|
+
data: cachedData,
|
|
450
|
+
message: `Successfully fetched details for ${name}@${versionTag} from cache.`,
|
|
451
|
+
};
|
|
689
452
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
453
|
+
try {
|
|
454
|
+
const response = await fetch(`https://registry.npmjs.org/${name}/${versionTag}`, {
|
|
455
|
+
headers: {
|
|
456
|
+
Accept: 'application/json',
|
|
457
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
let errorMsg = `Failed to fetch package version: ${response.status} ${response.statusText}`;
|
|
462
|
+
if (response.status === 404) {
|
|
463
|
+
errorMsg = `Package ${name}@${versionTag} not found.`;
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
packageInput: pkgInput,
|
|
467
|
+
packageName: name,
|
|
468
|
+
versionQueried: versionTag,
|
|
469
|
+
status: 'error',
|
|
470
|
+
error: errorMsg,
|
|
471
|
+
data: null,
|
|
472
|
+
message: `Could not retrieve version ${versionTag} for package ${name}.`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const data = await response.json();
|
|
476
|
+
if (!isNpmPackageVersionData(data)) {
|
|
477
|
+
return {
|
|
478
|
+
packageInput: pkgInput,
|
|
479
|
+
packageName: name,
|
|
480
|
+
versionQueried: versionTag,
|
|
481
|
+
status: 'error',
|
|
482
|
+
error: 'Invalid package data format received for version',
|
|
483
|
+
data: null,
|
|
484
|
+
message: `Received malformed data for ${name}@${versionTag}.`,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const versionData = {
|
|
488
|
+
name: data.name,
|
|
489
|
+
version: data.version,
|
|
490
|
+
description: data.description || null,
|
|
491
|
+
author: (typeof data.author === 'string' ? data.author : data.author?.name) || null,
|
|
492
|
+
license: data.license || null,
|
|
493
|
+
homepage: data.homepage || null,
|
|
494
|
+
repositoryUrl: data.repository?.url || null,
|
|
495
|
+
bugsUrl: data.bugs?.url || null,
|
|
496
|
+
dependenciesCount: Object.keys(data.dependencies || {}).length,
|
|
497
|
+
devDependenciesCount: Object.keys(data.devDependencies || {}).length,
|
|
498
|
+
peerDependenciesCount: Object.keys(data.peerDependencies || {}).length,
|
|
499
|
+
dist: data.dist || null,
|
|
500
|
+
types: data.types || data.typings || null,
|
|
501
|
+
};
|
|
502
|
+
cacheSet(cacheKey, versionData, CACHE_TTL_MEDIUM);
|
|
503
|
+
return {
|
|
504
|
+
packageInput: pkgInput,
|
|
505
|
+
packageName: name,
|
|
506
|
+
versionQueried: versionTag,
|
|
507
|
+
status: 'success',
|
|
508
|
+
error: null,
|
|
509
|
+
data: versionData,
|
|
510
|
+
message: `Successfully fetched details for ${data.name}@${data.version}.`,
|
|
511
|
+
};
|
|
697
512
|
}
|
|
698
|
-
|
|
699
|
-
|
|
513
|
+
catch (error) {
|
|
514
|
+
return {
|
|
515
|
+
packageInput: pkgInput,
|
|
516
|
+
packageName: name,
|
|
517
|
+
versionQueried: versionTag,
|
|
518
|
+
status: 'error',
|
|
519
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
520
|
+
data: null,
|
|
521
|
+
message: `An unexpected error occurred while processing ${pkgInput}.`,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}));
|
|
525
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
526
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
700
527
|
}
|
|
701
528
|
catch (error) {
|
|
529
|
+
const errorResponse = JSON.stringify({
|
|
530
|
+
results: [],
|
|
531
|
+
error: `General error fetching latest package information: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
532
|
+
}, null, 2);
|
|
702
533
|
return {
|
|
703
|
-
content: [
|
|
704
|
-
{ type: 'text', text: `Error checking TypeScript types: ${error.message}` },
|
|
705
|
-
],
|
|
534
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
706
535
|
isError: true,
|
|
707
536
|
};
|
|
708
537
|
}
|
|
709
538
|
}
|
|
710
|
-
export async function
|
|
539
|
+
export async function handleNpmDeps(args) {
|
|
711
540
|
try {
|
|
712
541
|
const packagesToProcess = args.packages || [];
|
|
713
542
|
if (packagesToProcess.length === 0) {
|
|
714
543
|
throw new Error('No package names provided');
|
|
715
544
|
}
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
545
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
546
|
+
let name = '';
|
|
547
|
+
let version = 'latest'; // Default to 'latest'
|
|
548
|
+
if (typeof pkgInput === 'string') {
|
|
549
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
550
|
+
if (atIdx > 0) {
|
|
551
|
+
name = pkgInput.slice(0, atIdx);
|
|
552
|
+
version = pkgInput.slice(atIdx + 1);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
name = pkgInput;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
return {
|
|
560
|
+
package: 'unknown_package_input',
|
|
561
|
+
status: 'error',
|
|
562
|
+
error: 'Invalid package input type',
|
|
563
|
+
data: null,
|
|
564
|
+
message: 'Package input was not a string.',
|
|
565
|
+
};
|
|
725
566
|
}
|
|
726
|
-
const
|
|
727
|
-
if
|
|
728
|
-
|
|
567
|
+
const packageNameForOutput = version === 'latest' ? name : `${name}@${version}`;
|
|
568
|
+
// Note: The cache key should ideally use the *resolved* version if 'latest' is input.
|
|
569
|
+
// However, to get the resolved version, we need an API call. For simplicity in this step,
|
|
570
|
+
// we'll cache based on the input version string. This means 'latest' will be cached as 'latest'.
|
|
571
|
+
// A more advanced caching would fetch resolved version first if 'latest' is given.
|
|
572
|
+
const cacheKey = generateCacheKey('handleNpmDeps', name, version);
|
|
573
|
+
const cachedData = cacheGet(cacheKey);
|
|
574
|
+
if (cachedData) {
|
|
575
|
+
return {
|
|
576
|
+
package: cachedData.packageNameForCache || packageNameForOutput, // Use cached name if available
|
|
577
|
+
status: 'success_cache',
|
|
578
|
+
error: null,
|
|
579
|
+
data: cachedData.depData,
|
|
580
|
+
message: `Dependencies for ${cachedData.packageNameForCache || packageNameForOutput} from cache.`,
|
|
581
|
+
};
|
|
729
582
|
}
|
|
730
|
-
|
|
731
|
-
name
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
583
|
+
try {
|
|
584
|
+
const response = await fetch(`https://registry.npmjs.org/${name}/${version}`, {
|
|
585
|
+
headers: {
|
|
586
|
+
Accept: 'application/json',
|
|
587
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
if (!response.ok) {
|
|
591
|
+
return {
|
|
592
|
+
package: packageNameForOutput,
|
|
593
|
+
status: 'error',
|
|
594
|
+
error: `Failed to fetch package info: ${response.status} ${response.statusText}`,
|
|
595
|
+
data: null,
|
|
596
|
+
message: `Could not retrieve information for ${packageNameForOutput}.`,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const rawData = await response.json();
|
|
600
|
+
if (!isNpmPackageData(rawData)) {
|
|
601
|
+
return {
|
|
602
|
+
package: packageNameForOutput,
|
|
603
|
+
status: 'error',
|
|
604
|
+
error: 'Invalid package data received from registry',
|
|
605
|
+
data: null,
|
|
606
|
+
message: `Received malformed data for ${packageNameForOutput}.`,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const mapDeps = (deps) => {
|
|
610
|
+
if (!deps)
|
|
611
|
+
return [];
|
|
612
|
+
return Object.entries(deps).map(([depName, depVersion]) => ({
|
|
613
|
+
name: depName,
|
|
614
|
+
version: depVersion,
|
|
615
|
+
}));
|
|
616
|
+
};
|
|
617
|
+
const depData = {
|
|
618
|
+
dependencies: mapDeps(rawData.dependencies),
|
|
619
|
+
devDependencies: mapDeps(rawData.devDependencies),
|
|
620
|
+
peerDependencies: mapDeps(rawData.peerDependencies),
|
|
621
|
+
};
|
|
622
|
+
const actualVersion = rawData.version || version; // Use version from response if available
|
|
623
|
+
const finalPackageName = `${name}@${actualVersion}`;
|
|
624
|
+
// Store with the actual resolved package name if 'latest' was used
|
|
625
|
+
cacheSet(cacheKey, { depData, packageNameForCache: finalPackageName }, CACHE_TTL_MEDIUM);
|
|
626
|
+
return {
|
|
627
|
+
package: finalPackageName,
|
|
628
|
+
status: 'success',
|
|
629
|
+
error: null,
|
|
630
|
+
data: depData,
|
|
631
|
+
message: `Dependencies for ${finalPackageName}`,
|
|
632
|
+
};
|
|
741
633
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
634
|
+
catch (error) {
|
|
635
|
+
return {
|
|
636
|
+
package: packageNameForOutput,
|
|
637
|
+
status: 'error',
|
|
638
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
639
|
+
data: null,
|
|
640
|
+
message: `An unexpected error occurred while processing ${packageNameForOutput}.`,
|
|
641
|
+
};
|
|
746
642
|
}
|
|
747
|
-
}
|
|
748
|
-
|
|
643
|
+
}));
|
|
644
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
645
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
749
646
|
}
|
|
750
647
|
catch (error) {
|
|
648
|
+
const errorResponse = JSON.stringify({
|
|
649
|
+
results: [],
|
|
650
|
+
error: `General error fetching dependencies: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
651
|
+
}, null, 2);
|
|
751
652
|
return {
|
|
752
|
-
content: [
|
|
753
|
-
{
|
|
754
|
-
type: 'text',
|
|
755
|
-
text: `Error fetching package sizes: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
756
|
-
},
|
|
757
|
-
],
|
|
653
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
758
654
|
isError: true,
|
|
759
655
|
};
|
|
760
656
|
}
|
|
761
657
|
}
|
|
762
|
-
export async function
|
|
658
|
+
export async function handleNpmTypes(args) {
|
|
763
659
|
try {
|
|
764
660
|
const packagesToProcess = args.packages || [];
|
|
765
661
|
if (packagesToProcess.length === 0) {
|
|
766
662
|
throw new Error('No package names provided');
|
|
767
663
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
if (!response.ok) {
|
|
782
|
-
return { name: pkg, error: `Failed to fetch vulnerability info: ${response.statusText}` };
|
|
664
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
665
|
+
let name = '';
|
|
666
|
+
let version = 'latest'; // Default to 'latest'
|
|
667
|
+
if (typeof pkgInput === 'string') {
|
|
668
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
669
|
+
if (atIdx > 0) {
|
|
670
|
+
name = pkgInput.slice(0, atIdx);
|
|
671
|
+
version = pkgInput.slice(atIdx + 1);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
name = pkgInput;
|
|
675
|
+
}
|
|
783
676
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
677
|
+
else {
|
|
678
|
+
return {
|
|
679
|
+
package: 'unknown_package_input',
|
|
680
|
+
status: 'error',
|
|
681
|
+
error: 'Invalid package input type',
|
|
682
|
+
data: null,
|
|
683
|
+
message: 'Package input was not a string.',
|
|
684
|
+
};
|
|
792
685
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
686
|
+
const packageNameForOutput = version === 'latest' ? name : `${name}@${version}`;
|
|
687
|
+
// As with handleNpmDeps, we cache based on the input version string for simplicity.
|
|
688
|
+
const cacheKey = generateCacheKey('handleNpmTypes', name, version);
|
|
689
|
+
const cachedData = cacheGet(cacheKey);
|
|
690
|
+
if (cachedData) {
|
|
691
|
+
return {
|
|
692
|
+
package: cachedData.finalPackageName || packageNameForOutput,
|
|
693
|
+
status: 'success_cache',
|
|
694
|
+
error: null,
|
|
695
|
+
data: cachedData.typesData,
|
|
696
|
+
message: `TypeScript information for ${cachedData.finalPackageName || packageNameForOutput} from cache.`,
|
|
697
|
+
};
|
|
796
698
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
699
|
+
try {
|
|
700
|
+
const response = await fetch(`https://registry.npmjs.org/${name}/${version}`, {
|
|
701
|
+
headers: {
|
|
702
|
+
Accept: 'application/json',
|
|
703
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
return {
|
|
708
|
+
package: packageNameForOutput,
|
|
709
|
+
status: 'error',
|
|
710
|
+
error: `Failed to fetch package info: ${response.status} ${response.statusText}`,
|
|
711
|
+
data: null,
|
|
712
|
+
message: `Could not retrieve information for ${packageNameForOutput}.`,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const mainPackageData = (await response.json());
|
|
716
|
+
const actualVersion = mainPackageData.version || version; // Use version from response
|
|
717
|
+
const finalPackageName = `${name}@${actualVersion}`;
|
|
718
|
+
const hasBuiltInTypes = Boolean(mainPackageData.types || mainPackageData.typings);
|
|
719
|
+
const typesPath = mainPackageData.types || mainPackageData.typings || null;
|
|
720
|
+
const typesPackageName = `@types/${name.replace('@', '').replace('/', '__')}`;
|
|
721
|
+
let typesPackageInfo = {
|
|
722
|
+
name: typesPackageName,
|
|
723
|
+
version: null,
|
|
724
|
+
isAvailable: false,
|
|
725
|
+
};
|
|
726
|
+
try {
|
|
727
|
+
const typesResponse = await fetch(`https://registry.npmjs.org/${typesPackageName}/latest`, {
|
|
728
|
+
headers: {
|
|
729
|
+
Accept: 'application/json',
|
|
730
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
if (typesResponse.ok) {
|
|
734
|
+
const typesData = (await typesResponse.json());
|
|
735
|
+
typesPackageInfo = {
|
|
736
|
+
name: typesPackageName,
|
|
737
|
+
version: typesData.version || 'unknown',
|
|
738
|
+
isAvailable: true,
|
|
739
|
+
};
|
|
807
740
|
}
|
|
808
|
-
text += '\n';
|
|
809
741
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
export async function handleNpmTrends(args) {
|
|
828
|
-
try {
|
|
829
|
-
// Si period es undefined, vacío o inválido, usar el valor por defecto
|
|
830
|
-
const period = args.period && ['last-week', 'last-month', 'last-year'].includes(args.period)
|
|
831
|
-
? args.period
|
|
832
|
-
: 'last-month';
|
|
833
|
-
const periodDays = {
|
|
834
|
-
'last-week': 7,
|
|
835
|
-
'last-month': 30,
|
|
836
|
-
'last-year': 365,
|
|
837
|
-
};
|
|
838
|
-
const results = await Promise.all(args.packages.map(async (pkg) => {
|
|
839
|
-
const response = await fetch(`https://api.npmjs.org/downloads/point/${period}/${pkg}`, {
|
|
840
|
-
headers: {
|
|
841
|
-
Accept: 'application/json',
|
|
842
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
843
|
-
},
|
|
844
|
-
});
|
|
845
|
-
if (!response.ok) {
|
|
742
|
+
catch (typesError) {
|
|
743
|
+
// Keep this debug for visibility on @types fetch failures
|
|
744
|
+
console.debug(`Could not fetch @types package ${typesPackageName}: ${typesError}`);
|
|
745
|
+
}
|
|
746
|
+
const resultData = {
|
|
747
|
+
mainPackage: {
|
|
748
|
+
name: name,
|
|
749
|
+
version: actualVersion,
|
|
750
|
+
hasBuiltInTypes: hasBuiltInTypes,
|
|
751
|
+
typesPath: typesPath,
|
|
752
|
+
},
|
|
753
|
+
typesPackage: typesPackageInfo,
|
|
754
|
+
};
|
|
755
|
+
cacheSet(cacheKey, { typesData: resultData, finalPackageName }, CACHE_TTL_LONG);
|
|
846
756
|
return {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
757
|
+
package: finalPackageName,
|
|
758
|
+
status: 'success',
|
|
759
|
+
error: null,
|
|
760
|
+
data: resultData,
|
|
761
|
+
message: `TypeScript information for ${finalPackageName}`,
|
|
850
762
|
};
|
|
851
763
|
}
|
|
852
|
-
|
|
853
|
-
if (!isNpmDownloadsData(data)) {
|
|
764
|
+
catch (error) {
|
|
854
765
|
return {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
766
|
+
package: packageNameForOutput,
|
|
767
|
+
status: 'error',
|
|
768
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
769
|
+
data: null,
|
|
770
|
+
message: `An unexpected error occurred while processing ${packageNameForOutput}.`,
|
|
858
771
|
};
|
|
859
772
|
}
|
|
860
|
-
return {
|
|
861
|
-
name: pkg,
|
|
862
|
-
downloads: data.downloads,
|
|
863
|
-
success: true,
|
|
864
|
-
};
|
|
865
773
|
}));
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
// Individual package stats
|
|
869
|
-
for (const result of results) {
|
|
870
|
-
if (!result.success) {
|
|
871
|
-
text += `❌ ${result.name}: ${result.error}\n`;
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
text += `📦 ${result.name}\n`;
|
|
875
|
-
text += `Total downloads: ${result.downloads.toLocaleString()}\n`;
|
|
876
|
-
text += `Average daily downloads: ${Math.round(result.downloads / periodDays[period]).toLocaleString()}\n\n`;
|
|
877
|
-
}
|
|
878
|
-
// Total stats
|
|
879
|
-
const totalDownloads = results.reduce((total, result) => {
|
|
880
|
-
if (result.success) {
|
|
881
|
-
return total + result.downloads;
|
|
882
|
-
}
|
|
883
|
-
return total;
|
|
884
|
-
}, 0);
|
|
885
|
-
text += `Total downloads across all packages: ${totalDownloads.toLocaleString()}\n`;
|
|
886
|
-
text += `Average daily downloads across all packages: ${Math.round(totalDownloads / periodDays[period]).toLocaleString()}\n`;
|
|
887
|
-
return { content: [{ type: 'text', text }], isError: false };
|
|
774
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
775
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
888
776
|
}
|
|
889
777
|
catch (error) {
|
|
778
|
+
const errorResponse = JSON.stringify({
|
|
779
|
+
results: [],
|
|
780
|
+
error: `General error checking TypeScript types: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
781
|
+
}, null, 2);
|
|
890
782
|
return {
|
|
891
|
-
content: [
|
|
892
|
-
{ type: 'text', text: `Error fetching download trends: ${error.message}` },
|
|
893
|
-
],
|
|
783
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
894
784
|
isError: true,
|
|
895
785
|
};
|
|
896
786
|
}
|
|
897
787
|
}
|
|
898
|
-
export async function
|
|
788
|
+
export async function handleNpmSize(args) {
|
|
899
789
|
try {
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
fetch(`https://api.npmjs.org/downloads/point/last-month/${pkg}`),
|
|
904
|
-
]);
|
|
905
|
-
if (!infoRes.ok || !downloadsRes.ok) {
|
|
906
|
-
throw new Error(`Failed to fetch data for ${pkg}`);
|
|
907
|
-
}
|
|
908
|
-
const info = await infoRes.json();
|
|
909
|
-
const downloads = await downloadsRes.json();
|
|
910
|
-
if (!isNpmPackageData(info) || !isNpmDownloadsData(downloads)) {
|
|
911
|
-
throw new Error(`Invalid response format for ${pkg}`);
|
|
912
|
-
}
|
|
913
|
-
return {
|
|
914
|
-
name: pkg,
|
|
915
|
-
version: info.version,
|
|
916
|
-
description: info.description,
|
|
917
|
-
downloads: downloads.downloads,
|
|
918
|
-
license: info.license,
|
|
919
|
-
dependencies: Object.keys(info.dependencies || {}).length,
|
|
920
|
-
};
|
|
921
|
-
}));
|
|
922
|
-
let text = '📊 Package Comparison\n\n';
|
|
923
|
-
// Table header
|
|
924
|
-
text += 'Package | Version | Monthly Downloads | Dependencies | License\n';
|
|
925
|
-
text += '--------|---------|------------------|--------------|--------\n';
|
|
926
|
-
// Table rows
|
|
927
|
-
for (const pkg of results) {
|
|
928
|
-
text += `${pkg.name} | ${pkg.version} | ${pkg.downloads.toLocaleString()} | ${pkg.dependencies} | ${pkg.license || 'N/A'}\n`;
|
|
790
|
+
const packagesToProcess = args.packages || [];
|
|
791
|
+
if (packagesToProcess.length === 0) {
|
|
792
|
+
throw new Error('No package names provided');
|
|
929
793
|
}
|
|
930
|
-
|
|
794
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
795
|
+
let name = '';
|
|
796
|
+
let version = 'latest'; // Default to 'latest'
|
|
797
|
+
if (typeof pkgInput === 'string') {
|
|
798
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
799
|
+
if (atIdx > 0) {
|
|
800
|
+
name = pkgInput.slice(0, atIdx);
|
|
801
|
+
version = pkgInput.slice(atIdx + 1);
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
name = pkgInput;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
return {
|
|
809
|
+
package: 'unknown_package_input',
|
|
810
|
+
status: 'error',
|
|
811
|
+
error: 'Invalid package input type',
|
|
812
|
+
data: null,
|
|
813
|
+
message: 'Package input was not a string.',
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const bundlephobiaQuery = version === 'latest' ? name : `${name}@${version}`;
|
|
817
|
+
const packageNameForOutput = bundlephobiaQuery;
|
|
818
|
+
const cacheKey = generateCacheKey('handleNpmSize', bundlephobiaQuery);
|
|
819
|
+
const cachedData = cacheGet(cacheKey);
|
|
820
|
+
if (cachedData) {
|
|
821
|
+
return {
|
|
822
|
+
package: packageNameForOutput, // Or cachedData.packageName if stored
|
|
823
|
+
status: 'success_cache',
|
|
824
|
+
error: null,
|
|
825
|
+
data: cachedData,
|
|
826
|
+
message: `Size information for ${packageNameForOutput} from cache.`,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const response = await fetch(`https://bundlephobia.com/api/size?package=${bundlephobiaQuery}`, {
|
|
831
|
+
headers: {
|
|
832
|
+
Accept: 'application/json',
|
|
833
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
if (!response.ok) {
|
|
837
|
+
let errorMsg = `Failed to fetch package size: ${response.status} ${response.statusText}`;
|
|
838
|
+
if (response.status === 404) {
|
|
839
|
+
errorMsg = `Package ${packageNameForOutput} not found or version not available on Bundlephobia.`;
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
package: packageNameForOutput,
|
|
843
|
+
status: 'error',
|
|
844
|
+
error: errorMsg,
|
|
845
|
+
data: null,
|
|
846
|
+
message: `Could not retrieve size information for ${packageNameForOutput}.`,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const rawData = await response.json();
|
|
850
|
+
if (rawData.error) {
|
|
851
|
+
return {
|
|
852
|
+
package: packageNameForOutput,
|
|
853
|
+
status: 'error',
|
|
854
|
+
error: `Bundlephobia error: ${rawData.error.message || 'Unknown error'}`,
|
|
855
|
+
data: null,
|
|
856
|
+
message: `Bundlephobia reported an error for ${packageNameForOutput}.`,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (!isBundlephobiaData(rawData)) {
|
|
860
|
+
return {
|
|
861
|
+
package: packageNameForOutput,
|
|
862
|
+
status: 'error',
|
|
863
|
+
error: 'Invalid package data received from Bundlephobia',
|
|
864
|
+
data: null,
|
|
865
|
+
message: `Received malformed size data for ${packageNameForOutput}.`,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
const typedRawData = rawData;
|
|
869
|
+
const sizeData = {
|
|
870
|
+
name: typedRawData.name || name,
|
|
871
|
+
version: typedRawData.version || (version === 'latest' ? 'latest_resolved' : version),
|
|
872
|
+
sizeInKb: Number((typedRawData.size / 1024).toFixed(2)),
|
|
873
|
+
gzipInKb: Number((typedRawData.gzip / 1024).toFixed(2)),
|
|
874
|
+
dependencyCount: typedRawData.dependencyCount,
|
|
875
|
+
};
|
|
876
|
+
cacheSet(cacheKey, sizeData, CACHE_TTL_MEDIUM);
|
|
877
|
+
return {
|
|
878
|
+
package: packageNameForOutput,
|
|
879
|
+
status: 'success',
|
|
880
|
+
error: null,
|
|
881
|
+
data: sizeData,
|
|
882
|
+
message: `Size information for ${packageNameForOutput}`,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
return {
|
|
887
|
+
package: packageNameForOutput,
|
|
888
|
+
status: 'error',
|
|
889
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
890
|
+
data: null,
|
|
891
|
+
message: `An unexpected error occurred while processing ${packageNameForOutput}.`,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
}));
|
|
895
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
896
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
931
897
|
}
|
|
932
898
|
catch (error) {
|
|
899
|
+
const errorResponse = JSON.stringify({
|
|
900
|
+
results: [],
|
|
901
|
+
error: `General error fetching package sizes: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
902
|
+
}, null, 2);
|
|
933
903
|
return {
|
|
934
|
-
content: [{ type: 'text', text:
|
|
904
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
935
905
|
isError: true,
|
|
936
906
|
};
|
|
937
907
|
}
|
|
938
908
|
}
|
|
939
|
-
|
|
940
|
-
export async function handleNpmQuality(args) {
|
|
909
|
+
export async function handleNpmVulnerabilities(args) {
|
|
941
910
|
try {
|
|
942
|
-
const
|
|
943
|
-
|
|
911
|
+
const packagesToProcess = args.packages || [];
|
|
912
|
+
if (packagesToProcess.length === 0) {
|
|
913
|
+
throw new Error('No package names provided');
|
|
914
|
+
}
|
|
915
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
916
|
+
let name = '';
|
|
917
|
+
let version = undefined;
|
|
918
|
+
if (typeof pkgInput === 'string') {
|
|
919
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
920
|
+
if (atIdx > 0) {
|
|
921
|
+
name = pkgInput.slice(0, atIdx);
|
|
922
|
+
version = pkgInput.slice(atIdx + 1);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
name = pkgInput;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
else if (typeof pkgInput === 'object' && pkgInput !== null) {
|
|
929
|
+
name = pkgInput.name;
|
|
930
|
+
version = pkgInput.version;
|
|
931
|
+
}
|
|
932
|
+
const packageNameForOutput = version ? `${name}@${version}` : name;
|
|
933
|
+
const cacheKey = generateCacheKey('handleNpmVulnerabilities', name, version || 'all');
|
|
934
|
+
const cachedData = cacheGet(cacheKey);
|
|
935
|
+
if (cachedData) {
|
|
936
|
+
return {
|
|
937
|
+
package: packageNameForOutput,
|
|
938
|
+
versionQueried: version || null,
|
|
939
|
+
status: 'success_cache',
|
|
940
|
+
vulnerabilities: cachedData.vulnerabilities,
|
|
941
|
+
message: `${cachedData.message} (from cache)`,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const osvBody = {
|
|
945
|
+
package: {
|
|
946
|
+
name,
|
|
947
|
+
ecosystem: 'npm',
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
if (version) {
|
|
951
|
+
osvBody.version = version;
|
|
952
|
+
}
|
|
953
|
+
const response = await fetch('https://api.osv.dev/v1/query', {
|
|
954
|
+
method: 'POST',
|
|
944
955
|
headers: {
|
|
945
|
-
|
|
946
|
-
'User-Agent': 'NPM-Sentinel-MCP',
|
|
956
|
+
'Content-Type': 'application/json',
|
|
947
957
|
},
|
|
958
|
+
body: JSON.stringify(osvBody),
|
|
948
959
|
});
|
|
960
|
+
const queryVersionSpecified = !!version;
|
|
949
961
|
if (!response.ok) {
|
|
950
|
-
|
|
962
|
+
const errorResult = {
|
|
963
|
+
package: packageNameForOutput,
|
|
964
|
+
versionQueried: version || null,
|
|
965
|
+
status: 'error',
|
|
966
|
+
error: `OSV API Error: ${response.statusText}`,
|
|
967
|
+
vulnerabilities: [],
|
|
968
|
+
};
|
|
969
|
+
// Do not cache error responses from OSV API as they might be temporary
|
|
970
|
+
return errorResult;
|
|
951
971
|
}
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
972
|
+
const data = (await response.json());
|
|
973
|
+
const vulns = data.vulns || [];
|
|
974
|
+
let message;
|
|
975
|
+
if (vulns.length === 0) {
|
|
976
|
+
message = `No known vulnerabilities found${queryVersionSpecified ? ' for the specified version' : ''}.`;
|
|
955
977
|
}
|
|
956
|
-
|
|
978
|
+
else {
|
|
979
|
+
message = `${vulns.length} vulnerability(ies) found${queryVersionSpecified ? ' for the specified version' : ''}.`;
|
|
980
|
+
}
|
|
981
|
+
const processedVulns = vulns.map((vuln) => {
|
|
982
|
+
const sev = typeof vuln.severity === 'object'
|
|
983
|
+
? vuln.severity.type || 'Unknown'
|
|
984
|
+
: vuln.severity || 'Unknown';
|
|
985
|
+
const refs = vuln.references ? vuln.references.map((r) => r.url) : [];
|
|
986
|
+
const affectedRanges = [];
|
|
987
|
+
const affectedVersionsListed = [];
|
|
988
|
+
const vulnerabilityDetails = {
|
|
989
|
+
summary: vuln.summary,
|
|
990
|
+
severity: sev,
|
|
991
|
+
references: refs,
|
|
992
|
+
};
|
|
993
|
+
if (vuln.affected && vuln.affected.length > 0) {
|
|
994
|
+
const lifecycle = {};
|
|
995
|
+
const firstAffectedEvents = vuln.affected[0]?.ranges?.[0]?.events;
|
|
996
|
+
if (firstAffectedEvents) {
|
|
997
|
+
const introducedEvent = firstAffectedEvents.find((e) => e.introduced);
|
|
998
|
+
const fixedEvent = firstAffectedEvents.find((e) => e.fixed);
|
|
999
|
+
if (introducedEvent?.introduced)
|
|
1000
|
+
lifecycle.introduced = introducedEvent.introduced;
|
|
1001
|
+
if (fixedEvent?.fixed)
|
|
1002
|
+
lifecycle.fixed = fixedEvent.fixed;
|
|
1003
|
+
}
|
|
1004
|
+
if (Object.keys(lifecycle).length > 0) {
|
|
1005
|
+
vulnerabilityDetails.lifecycle = lifecycle;
|
|
1006
|
+
if (queryVersionSpecified && version && lifecycle.fixed) {
|
|
1007
|
+
const queriedParts = version.split('.').map(Number);
|
|
1008
|
+
const fixedParts = lifecycle.fixed.split('.').map(Number);
|
|
1009
|
+
let isFixedDecision = false;
|
|
1010
|
+
const maxLength = Math.max(queriedParts.length, fixedParts.length);
|
|
1011
|
+
for (let i = 0; i < maxLength; i++) {
|
|
1012
|
+
const qp = queriedParts[i] || 0;
|
|
1013
|
+
const fp = fixedParts[i] || 0;
|
|
1014
|
+
if (fp < qp) {
|
|
1015
|
+
isFixedDecision = true;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
if (fp > qp) {
|
|
1019
|
+
isFixedDecision = false;
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
if (i === maxLength - 1) {
|
|
1023
|
+
isFixedDecision = fixedParts.length <= queriedParts.length;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
vulnerabilityDetails.isFixedInQueriedVersion = isFixedDecision;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (!queryVersionSpecified && vuln.affected) {
|
|
1031
|
+
for (const aff of vuln.affected) {
|
|
1032
|
+
if (aff.ranges) {
|
|
1033
|
+
for (const range of aff.ranges) {
|
|
1034
|
+
affectedRanges.push({ type: range.type, events: range.events });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (aff.versions && aff.versions.length > 0) {
|
|
1038
|
+
affectedVersionsListed.push(...aff.versions);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (affectedRanges.length > 0) {
|
|
1042
|
+
vulnerabilityDetails.affectedRanges = affectedRanges;
|
|
1043
|
+
}
|
|
1044
|
+
if (affectedVersionsListed.length > 0) {
|
|
1045
|
+
vulnerabilityDetails.affectedVersionsListed = affectedVersionsListed;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return vulnerabilityDetails;
|
|
1049
|
+
});
|
|
1050
|
+
const resultToCache = {
|
|
1051
|
+
vulnerabilities: processedVulns,
|
|
1052
|
+
message: message,
|
|
1053
|
+
};
|
|
1054
|
+
cacheSet(cacheKey, resultToCache, CACHE_TTL_MEDIUM);
|
|
957
1055
|
return {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
linting: 0,
|
|
964
|
-
types: 0,
|
|
965
|
-
}),
|
|
1056
|
+
package: packageNameForOutput,
|
|
1057
|
+
versionQueried: version || null,
|
|
1058
|
+
status: 'success',
|
|
1059
|
+
vulnerabilities: processedVulns,
|
|
1060
|
+
message: message,
|
|
966
1061
|
};
|
|
967
1062
|
}));
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if ('error' in result) {
|
|
971
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
text += `📦 ${result.name}\n`;
|
|
975
|
-
text += `- Overall Score: ${result.score}\n`;
|
|
976
|
-
text +=
|
|
977
|
-
'- Note: Detailed metrics (tests, coverage, linting, types) are no longer provided by the API\n\n';
|
|
978
|
-
}
|
|
979
|
-
return { content: [{ type: 'text', text }], isError: false };
|
|
1063
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
1064
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
980
1065
|
}
|
|
981
1066
|
catch (error) {
|
|
1067
|
+
const errorResponse = JSON.stringify({
|
|
1068
|
+
results: [],
|
|
1069
|
+
error: `General error checking vulnerabilities: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1070
|
+
}, null, 2);
|
|
982
1071
|
return {
|
|
983
|
-
content: [
|
|
984
|
-
{
|
|
985
|
-
type: 'text',
|
|
986
|
-
text: `Error fetching quality metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
987
|
-
},
|
|
988
|
-
],
|
|
1072
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
989
1073
|
isError: true,
|
|
990
1074
|
};
|
|
991
1075
|
}
|
|
992
1076
|
}
|
|
993
|
-
export async function
|
|
1077
|
+
export async function handleNpmTrends(args) {
|
|
994
1078
|
try {
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1079
|
+
const packagesToProcess = args.packages || [];
|
|
1080
|
+
if (packagesToProcess.length === 0) {
|
|
1081
|
+
throw new Error('No package names provided for trends analysis.');
|
|
1082
|
+
}
|
|
1083
|
+
const period = args.period && ['last-week', 'last-month', 'last-year'].includes(args.period)
|
|
1084
|
+
? args.period
|
|
1085
|
+
: 'last-month';
|
|
1086
|
+
const periodDaysMap = {
|
|
1087
|
+
'last-week': 7,
|
|
1088
|
+
'last-month': 30,
|
|
1089
|
+
'last-year': 365,
|
|
1090
|
+
};
|
|
1091
|
+
const daysInPeriod = periodDaysMap[period];
|
|
1092
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1093
|
+
let name = '';
|
|
1094
|
+
if (typeof pkgInput === 'string') {
|
|
1095
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1096
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
|
|
1004
1097
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1098
|
+
else {
|
|
1099
|
+
return {
|
|
1100
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1101
|
+
packageName: 'unknown_package_input',
|
|
1102
|
+
status: 'error',
|
|
1103
|
+
error: 'Invalid package input type',
|
|
1104
|
+
data: null,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
if (!name) {
|
|
1108
|
+
return {
|
|
1109
|
+
packageInput: pkgInput,
|
|
1110
|
+
packageName: 'empty_package_name',
|
|
1111
|
+
status: 'error',
|
|
1112
|
+
error: 'Empty package name derived from input',
|
|
1113
|
+
data: null,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
const cacheKey = generateCacheKey('handleNpmTrends', name, period);
|
|
1117
|
+
const cachedData = cacheGet(cacheKey);
|
|
1118
|
+
if (cachedData) {
|
|
1119
|
+
return {
|
|
1120
|
+
packageInput: pkgInput,
|
|
1121
|
+
packageName: name,
|
|
1122
|
+
status: 'success_cache',
|
|
1123
|
+
error: null,
|
|
1124
|
+
data: cachedData,
|
|
1125
|
+
message: `Download trends for ${name} (${period}) from cache.`,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
const response = await fetch(`https://api.npmjs.org/downloads/point/${period}/${name}`, {
|
|
1130
|
+
headers: {
|
|
1131
|
+
Accept: 'application/json',
|
|
1132
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
if (!response.ok) {
|
|
1136
|
+
let errorMsg = `Failed to fetch download trends: ${response.status} ${response.statusText}`;
|
|
1137
|
+
if (response.status === 404) {
|
|
1138
|
+
errorMsg = `Package ${name} not found or no download data for the period.`;
|
|
1139
|
+
}
|
|
1140
|
+
return {
|
|
1141
|
+
packageInput: pkgInput,
|
|
1142
|
+
packageName: name,
|
|
1143
|
+
status: 'error',
|
|
1144
|
+
error: errorMsg,
|
|
1145
|
+
data: null,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
const data = await response.json();
|
|
1149
|
+
if (!isNpmDownloadsData(data)) {
|
|
1150
|
+
return {
|
|
1151
|
+
packageInput: pkgInput,
|
|
1152
|
+
packageName: name,
|
|
1153
|
+
status: 'error',
|
|
1154
|
+
error: 'Invalid response format from npm downloads API',
|
|
1155
|
+
data: null,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const trendData = {
|
|
1159
|
+
downloads: data.downloads,
|
|
1160
|
+
period: period,
|
|
1161
|
+
startDate: data.start,
|
|
1162
|
+
endDate: data.end,
|
|
1163
|
+
averageDailyDownloads: Math.round(data.downloads / daysInPeriod),
|
|
1164
|
+
};
|
|
1165
|
+
cacheSet(cacheKey, trendData, CACHE_TTL_MEDIUM);
|
|
1166
|
+
return {
|
|
1167
|
+
packageInput: pkgInput,
|
|
1168
|
+
packageName: name,
|
|
1169
|
+
status: 'success',
|
|
1170
|
+
error: null,
|
|
1171
|
+
data: trendData,
|
|
1172
|
+
message: `Successfully fetched download trends for ${name} (${period}).`,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
return {
|
|
1177
|
+
packageInput: pkgInput,
|
|
1178
|
+
packageName: name,
|
|
1179
|
+
status: 'error',
|
|
1180
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1181
|
+
data: null,
|
|
1182
|
+
};
|
|
1008
1183
|
}
|
|
1009
|
-
const maintenance = rawData.score.detail.maintenance;
|
|
1010
|
-
return {
|
|
1011
|
-
name: pkg,
|
|
1012
|
-
score: Math.round(maintenance * 100) / 100,
|
|
1013
|
-
};
|
|
1014
1184
|
}));
|
|
1015
|
-
let
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
text += `- Maintenance Score: ${result.score}\n\n`;
|
|
1185
|
+
let totalSuccessful = 0;
|
|
1186
|
+
let overallTotalDownloads = 0;
|
|
1187
|
+
for (const result of processedResults) {
|
|
1188
|
+
if (result.status === 'success' && result.data) {
|
|
1189
|
+
totalSuccessful++;
|
|
1190
|
+
overallTotalDownloads += result.data.downloads;
|
|
1191
|
+
}
|
|
1023
1192
|
}
|
|
1024
|
-
|
|
1193
|
+
const summary = {
|
|
1194
|
+
totalPackagesProcessed: packagesToProcess.length,
|
|
1195
|
+
totalSuccessful: totalSuccessful,
|
|
1196
|
+
totalFailed: packagesToProcess.length - totalSuccessful,
|
|
1197
|
+
overallTotalDownloads: overallTotalDownloads,
|
|
1198
|
+
overallAverageDailyDownloads: totalSuccessful > 0
|
|
1199
|
+
? Math.round(overallTotalDownloads / daysInPeriod / totalSuccessful)
|
|
1200
|
+
: 0,
|
|
1201
|
+
};
|
|
1202
|
+
const finalResponse = {
|
|
1203
|
+
query: {
|
|
1204
|
+
packagesInput: args.packages,
|
|
1205
|
+
periodUsed: period,
|
|
1206
|
+
},
|
|
1207
|
+
results: processedResults,
|
|
1208
|
+
summary: summary,
|
|
1209
|
+
};
|
|
1210
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1211
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1025
1212
|
}
|
|
1026
1213
|
catch (error) {
|
|
1214
|
+
const errorResponse = JSON.stringify({
|
|
1215
|
+
query: { packagesInput: args.packages, periodUsed: args.period || 'last-month' },
|
|
1216
|
+
results: [],
|
|
1217
|
+
summary: null,
|
|
1218
|
+
error: `General error fetching download trends: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1219
|
+
}, null, 2);
|
|
1027
1220
|
return {
|
|
1028
|
-
content: [
|
|
1029
|
-
{
|
|
1030
|
-
type: 'text',
|
|
1031
|
-
text: `Error fetching maintenance metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1032
|
-
},
|
|
1033
|
-
],
|
|
1221
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1034
1222
|
isError: true,
|
|
1035
1223
|
};
|
|
1036
1224
|
}
|
|
1037
1225
|
}
|
|
1038
|
-
export async function
|
|
1226
|
+
export async function handleNpmCompare(args) {
|
|
1039
1227
|
try {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1228
|
+
const packagesToProcess = args.packages || [];
|
|
1229
|
+
if (packagesToProcess.length === 0) {
|
|
1230
|
+
throw new Error('No package names provided for comparison.');
|
|
1231
|
+
}
|
|
1232
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1233
|
+
let name = '';
|
|
1234
|
+
let versionTag = 'latest';
|
|
1235
|
+
if (typeof pkgInput === 'string') {
|
|
1236
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1237
|
+
if (atIdx > 0) {
|
|
1238
|
+
name = pkgInput.slice(0, atIdx);
|
|
1239
|
+
versionTag = pkgInput.slice(atIdx + 1);
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
name = pkgInput;
|
|
1243
|
+
}
|
|
1049
1244
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1245
|
+
else {
|
|
1246
|
+
return {
|
|
1247
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1248
|
+
packageName: 'unknown_package_input',
|
|
1249
|
+
versionQueried: versionTag,
|
|
1250
|
+
status: 'error',
|
|
1251
|
+
error: 'Invalid package input type',
|
|
1252
|
+
data: null,
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
if (!name) {
|
|
1256
|
+
return {
|
|
1257
|
+
packageInput: pkgInput,
|
|
1258
|
+
packageName: 'empty_package_name',
|
|
1259
|
+
versionQueried: versionTag,
|
|
1260
|
+
status: 'error',
|
|
1261
|
+
error: 'Empty package name derived from input',
|
|
1262
|
+
data: null,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
const cacheKey = generateCacheKey('handleNpmCompare', name, versionTag);
|
|
1266
|
+
const cachedData = cacheGet(cacheKey);
|
|
1267
|
+
if (cachedData) {
|
|
1268
|
+
return {
|
|
1269
|
+
packageInput: pkgInput,
|
|
1270
|
+
packageName: name, // Or cachedData.name if preferred
|
|
1271
|
+
versionQueried: versionTag,
|
|
1272
|
+
status: 'success_cache',
|
|
1273
|
+
error: null,
|
|
1274
|
+
data: cachedData,
|
|
1275
|
+
message: `Comparison data for ${name}@${versionTag} from cache.`,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
// Fetch package version details from registry
|
|
1280
|
+
const pkgResponse = await fetch(`https://registry.npmjs.org/${name}/${versionTag}`);
|
|
1281
|
+
if (!pkgResponse.ok) {
|
|
1282
|
+
throw new Error(`Failed to fetch package info for ${name}@${versionTag}: ${pkgResponse.status} ${pkgResponse.statusText}`);
|
|
1283
|
+
}
|
|
1284
|
+
const pkgData = await pkgResponse.json();
|
|
1285
|
+
if (!isNpmPackageVersionData(pkgData)) {
|
|
1286
|
+
throw new Error(`Invalid package data format for ${name}@${versionTag}`);
|
|
1287
|
+
}
|
|
1288
|
+
// Fetch monthly downloads
|
|
1289
|
+
let monthlyDownloads = null;
|
|
1290
|
+
try {
|
|
1291
|
+
const downloadsResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${name}`);
|
|
1292
|
+
if (downloadsResponse.ok) {
|
|
1293
|
+
const downloadsData = await downloadsResponse.json();
|
|
1294
|
+
if (isNpmDownloadsData(downloadsData)) {
|
|
1295
|
+
monthlyDownloads = downloadsData.downloads;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
catch (dlError) {
|
|
1300
|
+
console.debug(`Could not fetch downloads for ${name}: ${dlError}`);
|
|
1301
|
+
}
|
|
1302
|
+
// Fetch publish date for this specific version
|
|
1303
|
+
// Need to fetch the full package info to get to the 'time' field for specific version
|
|
1304
|
+
let publishDate = null;
|
|
1305
|
+
try {
|
|
1306
|
+
const fullPkgInfoResponse = await fetch(`https://registry.npmjs.org/${name}`);
|
|
1307
|
+
if (fullPkgInfoResponse.ok) {
|
|
1308
|
+
const fullPkgInfo = await fullPkgInfoResponse.json();
|
|
1309
|
+
if (isNpmPackageInfo(fullPkgInfo) && fullPkgInfo.time) {
|
|
1310
|
+
publishDate = fullPkgInfo.time[pkgData.version] || null;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
catch (timeError) {
|
|
1315
|
+
console.debug(`Could not fetch time info for ${name}: ${timeError}`);
|
|
1316
|
+
}
|
|
1317
|
+
const comparisonData = {
|
|
1318
|
+
name: pkgData.name,
|
|
1319
|
+
version: pkgData.version,
|
|
1320
|
+
description: pkgData.description || null,
|
|
1321
|
+
license: pkgData.license || null,
|
|
1322
|
+
dependenciesCount: Object.keys(pkgData.dependencies || {}).length,
|
|
1323
|
+
devDependenciesCount: Object.keys(pkgData.devDependencies || {}).length,
|
|
1324
|
+
peerDependenciesCount: Object.keys(pkgData.peerDependencies || {}).length,
|
|
1325
|
+
monthlyDownloads: monthlyDownloads,
|
|
1326
|
+
publishDate: publishDate,
|
|
1327
|
+
repositoryUrl: pkgData.repository?.url || null,
|
|
1328
|
+
};
|
|
1329
|
+
cacheSet(cacheKey, comparisonData, CACHE_TTL_MEDIUM);
|
|
1330
|
+
return {
|
|
1331
|
+
packageInput: pkgInput,
|
|
1332
|
+
packageName: name, // or comparisonData.name
|
|
1333
|
+
versionQueried: versionTag,
|
|
1334
|
+
status: 'success',
|
|
1335
|
+
error: null,
|
|
1336
|
+
data: comparisonData,
|
|
1337
|
+
message: `Successfully fetched comparison data for ${name}@${pkgData.version}.`,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
return {
|
|
1342
|
+
packageInput: pkgInput,
|
|
1343
|
+
packageName: name,
|
|
1344
|
+
versionQueried: versionTag,
|
|
1345
|
+
status: 'error',
|
|
1346
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1347
|
+
data: null,
|
|
1348
|
+
};
|
|
1053
1349
|
}
|
|
1054
|
-
const popularityScore = data.score.detail.popularity;
|
|
1055
|
-
return {
|
|
1056
|
-
name: pkg,
|
|
1057
|
-
...NpmPopularitySchema.parse({
|
|
1058
|
-
score: Math.round(popularityScore * 100) / 100,
|
|
1059
|
-
stars: 0,
|
|
1060
|
-
downloads: 0,
|
|
1061
|
-
dependents: 0,
|
|
1062
|
-
communityInterest: 0,
|
|
1063
|
-
}),
|
|
1064
|
-
};
|
|
1065
1350
|
}));
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
text += `- Overall Score: ${result.score}\n`;
|
|
1074
|
-
text += '- Note: Detailed metrics are no longer provided by the API\n\n';
|
|
1075
|
-
}
|
|
1076
|
-
return { content: [{ type: 'text', text }], isError: false };
|
|
1351
|
+
const finalResponse = {
|
|
1352
|
+
queryPackages: args.packages,
|
|
1353
|
+
results: processedResults,
|
|
1354
|
+
message: `Comparison data for ${args.packages.length} package(s).`,
|
|
1355
|
+
};
|
|
1356
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1357
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1077
1358
|
}
|
|
1078
1359
|
catch (error) {
|
|
1360
|
+
const errorResponse = JSON.stringify({
|
|
1361
|
+
queryPackages: args.packages,
|
|
1362
|
+
results: [],
|
|
1363
|
+
error: `General error comparing packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1364
|
+
}, null, 2);
|
|
1079
1365
|
return {
|
|
1080
|
-
content: [
|
|
1081
|
-
{
|
|
1082
|
-
type: 'text',
|
|
1083
|
-
text: `Error fetching popularity metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1084
|
-
},
|
|
1085
|
-
],
|
|
1366
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1086
1367
|
isError: true,
|
|
1087
1368
|
};
|
|
1088
1369
|
}
|
|
1089
1370
|
}
|
|
1090
|
-
|
|
1371
|
+
// Function to get package quality metrics
|
|
1372
|
+
export async function handleNpmQuality(args) {
|
|
1091
1373
|
try {
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1374
|
+
const packagesToProcess = args.packages || [];
|
|
1375
|
+
if (packagesToProcess.length === 0) {
|
|
1376
|
+
throw new Error('No package names provided to fetch quality metrics.');
|
|
1377
|
+
}
|
|
1378
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1379
|
+
let name = '';
|
|
1380
|
+
if (typeof pkgInput === 'string') {
|
|
1381
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1382
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version is ignored by npms.io API endpoint for the main query
|
|
1383
|
+
}
|
|
1384
|
+
else {
|
|
1095
1385
|
return {
|
|
1096
|
-
|
|
1097
|
-
|
|
1386
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1387
|
+
packageName: 'unknown_package_input',
|
|
1388
|
+
status: 'error',
|
|
1389
|
+
error: 'Invalid package input type',
|
|
1390
|
+
data: null,
|
|
1391
|
+
message: 'Package input was not a string.',
|
|
1098
1392
|
};
|
|
1099
1393
|
}
|
|
1100
|
-
if (!
|
|
1101
|
-
|
|
1394
|
+
if (!name) {
|
|
1395
|
+
return {
|
|
1396
|
+
packageInput: pkgInput,
|
|
1397
|
+
packageName: 'empty_package_name',
|
|
1398
|
+
status: 'error',
|
|
1399
|
+
error: 'Empty package name derived from input',
|
|
1400
|
+
data: null,
|
|
1401
|
+
message: 'Package name could not be determined from input.',
|
|
1402
|
+
};
|
|
1102
1403
|
}
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1404
|
+
const cacheKey = generateCacheKey('handleNpmQuality', name);
|
|
1405
|
+
const cachedData = cacheGet(cacheKey);
|
|
1406
|
+
if (cachedData) {
|
|
1407
|
+
return {
|
|
1408
|
+
packageInput: pkgInput,
|
|
1409
|
+
packageName: name, // Or cachedData.packageName if stored differently
|
|
1410
|
+
status: 'success_cache',
|
|
1411
|
+
error: null,
|
|
1412
|
+
data: cachedData,
|
|
1413
|
+
message: `Quality score for ${name} (version analyzed: ${cachedData.versionInScore}) from cache.`,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
try {
|
|
1417
|
+
const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`, {
|
|
1418
|
+
headers: {
|
|
1419
|
+
Accept: 'application/json',
|
|
1420
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1423
|
+
if (!response.ok) {
|
|
1424
|
+
let errorMsg = `Failed to fetch quality data: ${response.status} ${response.statusText}`;
|
|
1425
|
+
if (response.status === 404) {
|
|
1426
|
+
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
packageInput: pkgInput,
|
|
1430
|
+
packageName: name,
|
|
1431
|
+
status: 'error',
|
|
1432
|
+
error: errorMsg,
|
|
1433
|
+
data: null,
|
|
1434
|
+
message: `Could not retrieve quality information for ${name}.`,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
const rawData = await response.json();
|
|
1438
|
+
if (!isValidNpmsResponse(rawData)) {
|
|
1439
|
+
return {
|
|
1440
|
+
packageInput: pkgInput,
|
|
1441
|
+
packageName: name,
|
|
1442
|
+
status: 'error',
|
|
1443
|
+
error: 'Invalid or incomplete response from npms.io API for quality data',
|
|
1444
|
+
data: null,
|
|
1445
|
+
message: `Received malformed quality data for ${name}.`,
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
const { score, collected, analyzedAt } = rawData;
|
|
1449
|
+
const qualityScore = score.detail.quality;
|
|
1450
|
+
const qualityData = {
|
|
1451
|
+
analyzedAt: analyzedAt,
|
|
1452
|
+
versionInScore: collected.metadata.version,
|
|
1453
|
+
qualityScore: qualityScore,
|
|
1454
|
+
// Detailed sub-metrics like tests, coverage, linting, types are no longer directly provided
|
|
1455
|
+
// by the npms.io v2 API in the same way. The overall quality score is the primary metric.
|
|
1456
|
+
};
|
|
1457
|
+
const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
|
|
1458
|
+
? CACHE_TTL_SHORT
|
|
1459
|
+
: CACHE_TTL_LONG;
|
|
1460
|
+
cacheSet(cacheKey, qualityData, ttl);
|
|
1461
|
+
return {
|
|
1462
|
+
packageInput: pkgInput,
|
|
1463
|
+
packageName: name,
|
|
1464
|
+
status: 'success',
|
|
1465
|
+
error: null,
|
|
1466
|
+
data: qualityData,
|
|
1467
|
+
message: `Successfully fetched quality score for ${name} (version analyzed: ${collected.metadata.version}).`,
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
return {
|
|
1472
|
+
packageInput: pkgInput,
|
|
1473
|
+
packageName: name,
|
|
1474
|
+
status: 'error',
|
|
1475
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1476
|
+
data: null,
|
|
1477
|
+
message: `An unexpected error occurred while processing quality for ${name}.`,
|
|
1478
|
+
};
|
|
1106
1479
|
}
|
|
1107
|
-
return {
|
|
1108
|
-
name: pkg,
|
|
1109
|
-
maintainers: data.maintainers || [],
|
|
1110
|
-
};
|
|
1111
1480
|
}));
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1481
|
+
const finalResponse = {
|
|
1482
|
+
queryPackages: args.packages,
|
|
1483
|
+
results: processedResults,
|
|
1484
|
+
};
|
|
1485
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1486
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1487
|
+
}
|
|
1488
|
+
catch (error) {
|
|
1489
|
+
const errorResponse = JSON.stringify({
|
|
1490
|
+
queryPackages: args.packages,
|
|
1491
|
+
results: [],
|
|
1492
|
+
error: `General error fetching quality metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1493
|
+
}, null, 2);
|
|
1494
|
+
return {
|
|
1495
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1496
|
+
isError: true,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
export async function handleNpmMaintenance(args) {
|
|
1501
|
+
try {
|
|
1502
|
+
const packagesToProcess = args.packages || [];
|
|
1503
|
+
if (packagesToProcess.length === 0) {
|
|
1504
|
+
throw new Error('No package names provided to fetch maintenance metrics.');
|
|
1505
|
+
}
|
|
1506
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1507
|
+
let name = '';
|
|
1508
|
+
if (typeof pkgInput === 'string') {
|
|
1509
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1510
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
|
|
1123
1511
|
}
|
|
1124
1512
|
else {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1513
|
+
return {
|
|
1514
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1515
|
+
packageName: 'unknown_package_input',
|
|
1516
|
+
status: 'error',
|
|
1517
|
+
error: 'Invalid package input type',
|
|
1518
|
+
data: null,
|
|
1519
|
+
message: 'Package input was not a string.',
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
if (!name) {
|
|
1523
|
+
return {
|
|
1524
|
+
packageInput: pkgInput,
|
|
1525
|
+
packageName: 'empty_package_name',
|
|
1526
|
+
status: 'error',
|
|
1527
|
+
error: 'Empty package name derived from input',
|
|
1528
|
+
data: null,
|
|
1529
|
+
message: 'Package name could not be determined from input.',
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
const cacheKey = generateCacheKey('handleNpmMaintenance', name);
|
|
1533
|
+
const cachedData = cacheGet(cacheKey);
|
|
1534
|
+
if (cachedData) {
|
|
1535
|
+
return {
|
|
1536
|
+
packageInput: pkgInput,
|
|
1537
|
+
packageName: name,
|
|
1538
|
+
status: 'success_cache',
|
|
1539
|
+
error: null,
|
|
1540
|
+
data: cachedData,
|
|
1541
|
+
message: `Maintenance score for ${name} (version analyzed: ${cachedData.versionInScore}) from cache.`,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`, {
|
|
1546
|
+
headers: {
|
|
1547
|
+
Accept: 'application/json',
|
|
1548
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1549
|
+
},
|
|
1550
|
+
});
|
|
1551
|
+
if (!response.ok) {
|
|
1552
|
+
let errorMsg = `Failed to fetch maintenance data: ${response.status} ${response.statusText}`;
|
|
1553
|
+
if (response.status === 404) {
|
|
1554
|
+
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1555
|
+
}
|
|
1556
|
+
return {
|
|
1557
|
+
packageInput: pkgInput,
|
|
1558
|
+
packageName: name,
|
|
1559
|
+
status: 'error',
|
|
1560
|
+
error: errorMsg,
|
|
1561
|
+
data: null,
|
|
1562
|
+
message: `Could not retrieve maintenance information for ${name}.`,
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
const rawData = await response.json();
|
|
1566
|
+
if (!isValidNpmsResponse(rawData)) {
|
|
1567
|
+
return {
|
|
1568
|
+
packageInput: pkgInput,
|
|
1569
|
+
packageName: name,
|
|
1570
|
+
status: 'error',
|
|
1571
|
+
error: 'Invalid or incomplete response from npms.io API for maintenance data',
|
|
1572
|
+
data: null,
|
|
1573
|
+
message: `Received malformed maintenance data for ${name}.`,
|
|
1574
|
+
};
|
|
1129
1575
|
}
|
|
1576
|
+
const { score, collected, analyzedAt } = rawData;
|
|
1577
|
+
const maintenanceScoreValue = score.detail.maintenance;
|
|
1578
|
+
const maintenanceData = {
|
|
1579
|
+
analyzedAt: analyzedAt,
|
|
1580
|
+
versionInScore: collected.metadata.version,
|
|
1581
|
+
maintenanceScore: maintenanceScoreValue,
|
|
1582
|
+
};
|
|
1583
|
+
const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
|
|
1584
|
+
? CACHE_TTL_SHORT
|
|
1585
|
+
: CACHE_TTL_LONG;
|
|
1586
|
+
cacheSet(cacheKey, maintenanceData, ttl);
|
|
1587
|
+
return {
|
|
1588
|
+
packageInput: pkgInput,
|
|
1589
|
+
packageName: name,
|
|
1590
|
+
status: 'success',
|
|
1591
|
+
error: null,
|
|
1592
|
+
data: maintenanceData,
|
|
1593
|
+
message: `Successfully fetched maintenance score for ${name} (version analyzed: ${collected.metadata.version}).`,
|
|
1594
|
+
};
|
|
1130
1595
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1596
|
+
catch (error) {
|
|
1597
|
+
return {
|
|
1598
|
+
packageInput: pkgInput,
|
|
1599
|
+
packageName: name,
|
|
1600
|
+
status: 'error',
|
|
1601
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1602
|
+
data: null,
|
|
1603
|
+
message: `An unexpected error occurred while processing maintenance for ${name}.`,
|
|
1604
|
+
};
|
|
1133
1605
|
}
|
|
1134
|
-
}
|
|
1606
|
+
}));
|
|
1607
|
+
const finalResponse = {
|
|
1608
|
+
queryPackages: args.packages,
|
|
1609
|
+
results: processedResults,
|
|
1610
|
+
};
|
|
1611
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1612
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1613
|
+
}
|
|
1614
|
+
catch (error) {
|
|
1615
|
+
const errorResponse = JSON.stringify({
|
|
1616
|
+
queryPackages: args.packages,
|
|
1617
|
+
results: [],
|
|
1618
|
+
error: `General error fetching maintenance metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1619
|
+
}, null, 2);
|
|
1135
1620
|
return {
|
|
1136
|
-
content: [
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1621
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1622
|
+
isError: true,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
export async function handleNpmMaintainers(args) {
|
|
1627
|
+
try {
|
|
1628
|
+
const packagesToProcess = args.packages || [];
|
|
1629
|
+
if (packagesToProcess.length === 0) {
|
|
1630
|
+
throw new Error('No package names provided to fetch maintainers.');
|
|
1631
|
+
}
|
|
1632
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1633
|
+
let name = '';
|
|
1634
|
+
if (typeof pkgInput === 'string') {
|
|
1635
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1636
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version is ignored for maintainers
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
return {
|
|
1640
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1641
|
+
packageName: 'unknown_package_input',
|
|
1642
|
+
status: 'error',
|
|
1643
|
+
error: 'Invalid package input type',
|
|
1644
|
+
data: null,
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
if (!name) {
|
|
1648
|
+
return {
|
|
1649
|
+
packageInput: pkgInput,
|
|
1650
|
+
packageName: 'empty_package_name',
|
|
1651
|
+
status: 'error',
|
|
1652
|
+
error: 'Empty package name derived from input',
|
|
1653
|
+
data: null,
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
const cacheKey = generateCacheKey('handleNpmMaintainers', name);
|
|
1657
|
+
const cachedData = cacheGet(cacheKey);
|
|
1658
|
+
if (cachedData) {
|
|
1659
|
+
return {
|
|
1660
|
+
packageInput: pkgInput,
|
|
1661
|
+
packageName: name,
|
|
1662
|
+
status: 'success_cache',
|
|
1663
|
+
error: null,
|
|
1664
|
+
data: cachedData,
|
|
1665
|
+
message: `Maintainer information for ${name} from cache.`,
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
try {
|
|
1669
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`);
|
|
1670
|
+
if (!response.ok) {
|
|
1671
|
+
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
1672
|
+
if (response.status === 404) {
|
|
1673
|
+
errorMsg = `Package ${name} not found in the npm registry.`;
|
|
1674
|
+
}
|
|
1675
|
+
return {
|
|
1676
|
+
packageInput: pkgInput,
|
|
1677
|
+
packageName: name,
|
|
1678
|
+
status: 'error',
|
|
1679
|
+
error: errorMsg,
|
|
1680
|
+
data: null,
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const data = await response.json();
|
|
1684
|
+
if (!isNpmPackageInfo(data)) {
|
|
1685
|
+
// Using NpmPackageInfoSchema as it contains maintainers
|
|
1686
|
+
return {
|
|
1687
|
+
packageInput: pkgInput,
|
|
1688
|
+
packageName: name,
|
|
1689
|
+
status: 'error',
|
|
1690
|
+
error: 'Invalid package info data received from registry',
|
|
1691
|
+
data: null,
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
const maintainers = (data.maintainers || []).map((m) => ({
|
|
1695
|
+
name: m.name,
|
|
1696
|
+
email: m.email || null, // Ensure email is null if not present
|
|
1697
|
+
url: m.url || null, // NpmMaintainerSchema has url optional
|
|
1698
|
+
}));
|
|
1699
|
+
const maintainersData = {
|
|
1700
|
+
maintainers: maintainers,
|
|
1701
|
+
maintainersCount: maintainers.length,
|
|
1702
|
+
};
|
|
1703
|
+
cacheSet(cacheKey, maintainersData, CACHE_TTL_VERY_LONG);
|
|
1704
|
+
return {
|
|
1705
|
+
packageInput: pkgInput,
|
|
1706
|
+
packageName: name,
|
|
1707
|
+
status: 'success',
|
|
1708
|
+
error: null,
|
|
1709
|
+
data: maintainersData,
|
|
1710
|
+
message: `Successfully fetched maintainer information for ${name}.`,
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
catch (error) {
|
|
1714
|
+
return {
|
|
1715
|
+
packageInput: pkgInput,
|
|
1716
|
+
packageName: name,
|
|
1717
|
+
status: 'error',
|
|
1718
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1719
|
+
data: null,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
}));
|
|
1723
|
+
const finalResponse = {
|
|
1724
|
+
queryPackages: args.packages,
|
|
1725
|
+
results: processedResults,
|
|
1726
|
+
message: `Maintainer information for ${args.packages.length} package(s).`,
|
|
1143
1727
|
};
|
|
1728
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1729
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1144
1730
|
}
|
|
1145
1731
|
catch (error) {
|
|
1732
|
+
const errorResponse = JSON.stringify({
|
|
1733
|
+
queryPackages: args.packages,
|
|
1734
|
+
results: [],
|
|
1735
|
+
error: `General error fetching maintainer information: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1736
|
+
}, null, 2);
|
|
1146
1737
|
return {
|
|
1147
|
-
content: [
|
|
1148
|
-
{
|
|
1149
|
-
type: 'text',
|
|
1150
|
-
text: `Error fetching package maintainers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1151
|
-
},
|
|
1152
|
-
],
|
|
1738
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1153
1739
|
isError: true,
|
|
1154
1740
|
};
|
|
1155
1741
|
}
|
|
1156
1742
|
}
|
|
1157
1743
|
export async function handleNpmScore(args) {
|
|
1158
1744
|
try {
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1745
|
+
const packagesToProcess = args.packages || [];
|
|
1746
|
+
if (packagesToProcess.length === 0) {
|
|
1747
|
+
throw new Error('No package names provided to fetch scores.');
|
|
1748
|
+
}
|
|
1749
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1750
|
+
let name = '';
|
|
1751
|
+
if (typeof pkgInput === 'string') {
|
|
1752
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1753
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version is ignored by npms.io API endpoint
|
|
1754
|
+
}
|
|
1755
|
+
else {
|
|
1756
|
+
return {
|
|
1757
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1758
|
+
packageName: 'unknown_package_input',
|
|
1759
|
+
status: 'error',
|
|
1760
|
+
error: 'Invalid package input type',
|
|
1761
|
+
data: null,
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
if (!name) {
|
|
1162
1765
|
return {
|
|
1163
|
-
|
|
1164
|
-
|
|
1766
|
+
packageInput: pkgInput,
|
|
1767
|
+
packageName: 'empty_package_name',
|
|
1768
|
+
status: 'error',
|
|
1769
|
+
error: 'Empty package name derived from input',
|
|
1770
|
+
data: null,
|
|
1165
1771
|
};
|
|
1166
1772
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1773
|
+
const cacheKey = generateCacheKey('handleNpmScore', name);
|
|
1774
|
+
const cachedData = cacheGet(cacheKey);
|
|
1775
|
+
if (cachedData) {
|
|
1776
|
+
return {
|
|
1777
|
+
packageInput: pkgInput,
|
|
1778
|
+
packageName: name,
|
|
1779
|
+
status: 'success_cache',
|
|
1780
|
+
error: null,
|
|
1781
|
+
data: cachedData,
|
|
1782
|
+
message: `Score data for ${name} (version analyzed: ${cachedData.versionInScore}) from cache.`,
|
|
1783
|
+
};
|
|
1169
1784
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1785
|
+
try {
|
|
1786
|
+
const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`);
|
|
1787
|
+
if (!response.ok) {
|
|
1788
|
+
let errorMsg = `Failed to fetch package score: ${response.status} ${response.statusText}`;
|
|
1789
|
+
if (response.status === 404) {
|
|
1790
|
+
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1791
|
+
}
|
|
1792
|
+
return {
|
|
1793
|
+
packageInput: pkgInput,
|
|
1794
|
+
packageName: name,
|
|
1795
|
+
status: 'error',
|
|
1796
|
+
error: errorMsg,
|
|
1797
|
+
data: null,
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
const rawData = await response.json();
|
|
1801
|
+
if (!isValidNpmsResponse(rawData)) {
|
|
1802
|
+
return {
|
|
1803
|
+
packageInput: pkgInput,
|
|
1804
|
+
packageName: name,
|
|
1805
|
+
status: 'error',
|
|
1806
|
+
error: 'Invalid or incomplete response from npms.io API',
|
|
1807
|
+
data: null,
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
const { score, collected, analyzedAt } = rawData;
|
|
1811
|
+
const { detail } = score;
|
|
1812
|
+
// Calculate total downloads for the last month from the typically first entry in downloads array
|
|
1813
|
+
const lastMonthDownloads = collected.npm?.downloads?.find((d) => {
|
|
1814
|
+
// Heuristic: find a download period that is roughly 30 days
|
|
1815
|
+
const from = new Date(d.from);
|
|
1816
|
+
const to = new Date(d.to);
|
|
1817
|
+
const diffTime = Math.abs(to.getTime() - from.getTime());
|
|
1818
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
1819
|
+
return diffDays >= 28 && diffDays <= 31; // Common range for monthly data
|
|
1820
|
+
})?.count ||
|
|
1821
|
+
collected.npm?.downloads?.[0]?.count ||
|
|
1822
|
+
0;
|
|
1823
|
+
const scoreData = {
|
|
1824
|
+
analyzedAt: analyzedAt,
|
|
1825
|
+
versionInScore: collected.metadata.version,
|
|
1826
|
+
score: {
|
|
1827
|
+
final: score.final,
|
|
1828
|
+
detail: {
|
|
1829
|
+
quality: detail.quality,
|
|
1830
|
+
popularity: detail.popularity,
|
|
1831
|
+
maintenance: detail.maintenance,
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
packageInfoFromScore: {
|
|
1835
|
+
name: collected.metadata.name,
|
|
1836
|
+
version: collected.metadata.version,
|
|
1837
|
+
description: collected.metadata.description || null,
|
|
1838
|
+
},
|
|
1839
|
+
npmStats: {
|
|
1840
|
+
downloadsLastMonth: lastMonthDownloads,
|
|
1841
|
+
starsCount: collected.npm.starsCount,
|
|
1842
|
+
},
|
|
1843
|
+
githubStats: collected.github
|
|
1844
|
+
? {
|
|
1845
|
+
starsCount: collected.github.starsCount,
|
|
1846
|
+
forksCount: collected.github.forksCount,
|
|
1847
|
+
subscribersCount: collected.github.subscribersCount,
|
|
1848
|
+
issues: {
|
|
1849
|
+
count: collected.github.issues.count,
|
|
1850
|
+
openCount: collected.github.issues.openCount,
|
|
1851
|
+
},
|
|
1852
|
+
}
|
|
1853
|
+
: null,
|
|
1854
|
+
};
|
|
1855
|
+
const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
|
|
1856
|
+
? CACHE_TTL_SHORT
|
|
1857
|
+
: CACHE_TTL_LONG;
|
|
1858
|
+
cacheSet(cacheKey, scoreData, ttl);
|
|
1172
1859
|
return {
|
|
1173
|
-
|
|
1174
|
-
|
|
1860
|
+
packageInput: pkgInput,
|
|
1861
|
+
packageName: name,
|
|
1862
|
+
status: 'success',
|
|
1863
|
+
error: null,
|
|
1864
|
+
data: scoreData,
|
|
1865
|
+
message: `Successfully fetched score data for ${name} (version analyzed: ${collected.metadata.version}).`,
|
|
1175
1866
|
};
|
|
1176
1867
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}));
|
|
1186
|
-
let text = '📊 Package Scores\n\n';
|
|
1187
|
-
for (const result of results) {
|
|
1188
|
-
if ('error' in result) {
|
|
1189
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
1190
|
-
continue;
|
|
1191
|
-
}
|
|
1192
|
-
text += `📦 ${result.name}\n`;
|
|
1193
|
-
text += `${'-'.repeat(40)}\n`;
|
|
1194
|
-
text += `Overall Score: ${(result.score.final * 100).toFixed(1)}%\n\n`;
|
|
1195
|
-
text += '🎯 Quality Breakdown:\n';
|
|
1196
|
-
text += `• Quality: ${(result.detail.quality * 100).toFixed(1)}%\n`;
|
|
1197
|
-
text += `• Maintenance: ${(result.detail.maintenance * 100).toFixed(1)}%\n`;
|
|
1198
|
-
text += `• Popularity: ${(result.detail.popularity * 100).toFixed(1)}%\n\n`;
|
|
1199
|
-
if (result.collected.github) {
|
|
1200
|
-
text += '📈 GitHub Stats:\n';
|
|
1201
|
-
text += `• Stars: ${result.collected.github.starsCount.toLocaleString()}\n`;
|
|
1202
|
-
text += `• Forks: ${result.collected.github.forksCount.toLocaleString()}\n`;
|
|
1203
|
-
text += `• Watchers: ${result.collected.github.subscribersCount.toLocaleString()}\n`;
|
|
1204
|
-
text += `• Total Issues: ${result.collected.github.issues.count.toLocaleString()}\n`;
|
|
1205
|
-
text += `• Open Issues: ${result.collected.github.issues.openCount.toLocaleString()}\n\n`;
|
|
1206
|
-
}
|
|
1207
|
-
if (result.collected.npm?.downloads?.length > 0) {
|
|
1208
|
-
const lastDownloads = result.collected.npm.downloads[0];
|
|
1209
|
-
text += '📥 NPM Downloads:\n';
|
|
1210
|
-
text += `• Last day: ${lastDownloads.count.toLocaleString()} (${new Date(lastDownloads.from).toLocaleDateString()} - ${new Date(lastDownloads.to).toLocaleDateString()})\n\n`;
|
|
1211
|
-
}
|
|
1212
|
-
if (results.indexOf(result) < results.length - 1) {
|
|
1213
|
-
text += '\n';
|
|
1868
|
+
catch (error) {
|
|
1869
|
+
return {
|
|
1870
|
+
packageInput: pkgInput,
|
|
1871
|
+
packageName: name,
|
|
1872
|
+
status: 'error',
|
|
1873
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1874
|
+
data: null,
|
|
1875
|
+
};
|
|
1214
1876
|
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
type: 'text',
|
|
1221
|
-
text,
|
|
1222
|
-
},
|
|
1223
|
-
],
|
|
1224
|
-
isError: false,
|
|
1877
|
+
}));
|
|
1878
|
+
const finalResponse = {
|
|
1879
|
+
queryPackages: args.packages,
|
|
1880
|
+
results: processedResults,
|
|
1881
|
+
message: `Score information for ${args.packages.length} package(s).`,
|
|
1225
1882
|
};
|
|
1883
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1884
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1226
1885
|
}
|
|
1227
1886
|
catch (error) {
|
|
1228
|
-
|
|
1887
|
+
const errorResponse = JSON.stringify({
|
|
1888
|
+
queryPackages: args.packages,
|
|
1889
|
+
results: [],
|
|
1890
|
+
error: `General error fetching package scores: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1891
|
+
}, null, 2);
|
|
1229
1892
|
return {
|
|
1230
|
-
content: [
|
|
1231
|
-
{
|
|
1232
|
-
type: 'text',
|
|
1233
|
-
text: `Error fetching package scores: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1234
|
-
},
|
|
1235
|
-
],
|
|
1893
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1236
1894
|
isError: true,
|
|
1237
1895
|
};
|
|
1238
1896
|
}
|
|
1239
1897
|
}
|
|
1240
1898
|
export async function handleNpmPackageReadme(args) {
|
|
1241
1899
|
try {
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1900
|
+
const packagesToProcess = args.packages || [];
|
|
1901
|
+
if (packagesToProcess.length === 0) {
|
|
1902
|
+
throw new Error('No package names provided to fetch READMEs.');
|
|
1903
|
+
}
|
|
1904
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1905
|
+
let name = '';
|
|
1906
|
+
let versionTag = undefined; // Explicitly undefined if not specified
|
|
1907
|
+
if (typeof pkgInput === 'string') {
|
|
1908
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1909
|
+
if (atIdx > 0) {
|
|
1910
|
+
name = pkgInput.slice(0, atIdx);
|
|
1911
|
+
versionTag = pkgInput.slice(atIdx + 1);
|
|
1912
|
+
}
|
|
1913
|
+
else {
|
|
1914
|
+
name = pkgInput;
|
|
1915
|
+
versionTag = 'latest'; // Default to latest if no version specified
|
|
1916
|
+
}
|
|
1246
1917
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1918
|
+
else {
|
|
1919
|
+
return {
|
|
1920
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1921
|
+
packageName: 'unknown_package_input',
|
|
1922
|
+
versionQueried: versionTag,
|
|
1923
|
+
versionFetched: null,
|
|
1924
|
+
status: 'error',
|
|
1925
|
+
error: 'Invalid package input type',
|
|
1926
|
+
data: null,
|
|
1927
|
+
};
|
|
1250
1928
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1929
|
+
if (!name) {
|
|
1930
|
+
return {
|
|
1931
|
+
packageInput: pkgInput,
|
|
1932
|
+
packageName: 'empty_package_name',
|
|
1933
|
+
versionQueried: versionTag,
|
|
1934
|
+
versionFetched: null,
|
|
1935
|
+
status: 'error',
|
|
1936
|
+
error: 'Empty package name derived from input',
|
|
1937
|
+
data: null,
|
|
1938
|
+
};
|
|
1254
1939
|
}
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1940
|
+
const cacheKey = generateCacheKey('handleNpmPackageReadme', name, versionTag);
|
|
1941
|
+
const cachedData = cacheGet(cacheKey);
|
|
1942
|
+
if (cachedData) {
|
|
1943
|
+
return {
|
|
1944
|
+
packageInput: pkgInput,
|
|
1945
|
+
packageName: name,
|
|
1946
|
+
versionQueried: versionTag,
|
|
1947
|
+
versionFetched: cachedData.versionFetched, // Retrieve stored fetched version
|
|
1948
|
+
status: 'success_cache',
|
|
1949
|
+
error: null,
|
|
1950
|
+
data: { readme: cachedData.readme, hasReadme: cachedData.hasReadme },
|
|
1951
|
+
message: `README for ${name}@${cachedData.versionFetched} from cache.`,
|
|
1952
|
+
};
|
|
1258
1953
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1954
|
+
try {
|
|
1955
|
+
const response = await fetch(`https://registry.npmjs.org/${name}`);
|
|
1956
|
+
if (!response.ok) {
|
|
1957
|
+
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
1958
|
+
if (response.status === 404) {
|
|
1959
|
+
errorMsg = `Package ${name} not found.`;
|
|
1960
|
+
}
|
|
1961
|
+
return {
|
|
1962
|
+
packageInput: pkgInput,
|
|
1963
|
+
packageName: name,
|
|
1964
|
+
versionQueried: versionTag,
|
|
1965
|
+
versionFetched: null,
|
|
1966
|
+
status: 'error',
|
|
1967
|
+
error: errorMsg,
|
|
1968
|
+
data: null,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
const packageInfo = await response.json();
|
|
1972
|
+
if (!isNpmPackageInfo(packageInfo)) {
|
|
1973
|
+
return {
|
|
1974
|
+
packageInput: pkgInput,
|
|
1975
|
+
packageName: name,
|
|
1976
|
+
versionQueried: versionTag,
|
|
1977
|
+
versionFetched: null,
|
|
1978
|
+
status: 'error',
|
|
1979
|
+
error: 'Invalid package info data received',
|
|
1980
|
+
data: null,
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
const versionToUse = versionTag === 'latest' ? packageInfo['dist-tags']?.latest : versionTag;
|
|
1984
|
+
if (!versionToUse || !packageInfo.versions || !packageInfo.versions[versionToUse]) {
|
|
1985
|
+
return {
|
|
1986
|
+
packageInput: pkgInput,
|
|
1987
|
+
packageName: name,
|
|
1988
|
+
versionQueried: versionTag,
|
|
1989
|
+
versionFetched: versionToUse || null,
|
|
1990
|
+
status: 'error',
|
|
1991
|
+
error: `Version ${versionToUse || 'requested'} not found or no version data available.`,
|
|
1992
|
+
data: null,
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
const versionData = packageInfo.versions[versionToUse];
|
|
1996
|
+
// README can be in version-specific data or at the root of packageInfo
|
|
1997
|
+
const readmeContent = versionData.readme || packageInfo.readme || null;
|
|
1998
|
+
const hasReadme = !!readmeContent;
|
|
1999
|
+
const readmeResultData = {
|
|
2000
|
+
readme: readmeContent,
|
|
2001
|
+
hasReadme: hasReadme,
|
|
2002
|
+
versionFetched: versionToUse, // Store the actually fetched version
|
|
2003
|
+
};
|
|
2004
|
+
cacheSet(cacheKey, readmeResultData, CACHE_TTL_LONG);
|
|
2005
|
+
return {
|
|
2006
|
+
packageInput: pkgInput,
|
|
2007
|
+
packageName: name,
|
|
2008
|
+
versionQueried: versionTag,
|
|
2009
|
+
versionFetched: versionToUse,
|
|
2010
|
+
status: 'success',
|
|
2011
|
+
error: null,
|
|
2012
|
+
data: { readme: readmeContent, hasReadme: hasReadme }, // Return only readme and hasReadme in data field for consistency
|
|
2013
|
+
message: `Successfully fetched README for ${name}@${versionToUse}.`,
|
|
2014
|
+
};
|
|
1270
2015
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
2016
|
+
catch (error) {
|
|
2017
|
+
return {
|
|
2018
|
+
packageInput: pkgInput,
|
|
2019
|
+
packageName: name,
|
|
2020
|
+
versionQueried: versionTag,
|
|
2021
|
+
versionFetched: null,
|
|
2022
|
+
status: 'error',
|
|
2023
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2024
|
+
data: null,
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
}));
|
|
2028
|
+
const finalResponse = {
|
|
2029
|
+
queryPackages: args.packages,
|
|
2030
|
+
results: processedResults,
|
|
2031
|
+
message: `README fetching status for ${args.packages.length} package(s).`,
|
|
2032
|
+
};
|
|
2033
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2034
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1273
2035
|
}
|
|
1274
2036
|
catch (error) {
|
|
2037
|
+
const errorResponse = JSON.stringify({
|
|
2038
|
+
queryPackages: args.packages,
|
|
2039
|
+
results: [],
|
|
2040
|
+
error: `General error fetching READMEs: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2041
|
+
}, null, 2);
|
|
1275
2042
|
return {
|
|
1276
|
-
content: [
|
|
1277
|
-
{
|
|
1278
|
-
type: 'text',
|
|
1279
|
-
text: `Error fetching READMEs: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1280
|
-
},
|
|
1281
|
-
],
|
|
2043
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1282
2044
|
isError: true,
|
|
1283
2045
|
};
|
|
1284
2046
|
}
|
|
1285
2047
|
}
|
|
1286
2048
|
export async function handleNpmSearch(args) {
|
|
1287
2049
|
try {
|
|
2050
|
+
const query = args.query;
|
|
1288
2051
|
const limit = args.limit || 10;
|
|
1289
|
-
|
|
2052
|
+
if (limit < 1 || limit > 250) {
|
|
2053
|
+
// NPM API search limit is typically 250
|
|
2054
|
+
throw new Error('Limit must be between 1 and 250.');
|
|
2055
|
+
}
|
|
2056
|
+
const cacheKey = generateCacheKey('handleNpmSearch', query, limit);
|
|
2057
|
+
const cachedData = cacheGet(cacheKey);
|
|
2058
|
+
if (cachedData) {
|
|
2059
|
+
const cachedResponseJson = JSON.stringify(cachedData, null, 2);
|
|
2060
|
+
return {
|
|
2061
|
+
content: [{ type: 'text', text: cachedResponseJson }],
|
|
2062
|
+
isError: false,
|
|
2063
|
+
message: `Search results for query '${query}' with limit ${limit} from cache.`,
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
const response = await fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
|
|
1290
2067
|
if (!response.ok) {
|
|
1291
|
-
throw new Error(`Failed to search packages: ${response.statusText}`);
|
|
2068
|
+
throw new Error(`Failed to search packages: ${response.status} ${response.statusText}`);
|
|
1292
2069
|
}
|
|
1293
2070
|
const rawData = await response.json();
|
|
1294
2071
|
const parseResult = NpmSearchResultSchema.safeParse(rawData);
|
|
1295
2072
|
if (!parseResult.success) {
|
|
1296
|
-
|
|
2073
|
+
console.error('Invalid search results data received:', parseResult.error.issues);
|
|
2074
|
+
throw new Error('Invalid search results data received from NPM registry.');
|
|
1297
2075
|
}
|
|
1298
2076
|
const { objects, total } = parseResult.data;
|
|
1299
|
-
|
|
1300
|
-
text += `Found ${total.toLocaleString()} packages (showing top ${limit})\n\n`;
|
|
1301
|
-
for (const result of objects) {
|
|
2077
|
+
const resultsData = objects.map((result) => {
|
|
1302
2078
|
const pkg = result.package;
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
2079
|
+
const scoreDetail = result.score.detail;
|
|
2080
|
+
return {
|
|
2081
|
+
name: pkg.name,
|
|
2082
|
+
version: pkg.version,
|
|
2083
|
+
description: pkg.description || null,
|
|
2084
|
+
keywords: pkg.keywords || [],
|
|
2085
|
+
publisher: pkg.publisher
|
|
2086
|
+
? { username: pkg.publisher.username, email: pkg.publisher.email || null }
|
|
2087
|
+
: null, // publisher might not have email
|
|
2088
|
+
date: pkg.date || null,
|
|
2089
|
+
links: {
|
|
2090
|
+
npm: pkg.links?.npm || null,
|
|
2091
|
+
homepage: pkg.links?.homepage || null,
|
|
2092
|
+
repository: pkg.links?.repository || null,
|
|
2093
|
+
bugs: pkg.links?.bugs || null, // NpmSearchResultSchema needs to be updated if bugs is not there
|
|
2094
|
+
},
|
|
2095
|
+
score: {
|
|
2096
|
+
final: result.score.final,
|
|
2097
|
+
detail: {
|
|
2098
|
+
quality: scoreDetail.quality,
|
|
2099
|
+
popularity: scoreDetail.popularity,
|
|
2100
|
+
maintenance: scoreDetail.maintenance,
|
|
2101
|
+
},
|
|
2102
|
+
},
|
|
2103
|
+
searchScore: result.searchScore,
|
|
2104
|
+
};
|
|
2105
|
+
});
|
|
2106
|
+
const finalResponse = {
|
|
2107
|
+
query: query,
|
|
2108
|
+
limitUsed: limit,
|
|
2109
|
+
totalResults: total,
|
|
2110
|
+
resultsCount: resultsData.length,
|
|
2111
|
+
results: resultsData,
|
|
2112
|
+
message: `Search completed. Found ${total} total packages, returning ${resultsData.length}.`,
|
|
2113
|
+
};
|
|
2114
|
+
cacheSet(cacheKey, finalResponse, CACHE_TTL_MEDIUM);
|
|
2115
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2116
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1326
2117
|
}
|
|
1327
2118
|
catch (error) {
|
|
2119
|
+
const errorResponse = JSON.stringify({
|
|
2120
|
+
query: args.query,
|
|
2121
|
+
limitUsed: args.limit || 10,
|
|
2122
|
+
totalResults: 0,
|
|
2123
|
+
resultsCount: 0,
|
|
2124
|
+
results: [],
|
|
2125
|
+
error: `Error searching packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2126
|
+
}, null, 2);
|
|
1328
2127
|
return {
|
|
1329
|
-
content: [
|
|
1330
|
-
{
|
|
1331
|
-
type: 'text',
|
|
1332
|
-
text: `Error searching packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1333
|
-
},
|
|
1334
|
-
],
|
|
2128
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1335
2129
|
isError: true,
|
|
1336
2130
|
};
|
|
1337
2131
|
}
|
|
@@ -1339,59 +2133,175 @@ export async function handleNpmSearch(args) {
|
|
|
1339
2133
|
// License compatibility checker
|
|
1340
2134
|
export async function handleNpmLicenseCompatibility(args) {
|
|
1341
2135
|
try {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
2136
|
+
const packagesToProcess = args.packages || [];
|
|
2137
|
+
if (packagesToProcess.length === 0) {
|
|
2138
|
+
throw new Error('No package names provided for license compatibility analysis.');
|
|
2139
|
+
}
|
|
2140
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2141
|
+
let name = '';
|
|
2142
|
+
let versionTag = 'latest';
|
|
2143
|
+
if (typeof pkgInput === 'string') {
|
|
2144
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2145
|
+
if (atIdx > 0) {
|
|
2146
|
+
name = pkgInput.slice(0, atIdx);
|
|
2147
|
+
versionTag = pkgInput.slice(atIdx + 1);
|
|
2148
|
+
}
|
|
2149
|
+
else {
|
|
2150
|
+
name = pkgInput;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
return {
|
|
2155
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2156
|
+
packageName: 'unknown_package_input',
|
|
2157
|
+
versionQueried: versionTag,
|
|
2158
|
+
versionFetched: null,
|
|
2159
|
+
status: 'error',
|
|
2160
|
+
error: 'Invalid package input type',
|
|
2161
|
+
data: null,
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
if (!name) {
|
|
2165
|
+
return {
|
|
2166
|
+
packageInput: pkgInput,
|
|
2167
|
+
packageName: 'empty_package_name',
|
|
2168
|
+
versionQueried: versionTag,
|
|
2169
|
+
versionFetched: null,
|
|
2170
|
+
status: 'error',
|
|
2171
|
+
error: 'Empty package name derived from input',
|
|
2172
|
+
data: null,
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
const cacheKey = generateCacheKey('npmLicenseInfoForCompatibility', name, versionTag);
|
|
2176
|
+
const cachedLicenseData = cacheGet(cacheKey);
|
|
2177
|
+
if (cachedLicenseData) {
|
|
2178
|
+
return {
|
|
2179
|
+
packageInput: pkgInput,
|
|
2180
|
+
packageName: name,
|
|
2181
|
+
versionQueried: versionTag,
|
|
2182
|
+
versionFetched: cachedLicenseData.versionFetched,
|
|
2183
|
+
status: 'success_cache',
|
|
2184
|
+
error: null,
|
|
2185
|
+
data: { license: cachedLicenseData.license },
|
|
2186
|
+
message: `License info for ${name}@${cachedLicenseData.versionFetched} from cache.`,
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
try {
|
|
2190
|
+
const response = await fetch(`https://registry.npmjs.org/${name}/${versionTag}`);
|
|
2191
|
+
if (!response.ok) {
|
|
2192
|
+
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
2193
|
+
if (response.status === 404) {
|
|
2194
|
+
errorMsg = `Package ${name}@${versionTag} not found.`;
|
|
2195
|
+
}
|
|
2196
|
+
return {
|
|
2197
|
+
packageInput: pkgInput,
|
|
2198
|
+
packageName: name,
|
|
2199
|
+
versionQueried: versionTag,
|
|
2200
|
+
versionFetched: null,
|
|
2201
|
+
status: 'error',
|
|
2202
|
+
error: errorMsg,
|
|
2203
|
+
data: null,
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
const versionData = await response.json();
|
|
2207
|
+
if (!isNpmPackageVersionData(versionData)) {
|
|
2208
|
+
return {
|
|
2209
|
+
packageInput: pkgInput,
|
|
2210
|
+
packageName: name,
|
|
2211
|
+
versionQueried: versionTag,
|
|
2212
|
+
versionFetched: null, // Could use versionData.version if partially valid
|
|
2213
|
+
status: 'error',
|
|
2214
|
+
error: 'Invalid package version data format received',
|
|
2215
|
+
data: null,
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
const licenseInfoToCache = {
|
|
2219
|
+
license: versionData.license || 'UNKNOWN', // Default to UNKNOWN if null/undefined
|
|
2220
|
+
versionFetched: versionData.version,
|
|
2221
|
+
};
|
|
2222
|
+
cacheSet(cacheKey, licenseInfoToCache, CACHE_TTL_VERY_LONG);
|
|
2223
|
+
return {
|
|
2224
|
+
packageInput: pkgInput,
|
|
2225
|
+
packageName: name,
|
|
2226
|
+
versionQueried: versionTag,
|
|
2227
|
+
versionFetched: versionData.version,
|
|
2228
|
+
status: 'success',
|
|
2229
|
+
error: null,
|
|
2230
|
+
data: {
|
|
2231
|
+
license: versionData.license || 'UNKNOWN', // Default to UNKNOWN if null/undefined
|
|
2232
|
+
},
|
|
2233
|
+
message: `Successfully fetched license info for ${name}@${versionData.version}.`,
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
catch (error) {
|
|
2237
|
+
return {
|
|
2238
|
+
packageInput: pkgInput,
|
|
2239
|
+
packageName: name,
|
|
2240
|
+
versionQueried: versionTag,
|
|
2241
|
+
versionFetched: null,
|
|
2242
|
+
status: 'error',
|
|
2243
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2244
|
+
data: null,
|
|
2245
|
+
};
|
|
1346
2246
|
}
|
|
1347
|
-
const data = (await response.json());
|
|
1348
|
-
return {
|
|
1349
|
-
package: pkg,
|
|
1350
|
-
license: data.license || 'UNKNOWN',
|
|
1351
|
-
};
|
|
1352
2247
|
}));
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
2248
|
+
// Perform analysis based on fetched licenses
|
|
2249
|
+
const warnings = [];
|
|
2250
|
+
const licensesFound = processedResults
|
|
2251
|
+
.filter((r) => r.status === 'success' && r.data)
|
|
2252
|
+
.map((r) => r.data.license.toUpperCase()); // Use toUpperCase for case-insensitive matching
|
|
2253
|
+
const uniqueLicenses = [...new Set(licensesFound)];
|
|
2254
|
+
const hasGPL = uniqueLicenses.some((lic) => lic.includes('GPL'));
|
|
2255
|
+
const hasMIT = uniqueLicenses.some((lic) => lic === 'MIT');
|
|
2256
|
+
const hasApache = uniqueLicenses.some((lic) => lic.includes('APACHE')); // Check for APACHE generally
|
|
2257
|
+
const hasUnknown = uniqueLicenses.some((lic) => lic === 'UNKNOWN');
|
|
2258
|
+
const allSuccess = processedResults.every((r) => r.status === 'success');
|
|
2259
|
+
if (!allSuccess) {
|
|
2260
|
+
warnings.push('Could not fetch license information for all packages.');
|
|
1357
2261
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
const hasGPL = licenses.some(({ license }) => license?.includes('GPL'));
|
|
1361
|
-
const hasMIT = licenses.some(({ license }) => license === 'MIT');
|
|
1362
|
-
const hasApache = licenses.some(({ license }) => license?.includes('Apache'));
|
|
1363
|
-
const hasUnknown = licenses.some(({ license }) => license === 'UNKNOWN');
|
|
1364
|
-
text += 'Compatibility Analysis:\n';
|
|
1365
|
-
if (hasUnknown) {
|
|
1366
|
-
text += '⚠️ Warning: Some packages have unknown licenses. Manual review recommended.\n';
|
|
2262
|
+
if (hasUnknown && licensesFound.length > 0) {
|
|
2263
|
+
warnings.push('Some packages have unknown or unspecified licenses. Manual review recommended.');
|
|
1367
2264
|
}
|
|
1368
2265
|
if (hasGPL) {
|
|
1369
|
-
|
|
2266
|
+
warnings.push('Contains GPL licensed code. Resulting work may need to be GPL licensed.');
|
|
1370
2267
|
if (hasMIT || hasApache) {
|
|
1371
|
-
|
|
2268
|
+
warnings.push('Mixed GPL with potentially incompatible licenses (e.g., MIT, Apache). Review carefully for compliance.');
|
|
1372
2269
|
}
|
|
1373
2270
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
2271
|
+
// Further refined compatibility checks can be added here if needed
|
|
2272
|
+
let summary = 'License compatibility analysis completed.';
|
|
2273
|
+
if (warnings.length > 0) {
|
|
2274
|
+
summary = 'License compatibility analysis completed with warnings.';
|
|
1376
2275
|
}
|
|
1377
|
-
else if (
|
|
1378
|
-
|
|
2276
|
+
else if (licensesFound.length === 0 && allSuccess) {
|
|
2277
|
+
summary = 'No license information found for the queried packages.';
|
|
1379
2278
|
}
|
|
1380
|
-
else if (
|
|
1381
|
-
|
|
2279
|
+
else if (licensesFound.length > 0 && !hasGPL && !hasUnknown) {
|
|
2280
|
+
summary = 'Licenses found appear to be generally compatible (non-GPL, known licenses).';
|
|
1382
2281
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
2282
|
+
const analysis = {
|
|
2283
|
+
summary: summary,
|
|
2284
|
+
warnings: warnings,
|
|
2285
|
+
uniqueLicensesFound: uniqueLicenses,
|
|
2286
|
+
};
|
|
2287
|
+
const finalResponse = {
|
|
2288
|
+
queryPackages: args.packages,
|
|
2289
|
+
results: processedResults,
|
|
2290
|
+
analysis: analysis,
|
|
2291
|
+
message: `License compatibility check for ${args.packages.length} package(s). Note: This is a basic analysis. For legal compliance, consult a legal expert.`,
|
|
2292
|
+
};
|
|
2293
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2294
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1386
2295
|
}
|
|
1387
2296
|
catch (error) {
|
|
2297
|
+
const errorResponse = JSON.stringify({
|
|
2298
|
+
queryPackages: args.packages,
|
|
2299
|
+
results: [],
|
|
2300
|
+
analysis: null,
|
|
2301
|
+
error: `General error analyzing license compatibility: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2302
|
+
}, null, 2);
|
|
1388
2303
|
return {
|
|
1389
|
-
content: [
|
|
1390
|
-
{
|
|
1391
|
-
type: 'text',
|
|
1392
|
-
text: `Error analyzing license compatibility: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1393
|
-
},
|
|
1394
|
-
],
|
|
2304
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1395
2305
|
isError: true,
|
|
1396
2306
|
};
|
|
1397
2307
|
}
|
|
@@ -1399,346 +2309,767 @@ export async function handleNpmLicenseCompatibility(args) {
|
|
|
1399
2309
|
// Repository statistics analyzer
|
|
1400
2310
|
export async function handleNpmRepoStats(args) {
|
|
1401
2311
|
try {
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
if (!npmResponse.ok) {
|
|
1406
|
-
throw new Error(`Failed to fetch npm info for ${pkg}: ${npmResponse.statusText}`);
|
|
1407
|
-
}
|
|
1408
|
-
const npmData = (await npmResponse.json());
|
|
1409
|
-
if (!npmData.repository?.url) {
|
|
1410
|
-
return { name: pkg, text: `No repository URL found for package ${pkg}` };
|
|
1411
|
-
}
|
|
1412
|
-
// Extract GitHub repo info from URL
|
|
1413
|
-
const repoUrl = npmData.repository.url;
|
|
1414
|
-
const match = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
1415
|
-
if (!match) {
|
|
1416
|
-
return { name: pkg, text: `Could not parse GitHub repository URL: ${repoUrl}` };
|
|
1417
|
-
}
|
|
1418
|
-
const [, owner, repo] = match;
|
|
1419
|
-
// Fetch repository stats from GitHub API
|
|
1420
|
-
const githubResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
1421
|
-
headers: {
|
|
1422
|
-
Accept: 'application/vnd.github.v3+json',
|
|
1423
|
-
'User-Agent': 'MCP-Server',
|
|
1424
|
-
},
|
|
1425
|
-
});
|
|
1426
|
-
if (!githubResponse.ok) {
|
|
1427
|
-
throw new Error(`Failed to fetch GitHub stats: ${githubResponse.statusText}`);
|
|
1428
|
-
}
|
|
1429
|
-
const data = (await githubResponse.json());
|
|
1430
|
-
const text = [
|
|
1431
|
-
`${'='.repeat(80)}`,
|
|
1432
|
-
`📊 Repository Statistics for ${pkg}`,
|
|
1433
|
-
`${'='.repeat(80)}\n`,
|
|
1434
|
-
'🌟 Engagement Metrics',
|
|
1435
|
-
`${'─'.repeat(40)}`,
|
|
1436
|
-
`• Stars: ${data.stargazers_count.toLocaleString().padEnd(10)} ⭐`,
|
|
1437
|
-
`• Forks: ${data.forks_count.toLocaleString().padEnd(10)} 🔄`,
|
|
1438
|
-
`• Watchers: ${data.watchers_count.toLocaleString().padEnd(10)} 👀`,
|
|
1439
|
-
`• Open Issues: ${data.open_issues_count.toLocaleString().padEnd(10)} 🔍\n`,
|
|
1440
|
-
'📅 Timeline',
|
|
1441
|
-
`${'─'.repeat(40)}`,
|
|
1442
|
-
`• Created: ${new Date(data.created_at).toLocaleDateString()}`,
|
|
1443
|
-
`• Last Updated: ${new Date(data.updated_at).toLocaleDateString()}\n`,
|
|
1444
|
-
'🔧 Repository Details',
|
|
1445
|
-
`${'─'.repeat(40)}`,
|
|
1446
|
-
`• Default Branch: ${data.default_branch}`,
|
|
1447
|
-
`• Wiki Enabled: ${data.has_wiki ? 'Yes' : 'No'}\n`,
|
|
1448
|
-
'🏷️ Topics',
|
|
1449
|
-
`${'─'.repeat(40)}`,
|
|
1450
|
-
data.topics.length
|
|
1451
|
-
? data.topics.map((topic) => `• ${topic}`).join('\n')
|
|
1452
|
-
: '• No topics found',
|
|
1453
|
-
'',
|
|
1454
|
-
].join('\n');
|
|
1455
|
-
return { name: pkg, text };
|
|
1456
|
-
}));
|
|
1457
|
-
let text = '';
|
|
1458
|
-
for (const result of results) {
|
|
1459
|
-
text += result.text;
|
|
1460
|
-
if (results.indexOf(result) < results.length - 1) {
|
|
1461
|
-
text += '\n\n';
|
|
1462
|
-
}
|
|
2312
|
+
const packagesToProcess = args.packages || [];
|
|
2313
|
+
if (packagesToProcess.length === 0) {
|
|
2314
|
+
throw new Error('No package names provided for repository statistics analysis.');
|
|
1463
2315
|
}
|
|
1464
|
-
|
|
2316
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2317
|
+
let name = '';
|
|
2318
|
+
if (typeof pkgInput === 'string') {
|
|
2319
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2320
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version typically ignored for repo stats
|
|
2321
|
+
}
|
|
2322
|
+
else {
|
|
2323
|
+
return {
|
|
2324
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2325
|
+
packageName: 'unknown_package_input',
|
|
2326
|
+
status: 'error',
|
|
2327
|
+
error: 'Invalid package input type',
|
|
2328
|
+
data: null,
|
|
2329
|
+
message: 'Package input was not a string.',
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
if (!name) {
|
|
2333
|
+
return {
|
|
2334
|
+
packageInput: pkgInput,
|
|
2335
|
+
packageName: 'empty_package_name',
|
|
2336
|
+
status: 'error',
|
|
2337
|
+
error: 'Empty package name derived from input',
|
|
2338
|
+
data: null,
|
|
2339
|
+
message: 'Package name could not be determined from input.',
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
const cacheKey = generateCacheKey('handleNpmRepoStats', name);
|
|
2343
|
+
const cachedResult = cacheGet(cacheKey); // Cache stores the entire result object structure
|
|
2344
|
+
if (cachedResult) {
|
|
2345
|
+
// Return the entire cached result object, which already includes status, data, message
|
|
2346
|
+
return {
|
|
2347
|
+
...cachedResult, // Spread the cached result
|
|
2348
|
+
packageInput: pkgInput, // Add current input for context
|
|
2349
|
+
packageName: name, // Add current name for context
|
|
2350
|
+
status: `${cachedResult.status}_cache`, // Append _cache to status
|
|
2351
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
try {
|
|
2355
|
+
const npmResponse = await fetch(`https://registry.npmjs.org/${name}/latest`);
|
|
2356
|
+
if (!npmResponse.ok) {
|
|
2357
|
+
const errorData = {
|
|
2358
|
+
packageInput: pkgInput,
|
|
2359
|
+
packageName: name,
|
|
2360
|
+
status: 'error',
|
|
2361
|
+
error: `Failed to fetch npm info for ${name}: ${npmResponse.status} ${npmResponse.statusText}`,
|
|
2362
|
+
data: null,
|
|
2363
|
+
message: `Could not retrieve NPM package data for ${name}.`,
|
|
2364
|
+
};
|
|
2365
|
+
// Do not cache primary API call failures
|
|
2366
|
+
return errorData;
|
|
2367
|
+
}
|
|
2368
|
+
const npmData = await npmResponse.json();
|
|
2369
|
+
if (!isNpmPackageVersionData(npmData)) {
|
|
2370
|
+
const errorData = {
|
|
2371
|
+
packageInput: pkgInput,
|
|
2372
|
+
packageName: name,
|
|
2373
|
+
status: 'error',
|
|
2374
|
+
error: 'Invalid NPM package data format received.',
|
|
2375
|
+
data: null,
|
|
2376
|
+
message: `Malformed NPM package data for ${name}.`,
|
|
2377
|
+
};
|
|
2378
|
+
return errorData;
|
|
2379
|
+
}
|
|
2380
|
+
const repoUrl = npmData.repository?.url;
|
|
2381
|
+
if (!repoUrl) {
|
|
2382
|
+
const resultNoRepo = {
|
|
2383
|
+
packageInput: pkgInput,
|
|
2384
|
+
packageName: name,
|
|
2385
|
+
status: 'no_repo_found',
|
|
2386
|
+
error: null,
|
|
2387
|
+
data: null,
|
|
2388
|
+
message: `No repository URL found in package data for ${name}.`,
|
|
2389
|
+
};
|
|
2390
|
+
cacheSet(cacheKey, resultNoRepo, CACHE_TTL_LONG);
|
|
2391
|
+
return resultNoRepo;
|
|
2392
|
+
}
|
|
2393
|
+
const githubMatch = repoUrl.match(/github\.com[:\/]([^\/]+)\/([^\/.]+)/);
|
|
2394
|
+
if (!githubMatch) {
|
|
2395
|
+
const resultNotGitHub = {
|
|
2396
|
+
packageInput: pkgInput,
|
|
2397
|
+
packageName: name,
|
|
2398
|
+
status: 'not_github_repo',
|
|
2399
|
+
error: null,
|
|
2400
|
+
data: { repositoryUrl: repoUrl },
|
|
2401
|
+
message: `Repository URL found (${repoUrl}) is not a standard GitHub URL.`,
|
|
2402
|
+
};
|
|
2403
|
+
cacheSet(cacheKey, resultNotGitHub, CACHE_TTL_LONG);
|
|
2404
|
+
return resultNotGitHub;
|
|
2405
|
+
}
|
|
2406
|
+
const [, owner, repo] = githubMatch;
|
|
2407
|
+
const githubRepoApiUrl = `https://api.github.com/repos/${owner}/${repo.replace(/\.git$/, '')}`;
|
|
2408
|
+
const githubResponse = await fetch(githubRepoApiUrl, {
|
|
2409
|
+
headers: {
|
|
2410
|
+
Accept: 'application/vnd.github.v3+json',
|
|
2411
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
2412
|
+
},
|
|
2413
|
+
});
|
|
2414
|
+
if (!githubResponse.ok) {
|
|
2415
|
+
const errorData = {
|
|
2416
|
+
packageInput: pkgInput,
|
|
2417
|
+
packageName: name,
|
|
2418
|
+
status: 'error',
|
|
2419
|
+
error: `Failed to fetch GitHub repo stats for ${owner}/${repo}: ${githubResponse.status} ${githubResponse.statusText}`,
|
|
2420
|
+
data: { githubRepoUrl: githubRepoApiUrl },
|
|
2421
|
+
message: `Could not retrieve GitHub repository statistics from ${githubRepoApiUrl}.`,
|
|
2422
|
+
};
|
|
2423
|
+
// Do not cache GitHub API call failures for now
|
|
2424
|
+
return errorData;
|
|
2425
|
+
}
|
|
2426
|
+
const githubData = (await githubResponse.json());
|
|
2427
|
+
const successResult = {
|
|
2428
|
+
packageInput: pkgInput,
|
|
2429
|
+
packageName: name,
|
|
2430
|
+
status: 'success',
|
|
2431
|
+
error: null,
|
|
2432
|
+
data: {
|
|
2433
|
+
githubRepoUrl: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}`,
|
|
2434
|
+
stars: githubData.stargazers_count,
|
|
2435
|
+
forks: githubData.forks_count,
|
|
2436
|
+
openIssues: githubData.open_issues_count,
|
|
2437
|
+
watchers: githubData.watchers_count,
|
|
2438
|
+
createdAt: githubData.created_at,
|
|
2439
|
+
updatedAt: githubData.updated_at,
|
|
2440
|
+
defaultBranch: githubData.default_branch,
|
|
2441
|
+
hasWiki: githubData.has_wiki,
|
|
2442
|
+
topics: githubData.topics || [],
|
|
2443
|
+
},
|
|
2444
|
+
message: 'GitHub repository statistics fetched successfully.',
|
|
2445
|
+
};
|
|
2446
|
+
cacheSet(cacheKey, successResult, CACHE_TTL_LONG);
|
|
2447
|
+
return successResult;
|
|
2448
|
+
}
|
|
2449
|
+
catch (error) {
|
|
2450
|
+
return {
|
|
2451
|
+
packageInput: pkgInput,
|
|
2452
|
+
packageName: name,
|
|
2453
|
+
status: 'error',
|
|
2454
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2455
|
+
data: null,
|
|
2456
|
+
message: `An unexpected error occurred while processing ${name}.`,
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
}));
|
|
2460
|
+
const finalResponse = {
|
|
2461
|
+
queryPackages: args.packages,
|
|
2462
|
+
results: processedResults,
|
|
2463
|
+
message: `Repository statistics analysis for ${args.packages.length} package(s).`,
|
|
2464
|
+
};
|
|
2465
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2466
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1465
2467
|
}
|
|
1466
2468
|
catch (error) {
|
|
2469
|
+
const errorResponse = JSON.stringify({
|
|
2470
|
+
queryPackages: args.packages,
|
|
2471
|
+
results: [],
|
|
2472
|
+
error: `General error analyzing repository stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2473
|
+
}, null, 2);
|
|
1467
2474
|
return {
|
|
1468
|
-
content: [
|
|
1469
|
-
{
|
|
1470
|
-
type: 'text',
|
|
1471
|
-
text: `Error analyzing repository stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1472
|
-
},
|
|
1473
|
-
],
|
|
2475
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1474
2476
|
isError: true,
|
|
1475
2477
|
};
|
|
1476
2478
|
}
|
|
1477
2479
|
}
|
|
1478
2480
|
export async function handleNpmDeprecated(args) {
|
|
1479
2481
|
try {
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
throw new Error('No latest version found');
|
|
1493
|
-
}
|
|
1494
|
-
const latestVersionInfo = rawData.versions[latestVersion];
|
|
1495
|
-
const dependencies = {
|
|
1496
|
-
...(latestVersionInfo.dependencies || {}),
|
|
1497
|
-
...(latestVersionInfo.devDependencies || {}),
|
|
1498
|
-
...(latestVersionInfo.peerDependencies || {}),
|
|
1499
|
-
};
|
|
1500
|
-
// Check each dependency
|
|
1501
|
-
const deprecatedDeps = [];
|
|
1502
|
-
await Promise.all(Object.entries(dependencies).map(async ([dep, version]) => {
|
|
1503
|
-
try {
|
|
1504
|
-
const depResponse = await fetch(`https://registry.npmjs.org/${dep}`);
|
|
1505
|
-
if (!depResponse.ok)
|
|
1506
|
-
return;
|
|
1507
|
-
const depData = (await depResponse.json());
|
|
1508
|
-
const depVersion = version.replace(/[^0-9.]/g, '');
|
|
1509
|
-
if (depData.versions?.[depVersion]?.deprecated) {
|
|
1510
|
-
deprecatedDeps.push({
|
|
1511
|
-
name: dep,
|
|
1512
|
-
version: depVersion,
|
|
1513
|
-
message: depData.versions[depVersion].deprecated || 'No message provided',
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
2482
|
+
const packagesToProcess = args.packages || [];
|
|
2483
|
+
if (packagesToProcess.length === 0) {
|
|
2484
|
+
throw new Error('No package names provided');
|
|
2485
|
+
}
|
|
2486
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2487
|
+
let name = '';
|
|
2488
|
+
let version = 'latest'; // Default to 'latest'
|
|
2489
|
+
if (typeof pkgInput === 'string') {
|
|
2490
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2491
|
+
if (atIdx > 0) {
|
|
2492
|
+
name = pkgInput.slice(0, atIdx);
|
|
2493
|
+
version = pkgInput.slice(atIdx + 1);
|
|
1516
2494
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
2495
|
+
else {
|
|
2496
|
+
name = pkgInput;
|
|
1519
2497
|
}
|
|
1520
|
-
}));
|
|
1521
|
-
// Check if the package itself is deprecated
|
|
1522
|
-
const isDeprecated = latestVersionInfo.deprecated;
|
|
1523
|
-
let text = `📦 Deprecation Check for ${pkg}@${latestVersion}\n\n`;
|
|
1524
|
-
if (isDeprecated) {
|
|
1525
|
-
text += '⚠️ WARNING: This package is deprecated!\n';
|
|
1526
|
-
text += `Deprecation message: ${latestVersionInfo.deprecated}\n\n`;
|
|
1527
2498
|
}
|
|
1528
2499
|
else {
|
|
1529
|
-
|
|
2500
|
+
return {
|
|
2501
|
+
package: 'unknown_package_input',
|
|
2502
|
+
status: 'error',
|
|
2503
|
+
error: 'Invalid package input type',
|
|
2504
|
+
data: null,
|
|
2505
|
+
message: 'Package input was not a string.',
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
const initialPackageNameForOutput = version === 'latest' ? name : `${name}@${version}`;
|
|
2509
|
+
const cacheKey = generateCacheKey('handleNpmDeprecated', name, version);
|
|
2510
|
+
const cachedResult = cacheGet(cacheKey);
|
|
2511
|
+
if (cachedResult) {
|
|
2512
|
+
// console.debug(`[handleNpmDeprecated] Cache hit for ${cacheKey}`);
|
|
2513
|
+
return {
|
|
2514
|
+
package: cachedResult.package,
|
|
2515
|
+
status: 'success_cache',
|
|
2516
|
+
error: null,
|
|
2517
|
+
data: cachedResult.data,
|
|
2518
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2519
|
+
};
|
|
1530
2520
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2521
|
+
// console.debug(`[handleNpmDeprecated] Cache miss for ${cacheKey}`);
|
|
2522
|
+
try {
|
|
2523
|
+
const mainPkgResponse = await fetch(`https://registry.npmjs.org/${name}`);
|
|
2524
|
+
if (!mainPkgResponse.ok) {
|
|
2525
|
+
return {
|
|
2526
|
+
package: initialPackageNameForOutput,
|
|
2527
|
+
status: 'error',
|
|
2528
|
+
error: `Failed to fetch package info for ${name}: ${mainPkgResponse.status} ${mainPkgResponse.statusText}`,
|
|
2529
|
+
data: null,
|
|
2530
|
+
message: `Could not retrieve main package data for ${name}.`,
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
const mainPkgData = (await mainPkgResponse.json());
|
|
2534
|
+
let versionToFetch = version;
|
|
2535
|
+
if (version === 'latest') {
|
|
2536
|
+
versionToFetch = mainPkgData['dist-tags']?.latest || 'latest';
|
|
2537
|
+
if (versionToFetch === 'latest' && !mainPkgData.versions?.[versionToFetch]) {
|
|
2538
|
+
const availableVersions = Object.keys(mainPkgData.versions || {});
|
|
2539
|
+
if (availableVersions.length > 0) {
|
|
2540
|
+
versionToFetch = availableVersions.sort().pop() || 'latest'; // Basic sort
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
1536
2543
|
}
|
|
2544
|
+
const finalPackageNameForOutput = `${name}@${versionToFetch}`;
|
|
2545
|
+
const versionInfo = mainPkgData.versions?.[versionToFetch];
|
|
2546
|
+
if (!versionInfo) {
|
|
2547
|
+
return {
|
|
2548
|
+
package: finalPackageNameForOutput,
|
|
2549
|
+
status: 'error',
|
|
2550
|
+
error: `Version ${versionToFetch} not found for package ${name}.`,
|
|
2551
|
+
data: null,
|
|
2552
|
+
message: `Specified version for ${name} does not exist.`,
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
const isPackageDeprecated = !!versionInfo.deprecated;
|
|
2556
|
+
const packageDeprecationMessage = versionInfo.deprecated || null;
|
|
2557
|
+
const processDependencies = async (deps) => {
|
|
2558
|
+
if (!deps)
|
|
2559
|
+
return [];
|
|
2560
|
+
const depChecks = Object.entries(deps).map(async ([depName, depSemVer]) => {
|
|
2561
|
+
const lookedUpAs = depName; // Strategy: always use original name, no cleaning.
|
|
2562
|
+
let statusMessage = '';
|
|
2563
|
+
try {
|
|
2564
|
+
// console.debug(`[handleNpmDeprecated] Checking dependency: ${depName}@${depSemVer}`);
|
|
2565
|
+
const depInfoResponse = await fetch(`https://registry.npmjs.org/${encodeURIComponent(depName)}`);
|
|
2566
|
+
if (!depInfoResponse.ok) {
|
|
2567
|
+
statusMessage = `Could not fetch dependency info for '${depName}' (status: ${depInfoResponse.status}). Deprecation status unknown.`;
|
|
2568
|
+
// console.warn(`[handleNpmDeprecated] ${statusMessage}`);
|
|
2569
|
+
return {
|
|
2570
|
+
name: depName,
|
|
2571
|
+
version: depSemVer,
|
|
2572
|
+
lookedUpAs: lookedUpAs,
|
|
2573
|
+
isDeprecated: false, // Assume not deprecated as status is unknown
|
|
2574
|
+
deprecationMessage: null,
|
|
2575
|
+
statusMessage: statusMessage,
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
const depData = (await depInfoResponse.json());
|
|
2579
|
+
const latestDepVersionTag = depData['dist-tags']?.latest;
|
|
2580
|
+
const latestDepVersionInfo = latestDepVersionTag
|
|
2581
|
+
? depData.versions?.[latestDepVersionTag]
|
|
2582
|
+
: undefined;
|
|
2583
|
+
statusMessage = `Successfully checked '${depName}'.`;
|
|
2584
|
+
return {
|
|
2585
|
+
name: depName,
|
|
2586
|
+
version: depSemVer,
|
|
2587
|
+
lookedUpAs: lookedUpAs,
|
|
2588
|
+
isDeprecated: !!latestDepVersionInfo?.deprecated,
|
|
2589
|
+
deprecationMessage: latestDepVersionInfo?.deprecated || null,
|
|
2590
|
+
statusMessage: statusMessage,
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
catch (error) {
|
|
2594
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown processing error';
|
|
2595
|
+
statusMessage = `Error processing dependency '${depName}': ${errorMessage}. Deprecation status unknown.`;
|
|
2596
|
+
// console.warn(`[handleNpmDeprecated] ${statusMessage}`);
|
|
2597
|
+
return {
|
|
2598
|
+
name: depName,
|
|
2599
|
+
version: depSemVer,
|
|
2600
|
+
lookedUpAs: lookedUpAs,
|
|
2601
|
+
isDeprecated: false, // Assume not deprecated as status is unknown
|
|
2602
|
+
deprecationMessage: null,
|
|
2603
|
+
statusMessage: statusMessage,
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
return Promise.all(depChecks);
|
|
2608
|
+
};
|
|
2609
|
+
const directDeps = await processDependencies(versionInfo.dependencies);
|
|
2610
|
+
const devDeps = await processDependencies(versionInfo.devDependencies);
|
|
2611
|
+
const peerDeps = await processDependencies(versionInfo.peerDependencies);
|
|
2612
|
+
const allDeps = [...directDeps, ...devDeps, ...peerDeps];
|
|
2613
|
+
const unverifiableDepsCount = allDeps.filter((dep) => {
|
|
2614
|
+
const msg = dep.statusMessage.toLowerCase();
|
|
2615
|
+
return msg.includes('could not fetch') || msg.includes('error processing');
|
|
2616
|
+
}).length;
|
|
2617
|
+
let dependencySummaryMessage = `Processed ${allDeps.length} total dependencies.`;
|
|
2618
|
+
if (unverifiableDepsCount > 0) {
|
|
2619
|
+
dependencySummaryMessage += ` Could not verify the status for ${unverifiableDepsCount} dependencies (e.g., package name not found in registry or network issues). Their deprecation status is unknown.`;
|
|
2620
|
+
}
|
|
2621
|
+
const resultData = {
|
|
2622
|
+
isPackageDeprecated,
|
|
2623
|
+
packageDeprecationMessage,
|
|
2624
|
+
dependencies: {
|
|
2625
|
+
direct: directDeps,
|
|
2626
|
+
development: devDeps,
|
|
2627
|
+
peer: peerDeps,
|
|
2628
|
+
},
|
|
2629
|
+
dependencySummary: {
|
|
2630
|
+
totalDependencies: allDeps.length,
|
|
2631
|
+
unverifiableDependencies: unverifiableDepsCount,
|
|
2632
|
+
message: dependencySummaryMessage,
|
|
2633
|
+
},
|
|
2634
|
+
};
|
|
2635
|
+
const fullMessage = `Deprecation status for ${finalPackageNameForOutput}. ${dependencySummaryMessage}`;
|
|
2636
|
+
const resultToCache = {
|
|
2637
|
+
package: finalPackageNameForOutput,
|
|
2638
|
+
data: resultData,
|
|
2639
|
+
message: fullMessage,
|
|
2640
|
+
};
|
|
2641
|
+
cacheSet(cacheKey, resultToCache, CACHE_TTL_MEDIUM);
|
|
2642
|
+
// console.debug(`[handleNpmDeprecated] Set cache for ${cacheKey}`);
|
|
2643
|
+
return {
|
|
2644
|
+
package: finalPackageNameForOutput,
|
|
2645
|
+
status: 'success',
|
|
2646
|
+
error: null,
|
|
2647
|
+
data: resultData,
|
|
2648
|
+
message: fullMessage,
|
|
2649
|
+
};
|
|
1537
2650
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
2651
|
+
catch (error) {
|
|
2652
|
+
// console.error(`[handleNpmDeprecated] Error processing ${initialPackageNameForOutput}: ${error}`);
|
|
2653
|
+
return {
|
|
2654
|
+
package: initialPackageNameForOutput,
|
|
2655
|
+
status: 'error',
|
|
2656
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2657
|
+
data: null,
|
|
2658
|
+
message: `An unexpected error occurred while processing ${initialPackageNameForOutput}.`,
|
|
2659
|
+
};
|
|
1540
2660
|
}
|
|
1541
|
-
return { name: pkg, text };
|
|
1542
2661
|
}));
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
text += result.text;
|
|
1546
|
-
}
|
|
1547
|
-
return { content: [{ type: 'text', text }], isError: false };
|
|
2662
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
2663
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1548
2664
|
}
|
|
1549
2665
|
catch (error) {
|
|
2666
|
+
// console.error(`[handleNpmDeprecated] General error: ${error}`);
|
|
2667
|
+
const errorResponse = JSON.stringify({
|
|
2668
|
+
results: [],
|
|
2669
|
+
error: `General error checking deprecated packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2670
|
+
}, null, 2);
|
|
1550
2671
|
return {
|
|
1551
|
-
content: [
|
|
1552
|
-
{
|
|
1553
|
-
type: 'text',
|
|
1554
|
-
text: `Error checking deprecated packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1555
|
-
},
|
|
1556
|
-
],
|
|
2672
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1557
2673
|
isError: true,
|
|
1558
2674
|
};
|
|
1559
2675
|
}
|
|
1560
2676
|
}
|
|
1561
2677
|
export async function handleNpmChangelogAnalysis(args) {
|
|
1562
2678
|
try {
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
return { name: pkg, text: `No repository found for package ${pkg}` };
|
|
1576
|
-
}
|
|
1577
|
-
// Extract GitHub repo info from URL
|
|
1578
|
-
const match = repository.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
1579
|
-
if (!match) {
|
|
1580
|
-
return { name: pkg, text: `Could not parse GitHub repository URL: ${repository}` };
|
|
1581
|
-
}
|
|
1582
|
-
const [, owner, repo] = match;
|
|
1583
|
-
// Check common changelog file names
|
|
1584
|
-
const changelogFiles = [
|
|
1585
|
-
'CHANGELOG.md',
|
|
1586
|
-
'changelog.md',
|
|
1587
|
-
'CHANGES.md',
|
|
1588
|
-
'changes.md',
|
|
1589
|
-
'HISTORY.md',
|
|
1590
|
-
'history.md',
|
|
1591
|
-
'NEWS.md',
|
|
1592
|
-
'news.md',
|
|
1593
|
-
'RELEASES.md',
|
|
1594
|
-
'releases.md',
|
|
1595
|
-
];
|
|
1596
|
-
let changelog = null;
|
|
1597
|
-
for (const file of changelogFiles) {
|
|
1598
|
-
try {
|
|
1599
|
-
const response = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/master/${file}`);
|
|
1600
|
-
if (response.ok) {
|
|
1601
|
-
changelog = await response.text();
|
|
1602
|
-
break;
|
|
1603
|
-
}
|
|
2679
|
+
const packagesToProcess = args.packages || [];
|
|
2680
|
+
if (packagesToProcess.length === 0) {
|
|
2681
|
+
throw new Error('No package names provided for changelog analysis.');
|
|
2682
|
+
}
|
|
2683
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2684
|
+
let name = '';
|
|
2685
|
+
let versionQueried = undefined;
|
|
2686
|
+
if (typeof pkgInput === 'string') {
|
|
2687
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2688
|
+
if (atIdx > 0) {
|
|
2689
|
+
name = pkgInput.slice(0, atIdx);
|
|
2690
|
+
versionQueried = pkgInput.slice(atIdx + 1);
|
|
1604
2691
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
2692
|
+
else {
|
|
2693
|
+
name = pkgInput;
|
|
1607
2694
|
}
|
|
1608
2695
|
}
|
|
1609
|
-
// Get release information from GitHub API
|
|
1610
|
-
const githubResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`, {
|
|
1611
|
-
headers: {
|
|
1612
|
-
Accept: 'application/vnd.github.v3+json',
|
|
1613
|
-
'User-Agent': 'MCP-Server',
|
|
1614
|
-
},
|
|
1615
|
-
});
|
|
1616
|
-
const releases = (githubResponse.ok ? await githubResponse.json() : []);
|
|
1617
|
-
let text = `📋 Changelog Analysis for ${pkg}\n\n`;
|
|
1618
|
-
// Analyze version history from npm
|
|
1619
|
-
const versions = Object.keys(npmData.versions || {}).sort((a, b) => {
|
|
1620
|
-
const [aMajor = 0, aMinor = 0] = a.split('.').map(Number);
|
|
1621
|
-
const [bMajor = 0, bMinor = 0] = b.split('.').map(Number);
|
|
1622
|
-
return bMajor - aMajor || bMinor - aMinor;
|
|
1623
|
-
});
|
|
1624
|
-
text += '📦 Version History:\n';
|
|
1625
|
-
text += `• Total versions: ${versions.length}\n`;
|
|
1626
|
-
text += `• Latest version: ${versions[0]}\n`;
|
|
1627
|
-
text += `• First version: ${versions[versions.length - 1]}\n\n`;
|
|
1628
|
-
if (changelog) {
|
|
1629
|
-
text += '📝 Changelog found!\n\n';
|
|
1630
|
-
// Extract and analyze the last few versions from changelog
|
|
1631
|
-
const recentChanges = changelog.split('\n').slice(0, 20).join('\n');
|
|
1632
|
-
text += `Recent changes:\n${recentChanges}\n...\n\n`;
|
|
1633
|
-
}
|
|
1634
2696
|
else {
|
|
1635
|
-
|
|
2697
|
+
return {
|
|
2698
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2699
|
+
packageName: 'unknown_package_input',
|
|
2700
|
+
versionQueried: versionQueried,
|
|
2701
|
+
status: 'error',
|
|
2702
|
+
error: 'Invalid package input type',
|
|
2703
|
+
data: null,
|
|
2704
|
+
message: 'Package input was not a string.',
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
if (!name) {
|
|
2708
|
+
return {
|
|
2709
|
+
packageInput: pkgInput,
|
|
2710
|
+
packageName: 'empty_package_name',
|
|
2711
|
+
versionQueried: versionQueried,
|
|
2712
|
+
status: 'error',
|
|
2713
|
+
error: 'Empty package name derived from input',
|
|
2714
|
+
data: null,
|
|
2715
|
+
message: 'Package name could not be determined from input.',
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
const cacheKey = generateCacheKey('handleNpmChangelogAnalysis', name);
|
|
2719
|
+
const cachedResult = cacheGet(cacheKey); // Expects the full result object to be cached
|
|
2720
|
+
if (cachedResult) {
|
|
2721
|
+
return {
|
|
2722
|
+
...cachedResult,
|
|
2723
|
+
packageInput: pkgInput, // Ensure these are current for this specific call
|
|
2724
|
+
packageName: name,
|
|
2725
|
+
versionQueried: versionQueried,
|
|
2726
|
+
status: `${cachedResult.status}_cache`,
|
|
2727
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2728
|
+
};
|
|
1636
2729
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
2730
|
+
try {
|
|
2731
|
+
const npmResponse = await fetch(`https://registry.npmjs.org/${name}`);
|
|
2732
|
+
if (!npmResponse.ok) {
|
|
2733
|
+
const errorResult = {
|
|
2734
|
+
packageInput: pkgInput,
|
|
2735
|
+
packageName: name,
|
|
2736
|
+
versionQueried: versionQueried,
|
|
2737
|
+
status: 'error',
|
|
2738
|
+
error: `Failed to fetch npm info for ${name}: ${npmResponse.status} ${npmResponse.statusText}`,
|
|
2739
|
+
data: null,
|
|
2740
|
+
message: `Could not retrieve NPM package data for ${name}.`,
|
|
2741
|
+
};
|
|
2742
|
+
return errorResult; // Do not cache this type of error
|
|
2743
|
+
}
|
|
2744
|
+
const npmData = await npmResponse.json();
|
|
2745
|
+
if (!isNpmPackageInfo(npmData)) {
|
|
2746
|
+
const errorResult = {
|
|
2747
|
+
packageInput: pkgInput,
|
|
2748
|
+
packageName: name,
|
|
2749
|
+
versionQueried: versionQueried,
|
|
2750
|
+
status: 'error',
|
|
2751
|
+
error: 'Invalid NPM package info data received',
|
|
2752
|
+
data: null,
|
|
2753
|
+
message: `Received malformed NPM package data for ${name}.`,
|
|
2754
|
+
};
|
|
2755
|
+
return errorResult; // Do not cache this type of error
|
|
2756
|
+
}
|
|
2757
|
+
const repositoryUrl = npmData.repository?.url;
|
|
2758
|
+
if (!repositoryUrl) {
|
|
2759
|
+
const resultNoRepo = {
|
|
2760
|
+
packageInput: pkgInput,
|
|
2761
|
+
packageName: name,
|
|
2762
|
+
versionQueried: versionQueried,
|
|
2763
|
+
status: 'no_repo_found',
|
|
2764
|
+
error: null,
|
|
2765
|
+
data: null,
|
|
2766
|
+
message: `No repository URL found in package data for ${name}.`,
|
|
2767
|
+
};
|
|
2768
|
+
cacheSet(cacheKey, resultNoRepo, CACHE_TTL_MEDIUM);
|
|
2769
|
+
return resultNoRepo;
|
|
2770
|
+
}
|
|
2771
|
+
const githubMatch = repositoryUrl.match(/github\.com[:\/]([^\/]+)\/([^\/.]+)/);
|
|
2772
|
+
if (!githubMatch) {
|
|
2773
|
+
const resultNotGitHub = {
|
|
2774
|
+
packageInput: pkgInput,
|
|
2775
|
+
packageName: name,
|
|
2776
|
+
versionQueried: versionQueried,
|
|
2777
|
+
status: 'not_github_repo',
|
|
2778
|
+
error: null,
|
|
2779
|
+
data: { repositoryUrl: repositoryUrl },
|
|
2780
|
+
message: `Repository URL (${repositoryUrl}) is not a standard GitHub URL.`,
|
|
2781
|
+
};
|
|
2782
|
+
cacheSet(cacheKey, resultNotGitHub, CACHE_TTL_MEDIUM);
|
|
2783
|
+
return resultNotGitHub;
|
|
2784
|
+
}
|
|
2785
|
+
const [, owner, repo] = githubMatch;
|
|
2786
|
+
const repoNameForUrl = repo.replace(/\.git$/, '');
|
|
2787
|
+
const changelogFiles = [
|
|
2788
|
+
'CHANGELOG.md',
|
|
2789
|
+
'changelog.md',
|
|
2790
|
+
'CHANGES.md',
|
|
2791
|
+
'changes.md',
|
|
2792
|
+
'HISTORY.md',
|
|
2793
|
+
'history.md',
|
|
2794
|
+
'NEWS.md',
|
|
2795
|
+
'news.md',
|
|
2796
|
+
'RELEASES.md',
|
|
2797
|
+
'releases.md',
|
|
2798
|
+
];
|
|
2799
|
+
let changelogContent = null;
|
|
2800
|
+
let changelogSourceUrl = null;
|
|
2801
|
+
let hasChangelogFile = false;
|
|
2802
|
+
for (const file of changelogFiles) {
|
|
2803
|
+
try {
|
|
2804
|
+
const rawChangelogUrl = `https://raw.githubusercontent.com/${owner}/${repoNameForUrl}/master/${file}`;
|
|
2805
|
+
const response = await fetch(rawChangelogUrl);
|
|
2806
|
+
if (response.ok) {
|
|
2807
|
+
changelogContent = await response.text();
|
|
2808
|
+
changelogSourceUrl = rawChangelogUrl;
|
|
2809
|
+
hasChangelogFile = true;
|
|
2810
|
+
break;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
catch (error) {
|
|
2814
|
+
console.debug(`Error fetching changelog file ${file} for ${name}: ${error}`);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
let githubReleases = [];
|
|
2818
|
+
try {
|
|
2819
|
+
const githubApiResponse = await fetch(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
|
|
2820
|
+
headers: {
|
|
2821
|
+
Accept: 'application/vnd.github.v3+json',
|
|
2822
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
2823
|
+
},
|
|
2824
|
+
});
|
|
2825
|
+
if (githubApiResponse.ok) {
|
|
2826
|
+
const releasesData = (await githubApiResponse.json());
|
|
2827
|
+
githubReleases = releasesData.map((r) => ({
|
|
2828
|
+
tag_name: r.tag_name || null,
|
|
2829
|
+
name: r.name || null,
|
|
2830
|
+
published_at: r.published_at || null,
|
|
2831
|
+
}));
|
|
2832
|
+
}
|
|
1646
2833
|
}
|
|
2834
|
+
catch (error) {
|
|
2835
|
+
console.debug(`Error fetching GitHub releases for ${name}: ${error}`);
|
|
2836
|
+
}
|
|
2837
|
+
const versions = Object.keys(npmData.versions || {});
|
|
2838
|
+
const npmVersionHistory = {
|
|
2839
|
+
totalVersions: versions.length,
|
|
2840
|
+
latestVersion: npmData['dist-tags']?.latest || (versions.length > 0 ? versions.sort().pop() : null),
|
|
2841
|
+
firstVersion: versions.length > 0 ? versions.sort()[0] : null,
|
|
2842
|
+
};
|
|
2843
|
+
const status = changelogContent || githubReleases.length > 0 ? 'success' : 'no_changelog_found';
|
|
2844
|
+
const message = status === 'success'
|
|
2845
|
+
? `Changelog and release information retrieved for ${name}.`
|
|
2846
|
+
: status === 'no_changelog_found'
|
|
2847
|
+
? `No changelog file or GitHub releases found for ${name}.`
|
|
2848
|
+
: `Changelog analysis for ${name}.`;
|
|
2849
|
+
const resultToCache = {
|
|
2850
|
+
packageInput: pkgInput, // This might differ on subsequent cache hits, so store the original reference for this specific cache entry
|
|
2851
|
+
packageName: name,
|
|
2852
|
+
versionQueried: versionQueried,
|
|
2853
|
+
status: status,
|
|
2854
|
+
error: null,
|
|
2855
|
+
data: {
|
|
2856
|
+
repositoryUrl: repositoryUrl,
|
|
2857
|
+
changelogSourceUrl: changelogSourceUrl,
|
|
2858
|
+
changelogContent: changelogContent
|
|
2859
|
+
? `${changelogContent.split('\n').slice(0, 50).join('\n')}...`
|
|
2860
|
+
: null,
|
|
2861
|
+
hasChangelogFile: hasChangelogFile,
|
|
2862
|
+
githubReleases: githubReleases,
|
|
2863
|
+
npmVersionHistory: npmVersionHistory,
|
|
2864
|
+
},
|
|
2865
|
+
message: message,
|
|
2866
|
+
};
|
|
2867
|
+
cacheSet(cacheKey, resultToCache, CACHE_TTL_MEDIUM);
|
|
2868
|
+
return resultToCache;
|
|
1647
2869
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
2870
|
+
catch (error) {
|
|
2871
|
+
const errorResult = {
|
|
2872
|
+
packageInput: pkgInput,
|
|
2873
|
+
packageName: name,
|
|
2874
|
+
versionQueried: versionQueried,
|
|
2875
|
+
status: 'error',
|
|
2876
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2877
|
+
data: null,
|
|
2878
|
+
message: `An unexpected error occurred while analyzing changelog for ${name}.`,
|
|
2879
|
+
};
|
|
2880
|
+
return errorResult; // Do not cache general errors
|
|
1650
2881
|
}
|
|
1651
|
-
return { name: pkg, text };
|
|
1652
2882
|
}));
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
2883
|
+
const finalResponse = {
|
|
2884
|
+
queryPackages: args.packages,
|
|
2885
|
+
results: processedResults,
|
|
2886
|
+
};
|
|
2887
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2888
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1658
2889
|
}
|
|
1659
2890
|
catch (error) {
|
|
2891
|
+
const errorResponse = JSON.stringify({
|
|
2892
|
+
queryPackages: args.packages,
|
|
2893
|
+
results: [],
|
|
2894
|
+
error: `General error analyzing changelogs: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2895
|
+
}, null, 2);
|
|
1660
2896
|
return {
|
|
1661
|
-
content: [
|
|
1662
|
-
{
|
|
1663
|
-
type: 'text',
|
|
1664
|
-
text: `Error analyzing changelog: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1665
|
-
},
|
|
1666
|
-
],
|
|
2897
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1667
2898
|
isError: true,
|
|
1668
2899
|
};
|
|
1669
2900
|
}
|
|
1670
2901
|
}
|
|
1671
2902
|
export async function handleNpmAlternatives(args) {
|
|
1672
2903
|
try {
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2904
|
+
const packagesToProcess = args.packages || [];
|
|
2905
|
+
if (packagesToProcess.length === 0) {
|
|
2906
|
+
throw new Error('No package names provided to find alternatives.');
|
|
2907
|
+
}
|
|
2908
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2909
|
+
let originalPackageName = '';
|
|
2910
|
+
let versionQueried = undefined;
|
|
2911
|
+
if (typeof pkgInput === 'string') {
|
|
2912
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2913
|
+
if (atIdx > 0) {
|
|
2914
|
+
originalPackageName = pkgInput.slice(0, atIdx);
|
|
2915
|
+
versionQueried = pkgInput.slice(atIdx + 1);
|
|
2916
|
+
}
|
|
2917
|
+
else {
|
|
2918
|
+
originalPackageName = pkgInput;
|
|
2919
|
+
}
|
|
1677
2920
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2921
|
+
else {
|
|
2922
|
+
return {
|
|
2923
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2924
|
+
packageName: 'unknown_package_input',
|
|
2925
|
+
status: 'error',
|
|
2926
|
+
error: 'Invalid package input type',
|
|
2927
|
+
data: null,
|
|
2928
|
+
message: 'Package input was not a string.',
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
if (!originalPackageName) {
|
|
2932
|
+
return {
|
|
2933
|
+
packageInput: pkgInput,
|
|
2934
|
+
packageName: 'empty_package_name',
|
|
2935
|
+
status: 'error',
|
|
2936
|
+
error: 'Empty package name derived from input',
|
|
2937
|
+
data: null,
|
|
2938
|
+
message: 'Package name could not be determined from input.',
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
const cacheKey = generateCacheKey('handleNpmAlternatives', originalPackageName);
|
|
2942
|
+
const cachedResult = cacheGet(cacheKey); // Expects the full result object
|
|
2943
|
+
if (cachedResult) {
|
|
2944
|
+
return {
|
|
2945
|
+
...cachedResult,
|
|
2946
|
+
packageInput: pkgInput, // current input context
|
|
2947
|
+
packageName: originalPackageName, // current name context
|
|
2948
|
+
// versionQueried is part of cachedResult.data or similar if stored, or add if needed
|
|
2949
|
+
status: `${cachedResult.status}_cache`,
|
|
2950
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
try {
|
|
2954
|
+
const searchResponse = await fetch(`https://registry.npmjs.org/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=10`);
|
|
2955
|
+
if (!searchResponse.ok) {
|
|
2956
|
+
const errorResult = {
|
|
2957
|
+
packageInput: pkgInput,
|
|
2958
|
+
packageName: originalPackageName,
|
|
2959
|
+
status: 'error',
|
|
2960
|
+
error: `Failed to search for alternatives: ${searchResponse.status} ${searchResponse.statusText}`,
|
|
2961
|
+
data: null,
|
|
2962
|
+
message: 'Could not perform search for alternatives.',
|
|
2963
|
+
};
|
|
2964
|
+
return errorResult; // Do not cache API errors for search
|
|
2965
|
+
}
|
|
2966
|
+
const searchData = (await searchResponse.json());
|
|
2967
|
+
const alternativePackagesRaw = searchData.objects || [];
|
|
2968
|
+
let originalPackageDownloads = 0;
|
|
1681
2969
|
try {
|
|
1682
|
-
const
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2970
|
+
const dlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
|
|
2971
|
+
if (dlResponse.ok) {
|
|
2972
|
+
originalPackageDownloads =
|
|
2973
|
+
(await dlResponse.json()).downloads || 0;
|
|
2974
|
+
}
|
|
1687
2975
|
}
|
|
1688
|
-
catch (
|
|
1689
|
-
console.
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2976
|
+
catch (e) {
|
|
2977
|
+
console.debug(`Failed to fetch downloads for original package ${originalPackageName}: ${e}`);
|
|
2978
|
+
}
|
|
2979
|
+
const originalPackageKeywords = alternativePackagesRaw.find((p) => p.package.name === originalPackageName)?.package
|
|
2980
|
+
.keywords || [];
|
|
2981
|
+
const originalPackageStats = {
|
|
2982
|
+
name: originalPackageName,
|
|
2983
|
+
monthlyDownloads: originalPackageDownloads,
|
|
2984
|
+
keywords: originalPackageKeywords,
|
|
2985
|
+
};
|
|
2986
|
+
if (alternativePackagesRaw.length === 0 ||
|
|
2987
|
+
(alternativePackagesRaw.length === 1 &&
|
|
2988
|
+
alternativePackagesRaw[0].package.name === originalPackageName)) {
|
|
2989
|
+
const resultNoAlternatives = {
|
|
2990
|
+
packageInput: pkgInput,
|
|
2991
|
+
packageName: originalPackageName,
|
|
2992
|
+
status: 'no_alternatives_found',
|
|
2993
|
+
error: null,
|
|
2994
|
+
data: { originalPackageStats, alternatives: [] },
|
|
2995
|
+
message: `No significant alternatives found for ${originalPackageName} based on keyword search.`,
|
|
2996
|
+
};
|
|
2997
|
+
cacheSet(cacheKey, resultNoAlternatives, CACHE_TTL_MEDIUM);
|
|
2998
|
+
return resultNoAlternatives;
|
|
2999
|
+
}
|
|
3000
|
+
const alternativesData = await Promise.all(alternativePackagesRaw
|
|
3001
|
+
.filter((alt) => alt.package.name !== originalPackageName)
|
|
3002
|
+
.slice(0, 5)
|
|
3003
|
+
.map(async (alt) => {
|
|
3004
|
+
let altDownloads = 0;
|
|
3005
|
+
try {
|
|
3006
|
+
const altDlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
|
|
3007
|
+
if (altDlResponse.ok) {
|
|
3008
|
+
altDownloads = (await altDlResponse.json()).downloads || 0;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
catch (e) {
|
|
3012
|
+
console.debug(`Failed to fetch downloads for alternative ${alt.package.name}: ${e}`);
|
|
3013
|
+
}
|
|
3014
|
+
return {
|
|
3015
|
+
name: alt.package.name,
|
|
3016
|
+
description: alt.package.description || null,
|
|
3017
|
+
version: alt.package.version,
|
|
3018
|
+
monthlyDownloads: altDownloads,
|
|
3019
|
+
score: alt.score.final,
|
|
3020
|
+
repositoryUrl: alt.package.links?.repository || null,
|
|
3021
|
+
keywords: alt.package.keywords || [],
|
|
3022
|
+
};
|
|
3023
|
+
}));
|
|
3024
|
+
const successResult = {
|
|
3025
|
+
packageInput: pkgInput,
|
|
3026
|
+
packageName: originalPackageName,
|
|
3027
|
+
status: 'success',
|
|
3028
|
+
error: null,
|
|
3029
|
+
data: {
|
|
3030
|
+
originalPackageStats: originalPackageStats,
|
|
3031
|
+
alternatives: alternativesData,
|
|
3032
|
+
},
|
|
3033
|
+
message: `Found ${alternativesData.length} alternative(s) for ${originalPackageName}.`,
|
|
3034
|
+
};
|
|
3035
|
+
cacheSet(cacheKey, successResult, CACHE_TTL_MEDIUM);
|
|
3036
|
+
return successResult;
|
|
3037
|
+
}
|
|
3038
|
+
catch (error) {
|
|
3039
|
+
const errorResult = {
|
|
3040
|
+
packageInput: pkgInput,
|
|
3041
|
+
packageName: originalPackageName,
|
|
3042
|
+
status: 'error',
|
|
3043
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
3044
|
+
data: null,
|
|
3045
|
+
message: `An unexpected error occurred while finding alternatives for ${originalPackageName}.`,
|
|
3046
|
+
};
|
|
3047
|
+
return errorResult; // Do not cache general errors
|
|
3048
|
+
}
|
|
1719
3049
|
}));
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
}
|
|
1724
|
-
|
|
3050
|
+
const finalResponse = {
|
|
3051
|
+
queryPackages: args.packages,
|
|
3052
|
+
results: processedResults,
|
|
3053
|
+
};
|
|
3054
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
3055
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1725
3056
|
}
|
|
1726
3057
|
catch (error) {
|
|
3058
|
+
const errorResponse = JSON.stringify({
|
|
3059
|
+
queryPackages: args.packages,
|
|
3060
|
+
results: [],
|
|
3061
|
+
error: `General error finding alternatives: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
3062
|
+
}, null, 2);
|
|
1727
3063
|
return {
|
|
1728
|
-
content: [
|
|
1729
|
-
{
|
|
1730
|
-
type: 'text',
|
|
1731
|
-
text: `Error finding alternatives: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1732
|
-
},
|
|
1733
|
-
],
|
|
3064
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1734
3065
|
isError: true,
|
|
1735
3066
|
};
|
|
1736
3067
|
}
|
|
1737
3068
|
}
|
|
1738
3069
|
// Create server instance
|
|
1739
3070
|
const server = new McpServer({
|
|
1740
|
-
name: '
|
|
1741
|
-
version: '1.
|
|
3071
|
+
name: 'npm-sentinel-mcp',
|
|
3072
|
+
version: '1.6.1',
|
|
1742
3073
|
});
|
|
1743
3074
|
// Add NPM tools
|
|
1744
3075
|
server.tool('npmVersions', 'Get all available versions of an NPM package', {
|
|
@@ -1867,4 +3198,16 @@ process.on('unhandledRejection', (error) => {
|
|
|
1867
3198
|
server.close();
|
|
1868
3199
|
process.exit(1);
|
|
1869
3200
|
});
|
|
3201
|
+
// Type guard for NpmPackageVersionSchema
|
|
3202
|
+
function isNpmPackageVersionData(data) {
|
|
3203
|
+
try {
|
|
3204
|
+
// Use safeParse for type guards to avoid throwing errors on invalid data
|
|
3205
|
+
return NpmPackageVersionSchema.safeParse(data).success;
|
|
3206
|
+
}
|
|
3207
|
+
catch (e) {
|
|
3208
|
+
// This catch block might not be strictly necessary with safeParse but kept for safety
|
|
3209
|
+
// console.error("isNpmPackageVersionData validation failed unexpectedly:", e);
|
|
3210
|
+
return false;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
1870
3213
|
//# sourceMappingURL=index.js.map
|