@nekzus/mcp-server 1.5.6 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +393 -72
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2579 -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,1845 @@ 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
|
+
};
|
|
1403
|
+
}
|
|
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
|
+
};
|
|
1102
1415
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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+$/) ? CACHE_TTL_SHORT : CACHE_TTL_LONG;
|
|
1458
|
+
cacheSet(cacheKey, qualityData, ttl);
|
|
1459
|
+
return {
|
|
1460
|
+
packageInput: pkgInput,
|
|
1461
|
+
packageName: name,
|
|
1462
|
+
status: 'success',
|
|
1463
|
+
error: null,
|
|
1464
|
+
data: qualityData,
|
|
1465
|
+
message: `Successfully fetched quality score for ${name} (version analyzed: ${collected.metadata.version}).`,
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
catch (error) {
|
|
1469
|
+
return {
|
|
1470
|
+
packageInput: pkgInput,
|
|
1471
|
+
packageName: name,
|
|
1472
|
+
status: 'error',
|
|
1473
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1474
|
+
data: null,
|
|
1475
|
+
message: `An unexpected error occurred while processing quality for ${name}.`,
|
|
1476
|
+
};
|
|
1106
1477
|
}
|
|
1107
|
-
return {
|
|
1108
|
-
name: pkg,
|
|
1109
|
-
maintainers: data.maintainers || [],
|
|
1110
|
-
};
|
|
1111
1478
|
}));
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1479
|
+
const finalResponse = {
|
|
1480
|
+
queryPackages: args.packages,
|
|
1481
|
+
results: processedResults,
|
|
1482
|
+
};
|
|
1483
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1484
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1485
|
+
}
|
|
1486
|
+
catch (error) {
|
|
1487
|
+
const errorResponse = JSON.stringify({
|
|
1488
|
+
queryPackages: args.packages,
|
|
1489
|
+
results: [],
|
|
1490
|
+
error: `General error fetching quality metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1491
|
+
}, null, 2);
|
|
1492
|
+
return {
|
|
1493
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1494
|
+
isError: true,
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
export async function handleNpmMaintenance(args) {
|
|
1499
|
+
try {
|
|
1500
|
+
const packagesToProcess = args.packages || [];
|
|
1501
|
+
if (packagesToProcess.length === 0) {
|
|
1502
|
+
throw new Error('No package names provided to fetch maintenance metrics.');
|
|
1503
|
+
}
|
|
1504
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1505
|
+
let name = '';
|
|
1506
|
+
if (typeof pkgInput === 'string') {
|
|
1507
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1508
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
|
|
1123
1509
|
}
|
|
1124
1510
|
else {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1511
|
+
return {
|
|
1512
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1513
|
+
packageName: 'unknown_package_input',
|
|
1514
|
+
status: 'error',
|
|
1515
|
+
error: 'Invalid package input type',
|
|
1516
|
+
data: null,
|
|
1517
|
+
message: 'Package input was not a string.',
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
if (!name) {
|
|
1521
|
+
return {
|
|
1522
|
+
packageInput: pkgInput,
|
|
1523
|
+
packageName: 'empty_package_name',
|
|
1524
|
+
status: 'error',
|
|
1525
|
+
error: 'Empty package name derived from input',
|
|
1526
|
+
data: null,
|
|
1527
|
+
message: 'Package name could not be determined from input.',
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
const cacheKey = generateCacheKey('handleNpmMaintenance', name);
|
|
1531
|
+
const cachedData = cacheGet(cacheKey);
|
|
1532
|
+
if (cachedData) {
|
|
1533
|
+
return {
|
|
1534
|
+
packageInput: pkgInput,
|
|
1535
|
+
packageName: name,
|
|
1536
|
+
status: 'success_cache',
|
|
1537
|
+
error: null,
|
|
1538
|
+
data: cachedData,
|
|
1539
|
+
message: `Maintenance score for ${name} (version analyzed: ${cachedData.versionInScore}) from cache.`,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`, {
|
|
1544
|
+
headers: {
|
|
1545
|
+
Accept: 'application/json',
|
|
1546
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
if (!response.ok) {
|
|
1550
|
+
let errorMsg = `Failed to fetch maintenance data: ${response.status} ${response.statusText}`;
|
|
1551
|
+
if (response.status === 404) {
|
|
1552
|
+
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1553
|
+
}
|
|
1554
|
+
return {
|
|
1555
|
+
packageInput: pkgInput,
|
|
1556
|
+
packageName: name,
|
|
1557
|
+
status: 'error',
|
|
1558
|
+
error: errorMsg,
|
|
1559
|
+
data: null,
|
|
1560
|
+
message: `Could not retrieve maintenance information for ${name}.`,
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
const rawData = await response.json();
|
|
1564
|
+
if (!isValidNpmsResponse(rawData)) {
|
|
1565
|
+
return {
|
|
1566
|
+
packageInput: pkgInput,
|
|
1567
|
+
packageName: name,
|
|
1568
|
+
status: 'error',
|
|
1569
|
+
error: 'Invalid or incomplete response from npms.io API for maintenance data',
|
|
1570
|
+
data: null,
|
|
1571
|
+
message: `Received malformed maintenance data for ${name}.`,
|
|
1572
|
+
};
|
|
1129
1573
|
}
|
|
1574
|
+
const { score, collected, analyzedAt } = rawData;
|
|
1575
|
+
const maintenanceScoreValue = score.detail.maintenance;
|
|
1576
|
+
const maintenanceData = {
|
|
1577
|
+
analyzedAt: analyzedAt,
|
|
1578
|
+
versionInScore: collected.metadata.version,
|
|
1579
|
+
maintenanceScore: maintenanceScoreValue,
|
|
1580
|
+
};
|
|
1581
|
+
const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/) ? CACHE_TTL_SHORT : CACHE_TTL_LONG;
|
|
1582
|
+
cacheSet(cacheKey, maintenanceData, ttl);
|
|
1583
|
+
return {
|
|
1584
|
+
packageInput: pkgInput,
|
|
1585
|
+
packageName: name,
|
|
1586
|
+
status: 'success',
|
|
1587
|
+
error: null,
|
|
1588
|
+
data: maintenanceData,
|
|
1589
|
+
message: `Successfully fetched maintenance score for ${name} (version analyzed: ${collected.metadata.version}).`,
|
|
1590
|
+
};
|
|
1130
1591
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
return {
|
|
1594
|
+
packageInput: pkgInput,
|
|
1595
|
+
packageName: name,
|
|
1596
|
+
status: 'error',
|
|
1597
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1598
|
+
data: null,
|
|
1599
|
+
message: `An unexpected error occurred while processing maintenance for ${name}.`,
|
|
1600
|
+
};
|
|
1133
1601
|
}
|
|
1134
|
-
}
|
|
1602
|
+
}));
|
|
1603
|
+
const finalResponse = {
|
|
1604
|
+
queryPackages: args.packages,
|
|
1605
|
+
results: processedResults,
|
|
1606
|
+
};
|
|
1607
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1608
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1609
|
+
}
|
|
1610
|
+
catch (error) {
|
|
1611
|
+
const errorResponse = JSON.stringify({
|
|
1612
|
+
queryPackages: args.packages,
|
|
1613
|
+
results: [],
|
|
1614
|
+
error: `General error fetching maintenance metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1615
|
+
}, null, 2);
|
|
1135
1616
|
return {
|
|
1136
|
-
content: [
|
|
1137
|
-
|
|
1138
|
-
type: 'text',
|
|
1139
|
-
text,
|
|
1140
|
-
},
|
|
1141
|
-
],
|
|
1142
|
-
isError: false,
|
|
1617
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1618
|
+
isError: true,
|
|
1143
1619
|
};
|
|
1144
1620
|
}
|
|
1621
|
+
}
|
|
1622
|
+
export async function handleNpmMaintainers(args) {
|
|
1623
|
+
try {
|
|
1624
|
+
const packagesToProcess = args.packages || [];
|
|
1625
|
+
if (packagesToProcess.length === 0) {
|
|
1626
|
+
throw new Error('No package names provided to fetch maintainers.');
|
|
1627
|
+
}
|
|
1628
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1629
|
+
let name = '';
|
|
1630
|
+
if (typeof pkgInput === 'string') {
|
|
1631
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1632
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version is ignored for maintainers
|
|
1633
|
+
}
|
|
1634
|
+
else {
|
|
1635
|
+
return {
|
|
1636
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1637
|
+
packageName: 'unknown_package_input',
|
|
1638
|
+
status: 'error',
|
|
1639
|
+
error: 'Invalid package input type',
|
|
1640
|
+
data: null,
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
if (!name) {
|
|
1644
|
+
return {
|
|
1645
|
+
packageInput: pkgInput,
|
|
1646
|
+
packageName: 'empty_package_name',
|
|
1647
|
+
status: 'error',
|
|
1648
|
+
error: 'Empty package name derived from input',
|
|
1649
|
+
data: null,
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
const cacheKey = generateCacheKey('handleNpmMaintainers', name);
|
|
1653
|
+
const cachedData = cacheGet(cacheKey);
|
|
1654
|
+
if (cachedData) {
|
|
1655
|
+
return {
|
|
1656
|
+
packageInput: pkgInput,
|
|
1657
|
+
packageName: name,
|
|
1658
|
+
status: 'success_cache',
|
|
1659
|
+
error: null,
|
|
1660
|
+
data: cachedData,
|
|
1661
|
+
message: `Maintainer information for ${name} from cache.`,
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`);
|
|
1666
|
+
if (!response.ok) {
|
|
1667
|
+
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
1668
|
+
if (response.status === 404) {
|
|
1669
|
+
errorMsg = `Package ${name} not found in the npm registry.`;
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
packageInput: pkgInput,
|
|
1673
|
+
packageName: name,
|
|
1674
|
+
status: 'error',
|
|
1675
|
+
error: errorMsg,
|
|
1676
|
+
data: null,
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
const data = await response.json();
|
|
1680
|
+
if (!isNpmPackageInfo(data)) {
|
|
1681
|
+
// Using NpmPackageInfoSchema as it contains maintainers
|
|
1682
|
+
return {
|
|
1683
|
+
packageInput: pkgInput,
|
|
1684
|
+
packageName: name,
|
|
1685
|
+
status: 'error',
|
|
1686
|
+
error: 'Invalid package info data received from registry',
|
|
1687
|
+
data: null,
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
const maintainers = (data.maintainers || []).map((m) => ({
|
|
1691
|
+
name: m.name,
|
|
1692
|
+
email: m.email || null, // Ensure email is null if not present
|
|
1693
|
+
url: m.url || null, // NpmMaintainerSchema has url optional
|
|
1694
|
+
}));
|
|
1695
|
+
const maintainersData = {
|
|
1696
|
+
maintainers: maintainers,
|
|
1697
|
+
maintainersCount: maintainers.length,
|
|
1698
|
+
};
|
|
1699
|
+
cacheSet(cacheKey, maintainersData, CACHE_TTL_VERY_LONG);
|
|
1700
|
+
return {
|
|
1701
|
+
packageInput: pkgInput,
|
|
1702
|
+
packageName: name,
|
|
1703
|
+
status: 'success',
|
|
1704
|
+
error: null,
|
|
1705
|
+
data: maintainersData,
|
|
1706
|
+
message: `Successfully fetched maintainer information for ${name}.`,
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
catch (error) {
|
|
1710
|
+
return {
|
|
1711
|
+
packageInput: pkgInput,
|
|
1712
|
+
packageName: name,
|
|
1713
|
+
status: 'error',
|
|
1714
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1715
|
+
data: null,
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
}));
|
|
1719
|
+
const finalResponse = {
|
|
1720
|
+
queryPackages: args.packages,
|
|
1721
|
+
results: processedResults,
|
|
1722
|
+
message: `Maintainer information for ${args.packages.length} package(s).`,
|
|
1723
|
+
};
|
|
1724
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1725
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1726
|
+
}
|
|
1145
1727
|
catch (error) {
|
|
1728
|
+
const errorResponse = JSON.stringify({
|
|
1729
|
+
queryPackages: args.packages,
|
|
1730
|
+
results: [],
|
|
1731
|
+
error: `General error fetching maintainer information: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1732
|
+
}, null, 2);
|
|
1146
1733
|
return {
|
|
1147
|
-
content: [
|
|
1148
|
-
{
|
|
1149
|
-
type: 'text',
|
|
1150
|
-
text: `Error fetching package maintainers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1151
|
-
},
|
|
1152
|
-
],
|
|
1734
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1153
1735
|
isError: true,
|
|
1154
1736
|
};
|
|
1155
1737
|
}
|
|
1156
1738
|
}
|
|
1157
1739
|
export async function handleNpmScore(args) {
|
|
1158
1740
|
try {
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1741
|
+
const packagesToProcess = args.packages || [];
|
|
1742
|
+
if (packagesToProcess.length === 0) {
|
|
1743
|
+
throw new Error('No package names provided to fetch scores.');
|
|
1744
|
+
}
|
|
1745
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1746
|
+
let name = '';
|
|
1747
|
+
if (typeof pkgInput === 'string') {
|
|
1748
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1749
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version is ignored by npms.io API endpoint
|
|
1750
|
+
}
|
|
1751
|
+
else {
|
|
1162
1752
|
return {
|
|
1163
|
-
|
|
1164
|
-
|
|
1753
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1754
|
+
packageName: 'unknown_package_input',
|
|
1755
|
+
status: 'error',
|
|
1756
|
+
error: 'Invalid package input type',
|
|
1757
|
+
data: null,
|
|
1165
1758
|
};
|
|
1166
1759
|
}
|
|
1167
|
-
if (!
|
|
1168
|
-
|
|
1760
|
+
if (!name) {
|
|
1761
|
+
return {
|
|
1762
|
+
packageInput: pkgInput,
|
|
1763
|
+
packageName: 'empty_package_name',
|
|
1764
|
+
status: 'error',
|
|
1765
|
+
error: 'Empty package name derived from input',
|
|
1766
|
+
data: null,
|
|
1767
|
+
};
|
|
1169
1768
|
}
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1769
|
+
const cacheKey = generateCacheKey('handleNpmScore', name);
|
|
1770
|
+
const cachedData = cacheGet(cacheKey);
|
|
1771
|
+
if (cachedData) {
|
|
1172
1772
|
return {
|
|
1173
|
-
|
|
1174
|
-
|
|
1773
|
+
packageInput: pkgInput,
|
|
1774
|
+
packageName: name,
|
|
1775
|
+
status: 'success_cache',
|
|
1776
|
+
error: null,
|
|
1777
|
+
data: cachedData,
|
|
1778
|
+
message: `Score data for ${name} (version analyzed: ${cachedData.versionInScore}) from cache.`,
|
|
1175
1779
|
};
|
|
1176
1780
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1781
|
+
try {
|
|
1782
|
+
const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`);
|
|
1783
|
+
if (!response.ok) {
|
|
1784
|
+
let errorMsg = `Failed to fetch package score: ${response.status} ${response.statusText}`;
|
|
1785
|
+
if (response.status === 404) {
|
|
1786
|
+
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1787
|
+
}
|
|
1788
|
+
return {
|
|
1789
|
+
packageInput: pkgInput,
|
|
1790
|
+
packageName: name,
|
|
1791
|
+
status: 'error',
|
|
1792
|
+
error: errorMsg,
|
|
1793
|
+
data: null,
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
const rawData = await response.json();
|
|
1797
|
+
if (!isValidNpmsResponse(rawData)) {
|
|
1798
|
+
return {
|
|
1799
|
+
packageInput: pkgInput,
|
|
1800
|
+
packageName: name,
|
|
1801
|
+
status: 'error',
|
|
1802
|
+
error: 'Invalid or incomplete response from npms.io API',
|
|
1803
|
+
data: null,
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
const { score, collected, analyzedAt } = rawData;
|
|
1807
|
+
const { detail } = score;
|
|
1808
|
+
// Calculate total downloads for the last month from the typically first entry in downloads array
|
|
1809
|
+
const lastMonthDownloads = collected.npm?.downloads?.find((d) => {
|
|
1810
|
+
// Heuristic: find a download period that is roughly 30 days
|
|
1811
|
+
const from = new Date(d.from);
|
|
1812
|
+
const to = new Date(d.to);
|
|
1813
|
+
const diffTime = Math.abs(to.getTime() - from.getTime());
|
|
1814
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
1815
|
+
return diffDays >= 28 && diffDays <= 31; // Common range for monthly data
|
|
1816
|
+
})?.count ||
|
|
1817
|
+
collected.npm?.downloads?.[0]?.count ||
|
|
1818
|
+
0;
|
|
1819
|
+
const scoreData = {
|
|
1820
|
+
analyzedAt: analyzedAt,
|
|
1821
|
+
versionInScore: collected.metadata.version,
|
|
1822
|
+
score: {
|
|
1823
|
+
final: score.final,
|
|
1824
|
+
detail: {
|
|
1825
|
+
quality: detail.quality,
|
|
1826
|
+
popularity: detail.popularity,
|
|
1827
|
+
maintenance: detail.maintenance,
|
|
1828
|
+
},
|
|
1829
|
+
},
|
|
1830
|
+
packageInfoFromScore: {
|
|
1831
|
+
name: collected.metadata.name,
|
|
1832
|
+
version: collected.metadata.version,
|
|
1833
|
+
description: collected.metadata.description || null,
|
|
1834
|
+
},
|
|
1835
|
+
npmStats: {
|
|
1836
|
+
downloadsLastMonth: lastMonthDownloads,
|
|
1837
|
+
starsCount: collected.npm.starsCount,
|
|
1838
|
+
},
|
|
1839
|
+
githubStats: collected.github
|
|
1840
|
+
? {
|
|
1841
|
+
starsCount: collected.github.starsCount,
|
|
1842
|
+
forksCount: collected.github.forksCount,
|
|
1843
|
+
subscribersCount: collected.github.subscribersCount,
|
|
1844
|
+
issues: {
|
|
1845
|
+
count: collected.github.issues.count,
|
|
1846
|
+
openCount: collected.github.issues.openCount,
|
|
1847
|
+
},
|
|
1848
|
+
}
|
|
1849
|
+
: null,
|
|
1850
|
+
};
|
|
1851
|
+
const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/) ? CACHE_TTL_SHORT : CACHE_TTL_LONG;
|
|
1852
|
+
cacheSet(cacheKey, scoreData, ttl);
|
|
1853
|
+
return {
|
|
1854
|
+
packageInput: pkgInput,
|
|
1855
|
+
packageName: name,
|
|
1856
|
+
status: 'success',
|
|
1857
|
+
error: null,
|
|
1858
|
+
data: scoreData,
|
|
1859
|
+
message: `Successfully fetched score data for ${name} (version analyzed: ${collected.metadata.version}).`,
|
|
1860
|
+
};
|
|
1214
1861
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1862
|
+
catch (error) {
|
|
1863
|
+
return {
|
|
1864
|
+
packageInput: pkgInput,
|
|
1865
|
+
packageName: name,
|
|
1866
|
+
status: 'error',
|
|
1867
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
1868
|
+
data: null,
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
}));
|
|
1872
|
+
const finalResponse = {
|
|
1873
|
+
queryPackages: args.packages,
|
|
1874
|
+
results: processedResults,
|
|
1875
|
+
message: `Score information for ${args.packages.length} package(s).`,
|
|
1225
1876
|
};
|
|
1877
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1878
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1226
1879
|
}
|
|
1227
1880
|
catch (error) {
|
|
1228
|
-
|
|
1881
|
+
const errorResponse = JSON.stringify({
|
|
1882
|
+
queryPackages: args.packages,
|
|
1883
|
+
results: [],
|
|
1884
|
+
error: `General error fetching package scores: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1885
|
+
}, null, 2);
|
|
1229
1886
|
return {
|
|
1230
|
-
content: [
|
|
1231
|
-
{
|
|
1232
|
-
type: 'text',
|
|
1233
|
-
text: `Error fetching package scores: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1234
|
-
},
|
|
1235
|
-
],
|
|
1887
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1236
1888
|
isError: true,
|
|
1237
1889
|
};
|
|
1238
1890
|
}
|
|
1239
1891
|
}
|
|
1240
1892
|
export async function handleNpmPackageReadme(args) {
|
|
1241
1893
|
try {
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1894
|
+
const packagesToProcess = args.packages || [];
|
|
1895
|
+
if (packagesToProcess.length === 0) {
|
|
1896
|
+
throw new Error('No package names provided to fetch READMEs.');
|
|
1897
|
+
}
|
|
1898
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
1899
|
+
let name = '';
|
|
1900
|
+
let versionTag = undefined; // Explicitly undefined if not specified
|
|
1901
|
+
if (typeof pkgInput === 'string') {
|
|
1902
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
1903
|
+
if (atIdx > 0) {
|
|
1904
|
+
name = pkgInput.slice(0, atIdx);
|
|
1905
|
+
versionTag = pkgInput.slice(atIdx + 1);
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
name = pkgInput;
|
|
1909
|
+
versionTag = 'latest'; // Default to latest if no version specified
|
|
1910
|
+
}
|
|
1246
1911
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1912
|
+
else {
|
|
1913
|
+
return {
|
|
1914
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1915
|
+
packageName: 'unknown_package_input',
|
|
1916
|
+
versionQueried: versionTag,
|
|
1917
|
+
versionFetched: null,
|
|
1918
|
+
status: 'error',
|
|
1919
|
+
error: 'Invalid package input type',
|
|
1920
|
+
data: null,
|
|
1921
|
+
};
|
|
1250
1922
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1923
|
+
if (!name) {
|
|
1924
|
+
return {
|
|
1925
|
+
packageInput: pkgInput,
|
|
1926
|
+
packageName: 'empty_package_name',
|
|
1927
|
+
versionQueried: versionTag,
|
|
1928
|
+
versionFetched: null,
|
|
1929
|
+
status: 'error',
|
|
1930
|
+
error: 'Empty package name derived from input',
|
|
1931
|
+
data: null,
|
|
1932
|
+
};
|
|
1254
1933
|
}
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1934
|
+
const cacheKey = generateCacheKey('handleNpmPackageReadme', name, versionTag);
|
|
1935
|
+
const cachedData = cacheGet(cacheKey);
|
|
1936
|
+
if (cachedData) {
|
|
1937
|
+
return {
|
|
1938
|
+
packageInput: pkgInput,
|
|
1939
|
+
packageName: name,
|
|
1940
|
+
versionQueried: versionTag,
|
|
1941
|
+
versionFetched: cachedData.versionFetched, // Retrieve stored fetched version
|
|
1942
|
+
status: 'success_cache',
|
|
1943
|
+
error: null,
|
|
1944
|
+
data: { readme: cachedData.readme, hasReadme: cachedData.hasReadme },
|
|
1945
|
+
message: `README for ${name}@${cachedData.versionFetched} from cache.`,
|
|
1946
|
+
};
|
|
1258
1947
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1948
|
+
try {
|
|
1949
|
+
const response = await fetch(`https://registry.npmjs.org/${name}`);
|
|
1950
|
+
if (!response.ok) {
|
|
1951
|
+
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
1952
|
+
if (response.status === 404) {
|
|
1953
|
+
errorMsg = `Package ${name} not found.`;
|
|
1954
|
+
}
|
|
1955
|
+
return {
|
|
1956
|
+
packageInput: pkgInput,
|
|
1957
|
+
packageName: name,
|
|
1958
|
+
versionQueried: versionTag,
|
|
1959
|
+
versionFetched: null,
|
|
1960
|
+
status: 'error',
|
|
1961
|
+
error: errorMsg,
|
|
1962
|
+
data: null,
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
const packageInfo = await response.json();
|
|
1966
|
+
if (!isNpmPackageInfo(packageInfo)) {
|
|
1967
|
+
return {
|
|
1968
|
+
packageInput: pkgInput,
|
|
1969
|
+
packageName: name,
|
|
1970
|
+
versionQueried: versionTag,
|
|
1971
|
+
versionFetched: null,
|
|
1972
|
+
status: 'error',
|
|
1973
|
+
error: 'Invalid package info data received',
|
|
1974
|
+
data: null,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
const versionToUse = versionTag === 'latest' ? packageInfo['dist-tags']?.latest : versionTag;
|
|
1978
|
+
if (!versionToUse || !packageInfo.versions || !packageInfo.versions[versionToUse]) {
|
|
1979
|
+
return {
|
|
1980
|
+
packageInput: pkgInput,
|
|
1981
|
+
packageName: name,
|
|
1982
|
+
versionQueried: versionTag,
|
|
1983
|
+
versionFetched: versionToUse || null,
|
|
1984
|
+
status: 'error',
|
|
1985
|
+
error: `Version ${versionToUse || 'requested'} not found or no version data available.`,
|
|
1986
|
+
data: null,
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
const versionData = packageInfo.versions[versionToUse];
|
|
1990
|
+
// README can be in version-specific data or at the root of packageInfo
|
|
1991
|
+
const readmeContent = versionData.readme || packageInfo.readme || null;
|
|
1992
|
+
const hasReadme = !!readmeContent;
|
|
1993
|
+
const readmeResultData = {
|
|
1994
|
+
readme: readmeContent,
|
|
1995
|
+
hasReadme: hasReadme,
|
|
1996
|
+
versionFetched: versionToUse, // Store the actually fetched version
|
|
1997
|
+
};
|
|
1998
|
+
cacheSet(cacheKey, readmeResultData, CACHE_TTL_LONG);
|
|
1999
|
+
return {
|
|
2000
|
+
packageInput: pkgInput,
|
|
2001
|
+
packageName: name,
|
|
2002
|
+
versionQueried: versionTag,
|
|
2003
|
+
versionFetched: versionToUse,
|
|
2004
|
+
status: 'success',
|
|
2005
|
+
error: null,
|
|
2006
|
+
data: { readme: readmeContent, hasReadme: hasReadme }, // Return only readme and hasReadme in data field for consistency
|
|
2007
|
+
message: `Successfully fetched README for ${name}@${versionToUse}.`,
|
|
2008
|
+
};
|
|
1270
2009
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
2010
|
+
catch (error) {
|
|
2011
|
+
return {
|
|
2012
|
+
packageInput: pkgInput,
|
|
2013
|
+
packageName: name,
|
|
2014
|
+
versionQueried: versionTag,
|
|
2015
|
+
versionFetched: null,
|
|
2016
|
+
status: 'error',
|
|
2017
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2018
|
+
data: null,
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
}));
|
|
2022
|
+
const finalResponse = {
|
|
2023
|
+
queryPackages: args.packages,
|
|
2024
|
+
results: processedResults,
|
|
2025
|
+
message: `README fetching status for ${args.packages.length} package(s).`,
|
|
2026
|
+
};
|
|
2027
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2028
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1273
2029
|
}
|
|
1274
2030
|
catch (error) {
|
|
2031
|
+
const errorResponse = JSON.stringify({
|
|
2032
|
+
queryPackages: args.packages,
|
|
2033
|
+
results: [],
|
|
2034
|
+
error: `General error fetching READMEs: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2035
|
+
}, null, 2);
|
|
1275
2036
|
return {
|
|
1276
|
-
content: [
|
|
1277
|
-
{
|
|
1278
|
-
type: 'text',
|
|
1279
|
-
text: `Error fetching READMEs: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1280
|
-
},
|
|
1281
|
-
],
|
|
2037
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1282
2038
|
isError: true,
|
|
1283
2039
|
};
|
|
1284
2040
|
}
|
|
1285
2041
|
}
|
|
1286
2042
|
export async function handleNpmSearch(args) {
|
|
1287
2043
|
try {
|
|
2044
|
+
const query = args.query;
|
|
1288
2045
|
const limit = args.limit || 10;
|
|
1289
|
-
|
|
2046
|
+
if (limit < 1 || limit > 250) {
|
|
2047
|
+
// NPM API search limit is typically 250
|
|
2048
|
+
throw new Error('Limit must be between 1 and 250.');
|
|
2049
|
+
}
|
|
2050
|
+
const cacheKey = generateCacheKey('handleNpmSearch', query, limit);
|
|
2051
|
+
const cachedData = cacheGet(cacheKey);
|
|
2052
|
+
if (cachedData) {
|
|
2053
|
+
const cachedResponseJson = JSON.stringify(cachedData, null, 2);
|
|
2054
|
+
return { content: [{ type: 'text', text: cachedResponseJson }], isError: false, message: `Search results for query '${query}' with limit ${limit} from cache.` };
|
|
2055
|
+
}
|
|
2056
|
+
const response = await fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
|
|
1290
2057
|
if (!response.ok) {
|
|
1291
|
-
throw new Error(`Failed to search packages: ${response.statusText}`);
|
|
2058
|
+
throw new Error(`Failed to search packages: ${response.status} ${response.statusText}`);
|
|
1292
2059
|
}
|
|
1293
2060
|
const rawData = await response.json();
|
|
1294
2061
|
const parseResult = NpmSearchResultSchema.safeParse(rawData);
|
|
1295
2062
|
if (!parseResult.success) {
|
|
1296
|
-
|
|
2063
|
+
console.error('Invalid search results data received:', parseResult.error.issues);
|
|
2064
|
+
throw new Error('Invalid search results data received from NPM registry.');
|
|
1297
2065
|
}
|
|
1298
2066
|
const { objects, total } = parseResult.data;
|
|
1299
|
-
|
|
1300
|
-
text += `Found ${total.toLocaleString()} packages (showing top ${limit})\n\n`;
|
|
1301
|
-
for (const result of objects) {
|
|
2067
|
+
const resultsData = objects.map((result) => {
|
|
1302
2068
|
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
|
-
|
|
2069
|
+
const scoreDetail = result.score.detail;
|
|
2070
|
+
return {
|
|
2071
|
+
name: pkg.name,
|
|
2072
|
+
version: pkg.version,
|
|
2073
|
+
description: pkg.description || null,
|
|
2074
|
+
keywords: pkg.keywords || [],
|
|
2075
|
+
publisher: pkg.publisher
|
|
2076
|
+
? { username: pkg.publisher.username, email: pkg.publisher.email || null }
|
|
2077
|
+
: null, // publisher might not have email
|
|
2078
|
+
date: pkg.date || null,
|
|
2079
|
+
links: {
|
|
2080
|
+
npm: pkg.links?.npm || null,
|
|
2081
|
+
homepage: pkg.links?.homepage || null,
|
|
2082
|
+
repository: pkg.links?.repository || null,
|
|
2083
|
+
bugs: pkg.links?.bugs || null, // NpmSearchResultSchema needs to be updated if bugs is not there
|
|
2084
|
+
},
|
|
2085
|
+
score: {
|
|
2086
|
+
final: result.score.final,
|
|
2087
|
+
detail: {
|
|
2088
|
+
quality: scoreDetail.quality,
|
|
2089
|
+
popularity: scoreDetail.popularity,
|
|
2090
|
+
maintenance: scoreDetail.maintenance,
|
|
2091
|
+
},
|
|
2092
|
+
},
|
|
2093
|
+
searchScore: result.searchScore,
|
|
2094
|
+
};
|
|
2095
|
+
});
|
|
2096
|
+
const finalResponse = {
|
|
2097
|
+
query: query,
|
|
2098
|
+
limitUsed: limit,
|
|
2099
|
+
totalResults: total,
|
|
2100
|
+
resultsCount: resultsData.length,
|
|
2101
|
+
results: resultsData,
|
|
2102
|
+
message: `Search completed. Found ${total} total packages, returning ${resultsData.length}.`,
|
|
2103
|
+
};
|
|
2104
|
+
cacheSet(cacheKey, finalResponse, CACHE_TTL_MEDIUM);
|
|
2105
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2106
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1326
2107
|
}
|
|
1327
2108
|
catch (error) {
|
|
2109
|
+
const errorResponse = JSON.stringify({
|
|
2110
|
+
query: args.query,
|
|
2111
|
+
limitUsed: args.limit || 10,
|
|
2112
|
+
totalResults: 0,
|
|
2113
|
+
resultsCount: 0,
|
|
2114
|
+
results: [],
|
|
2115
|
+
error: `Error searching packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2116
|
+
}, null, 2);
|
|
1328
2117
|
return {
|
|
1329
|
-
content: [
|
|
1330
|
-
{
|
|
1331
|
-
type: 'text',
|
|
1332
|
-
text: `Error searching packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1333
|
-
},
|
|
1334
|
-
],
|
|
2118
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1335
2119
|
isError: true,
|
|
1336
2120
|
};
|
|
1337
2121
|
}
|
|
@@ -1339,59 +2123,175 @@ export async function handleNpmSearch(args) {
|
|
|
1339
2123
|
// License compatibility checker
|
|
1340
2124
|
export async function handleNpmLicenseCompatibility(args) {
|
|
1341
2125
|
try {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
2126
|
+
const packagesToProcess = args.packages || [];
|
|
2127
|
+
if (packagesToProcess.length === 0) {
|
|
2128
|
+
throw new Error('No package names provided for license compatibility analysis.');
|
|
2129
|
+
}
|
|
2130
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2131
|
+
let name = '';
|
|
2132
|
+
let versionTag = 'latest';
|
|
2133
|
+
if (typeof pkgInput === 'string') {
|
|
2134
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2135
|
+
if (atIdx > 0) {
|
|
2136
|
+
name = pkgInput.slice(0, atIdx);
|
|
2137
|
+
versionTag = pkgInput.slice(atIdx + 1);
|
|
2138
|
+
}
|
|
2139
|
+
else {
|
|
2140
|
+
name = pkgInput;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
else {
|
|
2144
|
+
return {
|
|
2145
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2146
|
+
packageName: 'unknown_package_input',
|
|
2147
|
+
versionQueried: versionTag,
|
|
2148
|
+
versionFetched: null,
|
|
2149
|
+
status: 'error',
|
|
2150
|
+
error: 'Invalid package input type',
|
|
2151
|
+
data: null,
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
if (!name) {
|
|
2155
|
+
return {
|
|
2156
|
+
packageInput: pkgInput,
|
|
2157
|
+
packageName: 'empty_package_name',
|
|
2158
|
+
versionQueried: versionTag,
|
|
2159
|
+
versionFetched: null,
|
|
2160
|
+
status: 'error',
|
|
2161
|
+
error: 'Empty package name derived from input',
|
|
2162
|
+
data: null,
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
const cacheKey = generateCacheKey('npmLicenseInfoForCompatibility', name, versionTag);
|
|
2166
|
+
const cachedLicenseData = cacheGet(cacheKey);
|
|
2167
|
+
if (cachedLicenseData) {
|
|
2168
|
+
return {
|
|
2169
|
+
packageInput: pkgInput,
|
|
2170
|
+
packageName: name,
|
|
2171
|
+
versionQueried: versionTag,
|
|
2172
|
+
versionFetched: cachedLicenseData.versionFetched,
|
|
2173
|
+
status: 'success_cache',
|
|
2174
|
+
error: null,
|
|
2175
|
+
data: { license: cachedLicenseData.license },
|
|
2176
|
+
message: `License info for ${name}@${cachedLicenseData.versionFetched} from cache.`,
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
const response = await fetch(`https://registry.npmjs.org/${name}/${versionTag}`);
|
|
2181
|
+
if (!response.ok) {
|
|
2182
|
+
let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
|
|
2183
|
+
if (response.status === 404) {
|
|
2184
|
+
errorMsg = `Package ${name}@${versionTag} not found.`;
|
|
2185
|
+
}
|
|
2186
|
+
return {
|
|
2187
|
+
packageInput: pkgInput,
|
|
2188
|
+
packageName: name,
|
|
2189
|
+
versionQueried: versionTag,
|
|
2190
|
+
versionFetched: null,
|
|
2191
|
+
status: 'error',
|
|
2192
|
+
error: errorMsg,
|
|
2193
|
+
data: null,
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
const versionData = await response.json();
|
|
2197
|
+
if (!isNpmPackageVersionData(versionData)) {
|
|
2198
|
+
return {
|
|
2199
|
+
packageInput: pkgInput,
|
|
2200
|
+
packageName: name,
|
|
2201
|
+
versionQueried: versionTag,
|
|
2202
|
+
versionFetched: null, // Could use versionData.version if partially valid
|
|
2203
|
+
status: 'error',
|
|
2204
|
+
error: 'Invalid package version data format received',
|
|
2205
|
+
data: null,
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
const licenseInfoToCache = {
|
|
2209
|
+
license: versionData.license || 'UNKNOWN', // Default to UNKNOWN if null/undefined
|
|
2210
|
+
versionFetched: versionData.version,
|
|
2211
|
+
};
|
|
2212
|
+
cacheSet(cacheKey, licenseInfoToCache, CACHE_TTL_VERY_LONG);
|
|
2213
|
+
return {
|
|
2214
|
+
packageInput: pkgInput,
|
|
2215
|
+
packageName: name,
|
|
2216
|
+
versionQueried: versionTag,
|
|
2217
|
+
versionFetched: versionData.version,
|
|
2218
|
+
status: 'success',
|
|
2219
|
+
error: null,
|
|
2220
|
+
data: {
|
|
2221
|
+
license: versionData.license || 'UNKNOWN', // Default to UNKNOWN if null/undefined
|
|
2222
|
+
},
|
|
2223
|
+
message: `Successfully fetched license info for ${name}@${versionData.version}.`,
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
catch (error) {
|
|
2227
|
+
return {
|
|
2228
|
+
packageInput: pkgInput,
|
|
2229
|
+
packageName: name,
|
|
2230
|
+
versionQueried: versionTag,
|
|
2231
|
+
versionFetched: null,
|
|
2232
|
+
status: 'error',
|
|
2233
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2234
|
+
data: null,
|
|
2235
|
+
};
|
|
1346
2236
|
}
|
|
1347
|
-
const data = (await response.json());
|
|
1348
|
-
return {
|
|
1349
|
-
package: pkg,
|
|
1350
|
-
license: data.license || 'UNKNOWN',
|
|
1351
|
-
};
|
|
1352
2237
|
}));
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
2238
|
+
// Perform analysis based on fetched licenses
|
|
2239
|
+
const warnings = [];
|
|
2240
|
+
const licensesFound = processedResults
|
|
2241
|
+
.filter((r) => r.status === 'success' && r.data)
|
|
2242
|
+
.map((r) => r.data.license.toUpperCase()); // Use toUpperCase for case-insensitive matching
|
|
2243
|
+
const uniqueLicenses = [...new Set(licensesFound)];
|
|
2244
|
+
const hasGPL = uniqueLicenses.some((lic) => lic.includes('GPL'));
|
|
2245
|
+
const hasMIT = uniqueLicenses.some((lic) => lic === 'MIT');
|
|
2246
|
+
const hasApache = uniqueLicenses.some((lic) => lic.includes('APACHE')); // Check for APACHE generally
|
|
2247
|
+
const hasUnknown = uniqueLicenses.some((lic) => lic === 'UNKNOWN');
|
|
2248
|
+
const allSuccess = processedResults.every((r) => r.status === 'success');
|
|
2249
|
+
if (!allSuccess) {
|
|
2250
|
+
warnings.push('Could not fetch license information for all packages.');
|
|
1357
2251
|
}
|
|
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';
|
|
2252
|
+
if (hasUnknown && licensesFound.length > 0) {
|
|
2253
|
+
warnings.push('Some packages have unknown or unspecified licenses. Manual review recommended.');
|
|
1367
2254
|
}
|
|
1368
2255
|
if (hasGPL) {
|
|
1369
|
-
|
|
2256
|
+
warnings.push('Contains GPL licensed code. Resulting work may need to be GPL licensed.');
|
|
1370
2257
|
if (hasMIT || hasApache) {
|
|
1371
|
-
|
|
2258
|
+
warnings.push('Mixed GPL with potentially incompatible licenses (e.g., MIT, Apache). Review carefully for compliance.');
|
|
1372
2259
|
}
|
|
1373
2260
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
2261
|
+
// Further refined compatibility checks can be added here if needed
|
|
2262
|
+
let summary = 'License compatibility analysis completed.';
|
|
2263
|
+
if (warnings.length > 0) {
|
|
2264
|
+
summary = 'License compatibility analysis completed with warnings.';
|
|
1376
2265
|
}
|
|
1377
|
-
else if (
|
|
1378
|
-
|
|
2266
|
+
else if (licensesFound.length === 0 && allSuccess) {
|
|
2267
|
+
summary = 'No license information found for the queried packages.';
|
|
1379
2268
|
}
|
|
1380
|
-
else if (
|
|
1381
|
-
|
|
2269
|
+
else if (licensesFound.length > 0 && !hasGPL && !hasUnknown) {
|
|
2270
|
+
summary = 'Licenses found appear to be generally compatible (non-GPL, known licenses).';
|
|
1382
2271
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
2272
|
+
const analysis = {
|
|
2273
|
+
summary: summary,
|
|
2274
|
+
warnings: warnings,
|
|
2275
|
+
uniqueLicensesFound: uniqueLicenses,
|
|
2276
|
+
};
|
|
2277
|
+
const finalResponse = {
|
|
2278
|
+
queryPackages: args.packages,
|
|
2279
|
+
results: processedResults,
|
|
2280
|
+
analysis: analysis,
|
|
2281
|
+
message: `License compatibility check for ${args.packages.length} package(s). Note: This is a basic analysis. For legal compliance, consult a legal expert.`,
|
|
2282
|
+
};
|
|
2283
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2284
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1386
2285
|
}
|
|
1387
2286
|
catch (error) {
|
|
2287
|
+
const errorResponse = JSON.stringify({
|
|
2288
|
+
queryPackages: args.packages,
|
|
2289
|
+
results: [],
|
|
2290
|
+
analysis: null,
|
|
2291
|
+
error: `General error analyzing license compatibility: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2292
|
+
}, null, 2);
|
|
1388
2293
|
return {
|
|
1389
|
-
content: [
|
|
1390
|
-
{
|
|
1391
|
-
type: 'text',
|
|
1392
|
-
text: `Error analyzing license compatibility: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1393
|
-
},
|
|
1394
|
-
],
|
|
2294
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1395
2295
|
isError: true,
|
|
1396
2296
|
};
|
|
1397
2297
|
}
|
|
@@ -1399,346 +2299,767 @@ export async function handleNpmLicenseCompatibility(args) {
|
|
|
1399
2299
|
// Repository statistics analyzer
|
|
1400
2300
|
export async function handleNpmRepoStats(args) {
|
|
1401
2301
|
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
|
-
}
|
|
2302
|
+
const packagesToProcess = args.packages || [];
|
|
2303
|
+
if (packagesToProcess.length === 0) {
|
|
2304
|
+
throw new Error('No package names provided for repository statistics analysis.');
|
|
1463
2305
|
}
|
|
1464
|
-
|
|
2306
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2307
|
+
let name = '';
|
|
2308
|
+
if (typeof pkgInput === 'string') {
|
|
2309
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2310
|
+
name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version typically ignored for repo stats
|
|
2311
|
+
}
|
|
2312
|
+
else {
|
|
2313
|
+
return {
|
|
2314
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2315
|
+
packageName: 'unknown_package_input',
|
|
2316
|
+
status: 'error',
|
|
2317
|
+
error: 'Invalid package input type',
|
|
2318
|
+
data: null,
|
|
2319
|
+
message: 'Package input was not a string.',
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
if (!name) {
|
|
2323
|
+
return {
|
|
2324
|
+
packageInput: pkgInput,
|
|
2325
|
+
packageName: 'empty_package_name',
|
|
2326
|
+
status: 'error',
|
|
2327
|
+
error: 'Empty package name derived from input',
|
|
2328
|
+
data: null,
|
|
2329
|
+
message: 'Package name could not be determined from input.',
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
const cacheKey = generateCacheKey('handleNpmRepoStats', name);
|
|
2333
|
+
const cachedResult = cacheGet(cacheKey); // Cache stores the entire result object structure
|
|
2334
|
+
if (cachedResult) {
|
|
2335
|
+
// Return the entire cached result object, which already includes status, data, message
|
|
2336
|
+
return {
|
|
2337
|
+
...cachedResult, // Spread the cached result
|
|
2338
|
+
packageInput: pkgInput, // Add current input for context
|
|
2339
|
+
packageName: name, // Add current name for context
|
|
2340
|
+
status: `${cachedResult.status}_cache`, // Append _cache to status
|
|
2341
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
try {
|
|
2345
|
+
const npmResponse = await fetch(`https://registry.npmjs.org/${name}/latest`);
|
|
2346
|
+
if (!npmResponse.ok) {
|
|
2347
|
+
const errorData = {
|
|
2348
|
+
packageInput: pkgInput,
|
|
2349
|
+
packageName: name,
|
|
2350
|
+
status: 'error',
|
|
2351
|
+
error: `Failed to fetch npm info for ${name}: ${npmResponse.status} ${npmResponse.statusText}`,
|
|
2352
|
+
data: null,
|
|
2353
|
+
message: `Could not retrieve NPM package data for ${name}.`,
|
|
2354
|
+
};
|
|
2355
|
+
// Do not cache primary API call failures
|
|
2356
|
+
return errorData;
|
|
2357
|
+
}
|
|
2358
|
+
const npmData = await npmResponse.json();
|
|
2359
|
+
if (!isNpmPackageVersionData(npmData)) {
|
|
2360
|
+
const errorData = {
|
|
2361
|
+
packageInput: pkgInput,
|
|
2362
|
+
packageName: name,
|
|
2363
|
+
status: 'error',
|
|
2364
|
+
error: 'Invalid NPM package data format received.',
|
|
2365
|
+
data: null,
|
|
2366
|
+
message: `Malformed NPM package data for ${name}.`,
|
|
2367
|
+
};
|
|
2368
|
+
return errorData;
|
|
2369
|
+
}
|
|
2370
|
+
const repoUrl = npmData.repository?.url;
|
|
2371
|
+
if (!repoUrl) {
|
|
2372
|
+
const resultNoRepo = {
|
|
2373
|
+
packageInput: pkgInput,
|
|
2374
|
+
packageName: name,
|
|
2375
|
+
status: 'no_repo_found',
|
|
2376
|
+
error: null,
|
|
2377
|
+
data: null,
|
|
2378
|
+
message: `No repository URL found in package data for ${name}.`,
|
|
2379
|
+
};
|
|
2380
|
+
cacheSet(cacheKey, resultNoRepo, CACHE_TTL_LONG);
|
|
2381
|
+
return resultNoRepo;
|
|
2382
|
+
}
|
|
2383
|
+
const githubMatch = repoUrl.match(/github\.com[:\/]([^\/]+)\/([^\/.]+)/);
|
|
2384
|
+
if (!githubMatch) {
|
|
2385
|
+
const resultNotGitHub = {
|
|
2386
|
+
packageInput: pkgInput,
|
|
2387
|
+
packageName: name,
|
|
2388
|
+
status: 'not_github_repo',
|
|
2389
|
+
error: null,
|
|
2390
|
+
data: { repositoryUrl: repoUrl },
|
|
2391
|
+
message: `Repository URL found (${repoUrl}) is not a standard GitHub URL.`,
|
|
2392
|
+
};
|
|
2393
|
+
cacheSet(cacheKey, resultNotGitHub, CACHE_TTL_LONG);
|
|
2394
|
+
return resultNotGitHub;
|
|
2395
|
+
}
|
|
2396
|
+
const [, owner, repo] = githubMatch;
|
|
2397
|
+
const githubRepoApiUrl = `https://api.github.com/repos/${owner}/${repo.replace(/\.git$/, '')}`;
|
|
2398
|
+
const githubResponse = await fetch(githubRepoApiUrl, {
|
|
2399
|
+
headers: {
|
|
2400
|
+
Accept: 'application/vnd.github.v3+json',
|
|
2401
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
2402
|
+
},
|
|
2403
|
+
});
|
|
2404
|
+
if (!githubResponse.ok) {
|
|
2405
|
+
const errorData = {
|
|
2406
|
+
packageInput: pkgInput,
|
|
2407
|
+
packageName: name,
|
|
2408
|
+
status: 'error',
|
|
2409
|
+
error: `Failed to fetch GitHub repo stats for ${owner}/${repo}: ${githubResponse.status} ${githubResponse.statusText}`,
|
|
2410
|
+
data: { githubRepoUrl: githubRepoApiUrl },
|
|
2411
|
+
message: `Could not retrieve GitHub repository statistics from ${githubRepoApiUrl}.`,
|
|
2412
|
+
};
|
|
2413
|
+
// Do not cache GitHub API call failures for now
|
|
2414
|
+
return errorData;
|
|
2415
|
+
}
|
|
2416
|
+
const githubData = (await githubResponse.json());
|
|
2417
|
+
const successResult = {
|
|
2418
|
+
packageInput: pkgInput,
|
|
2419
|
+
packageName: name,
|
|
2420
|
+
status: 'success',
|
|
2421
|
+
error: null,
|
|
2422
|
+
data: {
|
|
2423
|
+
githubRepoUrl: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}`,
|
|
2424
|
+
stars: githubData.stargazers_count,
|
|
2425
|
+
forks: githubData.forks_count,
|
|
2426
|
+
openIssues: githubData.open_issues_count,
|
|
2427
|
+
watchers: githubData.watchers_count,
|
|
2428
|
+
createdAt: githubData.created_at,
|
|
2429
|
+
updatedAt: githubData.updated_at,
|
|
2430
|
+
defaultBranch: githubData.default_branch,
|
|
2431
|
+
hasWiki: githubData.has_wiki,
|
|
2432
|
+
topics: githubData.topics || [],
|
|
2433
|
+
},
|
|
2434
|
+
message: 'GitHub repository statistics fetched successfully.',
|
|
2435
|
+
};
|
|
2436
|
+
cacheSet(cacheKey, successResult, CACHE_TTL_LONG);
|
|
2437
|
+
return successResult;
|
|
2438
|
+
}
|
|
2439
|
+
catch (error) {
|
|
2440
|
+
return {
|
|
2441
|
+
packageInput: pkgInput,
|
|
2442
|
+
packageName: name,
|
|
2443
|
+
status: 'error',
|
|
2444
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2445
|
+
data: null,
|
|
2446
|
+
message: `An unexpected error occurred while processing ${name}.`,
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
}));
|
|
2450
|
+
const finalResponse = {
|
|
2451
|
+
queryPackages: args.packages,
|
|
2452
|
+
results: processedResults,
|
|
2453
|
+
message: `Repository statistics analysis for ${args.packages.length} package(s).`,
|
|
2454
|
+
};
|
|
2455
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2456
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1465
2457
|
}
|
|
1466
2458
|
catch (error) {
|
|
2459
|
+
const errorResponse = JSON.stringify({
|
|
2460
|
+
queryPackages: args.packages,
|
|
2461
|
+
results: [],
|
|
2462
|
+
error: `General error analyzing repository stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2463
|
+
}, null, 2);
|
|
1467
2464
|
return {
|
|
1468
|
-
content: [
|
|
1469
|
-
{
|
|
1470
|
-
type: 'text',
|
|
1471
|
-
text: `Error analyzing repository stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1472
|
-
},
|
|
1473
|
-
],
|
|
2465
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1474
2466
|
isError: true,
|
|
1475
2467
|
};
|
|
1476
2468
|
}
|
|
1477
2469
|
}
|
|
1478
2470
|
export async function handleNpmDeprecated(args) {
|
|
1479
2471
|
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
|
-
}
|
|
2472
|
+
const packagesToProcess = args.packages || [];
|
|
2473
|
+
if (packagesToProcess.length === 0) {
|
|
2474
|
+
throw new Error('No package names provided');
|
|
2475
|
+
}
|
|
2476
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2477
|
+
let name = '';
|
|
2478
|
+
let version = 'latest'; // Default to 'latest'
|
|
2479
|
+
if (typeof pkgInput === 'string') {
|
|
2480
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2481
|
+
if (atIdx > 0) {
|
|
2482
|
+
name = pkgInput.slice(0, atIdx);
|
|
2483
|
+
version = pkgInput.slice(atIdx + 1);
|
|
1516
2484
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
2485
|
+
else {
|
|
2486
|
+
name = pkgInput;
|
|
1519
2487
|
}
|
|
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
2488
|
}
|
|
1528
2489
|
else {
|
|
1529
|
-
|
|
2490
|
+
return {
|
|
2491
|
+
package: 'unknown_package_input',
|
|
2492
|
+
status: 'error',
|
|
2493
|
+
error: 'Invalid package input type',
|
|
2494
|
+
data: null,
|
|
2495
|
+
message: 'Package input was not a string.',
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
const initialPackageNameForOutput = version === 'latest' ? name : `${name}@${version}`;
|
|
2499
|
+
const cacheKey = generateCacheKey('handleNpmDeprecated', name, version);
|
|
2500
|
+
const cachedResult = cacheGet(cacheKey);
|
|
2501
|
+
if (cachedResult) {
|
|
2502
|
+
// console.debug(`[handleNpmDeprecated] Cache hit for ${cacheKey}`);
|
|
2503
|
+
return {
|
|
2504
|
+
package: cachedResult.package,
|
|
2505
|
+
status: 'success_cache',
|
|
2506
|
+
error: null,
|
|
2507
|
+
data: cachedResult.data,
|
|
2508
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2509
|
+
};
|
|
1530
2510
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2511
|
+
// console.debug(`[handleNpmDeprecated] Cache miss for ${cacheKey}`);
|
|
2512
|
+
try {
|
|
2513
|
+
const mainPkgResponse = await fetch(`https://registry.npmjs.org/${name}`);
|
|
2514
|
+
if (!mainPkgResponse.ok) {
|
|
2515
|
+
return {
|
|
2516
|
+
package: initialPackageNameForOutput,
|
|
2517
|
+
status: 'error',
|
|
2518
|
+
error: `Failed to fetch package info for ${name}: ${mainPkgResponse.status} ${mainPkgResponse.statusText}`,
|
|
2519
|
+
data: null,
|
|
2520
|
+
message: `Could not retrieve main package data for ${name}.`,
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
const mainPkgData = (await mainPkgResponse.json());
|
|
2524
|
+
let versionToFetch = version;
|
|
2525
|
+
if (version === 'latest') {
|
|
2526
|
+
versionToFetch = mainPkgData['dist-tags']?.latest || 'latest';
|
|
2527
|
+
if (versionToFetch === 'latest' && !mainPkgData.versions?.[versionToFetch]) {
|
|
2528
|
+
const availableVersions = Object.keys(mainPkgData.versions || {});
|
|
2529
|
+
if (availableVersions.length > 0) {
|
|
2530
|
+
versionToFetch = availableVersions.sort().pop() || 'latest'; // Basic sort
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
1536
2533
|
}
|
|
2534
|
+
const finalPackageNameForOutput = `${name}@${versionToFetch}`;
|
|
2535
|
+
const versionInfo = mainPkgData.versions?.[versionToFetch];
|
|
2536
|
+
if (!versionInfo) {
|
|
2537
|
+
return {
|
|
2538
|
+
package: finalPackageNameForOutput,
|
|
2539
|
+
status: 'error',
|
|
2540
|
+
error: `Version ${versionToFetch} not found for package ${name}.`,
|
|
2541
|
+
data: null,
|
|
2542
|
+
message: `Specified version for ${name} does not exist.`,
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
const isPackageDeprecated = !!versionInfo.deprecated;
|
|
2546
|
+
const packageDeprecationMessage = versionInfo.deprecated || null;
|
|
2547
|
+
const processDependencies = async (deps) => {
|
|
2548
|
+
if (!deps)
|
|
2549
|
+
return [];
|
|
2550
|
+
const depChecks = Object.entries(deps).map(async ([depName, depSemVer]) => {
|
|
2551
|
+
const lookedUpAs = depName; // Strategy: always use original name, no cleaning.
|
|
2552
|
+
let statusMessage = '';
|
|
2553
|
+
try {
|
|
2554
|
+
// console.debug(`[handleNpmDeprecated] Checking dependency: ${depName}@${depSemVer}`);
|
|
2555
|
+
const depInfoResponse = await fetch(`https://registry.npmjs.org/${encodeURIComponent(depName)}`);
|
|
2556
|
+
if (!depInfoResponse.ok) {
|
|
2557
|
+
statusMessage = `Could not fetch dependency info for '${depName}' (status: ${depInfoResponse.status}). Deprecation status unknown.`;
|
|
2558
|
+
// console.warn(`[handleNpmDeprecated] ${statusMessage}`);
|
|
2559
|
+
return {
|
|
2560
|
+
name: depName,
|
|
2561
|
+
version: depSemVer,
|
|
2562
|
+
lookedUpAs: lookedUpAs,
|
|
2563
|
+
isDeprecated: false, // Assume not deprecated as status is unknown
|
|
2564
|
+
deprecationMessage: null,
|
|
2565
|
+
statusMessage: statusMessage,
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
const depData = (await depInfoResponse.json());
|
|
2569
|
+
const latestDepVersionTag = depData['dist-tags']?.latest;
|
|
2570
|
+
const latestDepVersionInfo = latestDepVersionTag
|
|
2571
|
+
? depData.versions?.[latestDepVersionTag]
|
|
2572
|
+
: undefined;
|
|
2573
|
+
statusMessage = `Successfully checked '${depName}'.`;
|
|
2574
|
+
return {
|
|
2575
|
+
name: depName,
|
|
2576
|
+
version: depSemVer,
|
|
2577
|
+
lookedUpAs: lookedUpAs,
|
|
2578
|
+
isDeprecated: !!latestDepVersionInfo?.deprecated,
|
|
2579
|
+
deprecationMessage: latestDepVersionInfo?.deprecated || null,
|
|
2580
|
+
statusMessage: statusMessage,
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
catch (error) {
|
|
2584
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown processing error';
|
|
2585
|
+
statusMessage = `Error processing dependency '${depName}': ${errorMessage}. Deprecation status unknown.`;
|
|
2586
|
+
// console.warn(`[handleNpmDeprecated] ${statusMessage}`);
|
|
2587
|
+
return {
|
|
2588
|
+
name: depName,
|
|
2589
|
+
version: depSemVer,
|
|
2590
|
+
lookedUpAs: lookedUpAs,
|
|
2591
|
+
isDeprecated: false, // Assume not deprecated as status is unknown
|
|
2592
|
+
deprecationMessage: null,
|
|
2593
|
+
statusMessage: statusMessage,
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
return Promise.all(depChecks);
|
|
2598
|
+
};
|
|
2599
|
+
const directDeps = await processDependencies(versionInfo.dependencies);
|
|
2600
|
+
const devDeps = await processDependencies(versionInfo.devDependencies);
|
|
2601
|
+
const peerDeps = await processDependencies(versionInfo.peerDependencies);
|
|
2602
|
+
const allDeps = [...directDeps, ...devDeps, ...peerDeps];
|
|
2603
|
+
const unverifiableDepsCount = allDeps.filter((dep) => {
|
|
2604
|
+
const msg = dep.statusMessage.toLowerCase();
|
|
2605
|
+
return msg.includes('could not fetch') || msg.includes('error processing');
|
|
2606
|
+
}).length;
|
|
2607
|
+
let dependencySummaryMessage = `Processed ${allDeps.length} total dependencies.`;
|
|
2608
|
+
if (unverifiableDepsCount > 0) {
|
|
2609
|
+
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.`;
|
|
2610
|
+
}
|
|
2611
|
+
const resultData = {
|
|
2612
|
+
isPackageDeprecated,
|
|
2613
|
+
packageDeprecationMessage,
|
|
2614
|
+
dependencies: {
|
|
2615
|
+
direct: directDeps,
|
|
2616
|
+
development: devDeps,
|
|
2617
|
+
peer: peerDeps,
|
|
2618
|
+
},
|
|
2619
|
+
dependencySummary: {
|
|
2620
|
+
totalDependencies: allDeps.length,
|
|
2621
|
+
unverifiableDependencies: unverifiableDepsCount,
|
|
2622
|
+
message: dependencySummaryMessage,
|
|
2623
|
+
},
|
|
2624
|
+
};
|
|
2625
|
+
const fullMessage = `Deprecation status for ${finalPackageNameForOutput}. ${dependencySummaryMessage}`;
|
|
2626
|
+
const resultToCache = {
|
|
2627
|
+
package: finalPackageNameForOutput,
|
|
2628
|
+
data: resultData,
|
|
2629
|
+
message: fullMessage,
|
|
2630
|
+
};
|
|
2631
|
+
cacheSet(cacheKey, resultToCache, CACHE_TTL_MEDIUM);
|
|
2632
|
+
// console.debug(`[handleNpmDeprecated] Set cache for ${cacheKey}`);
|
|
2633
|
+
return {
|
|
2634
|
+
package: finalPackageNameForOutput,
|
|
2635
|
+
status: 'success',
|
|
2636
|
+
error: null,
|
|
2637
|
+
data: resultData,
|
|
2638
|
+
message: fullMessage,
|
|
2639
|
+
};
|
|
1537
2640
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
2641
|
+
catch (error) {
|
|
2642
|
+
// console.error(`[handleNpmDeprecated] Error processing ${initialPackageNameForOutput}: ${error}`);
|
|
2643
|
+
return {
|
|
2644
|
+
package: initialPackageNameForOutput,
|
|
2645
|
+
status: 'error',
|
|
2646
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2647
|
+
data: null,
|
|
2648
|
+
message: `An unexpected error occurred while processing ${initialPackageNameForOutput}.`,
|
|
2649
|
+
};
|
|
1540
2650
|
}
|
|
1541
|
-
return { name: pkg, text };
|
|
1542
2651
|
}));
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
text += result.text;
|
|
1546
|
-
}
|
|
1547
|
-
return { content: [{ type: 'text', text }], isError: false };
|
|
2652
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
2653
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1548
2654
|
}
|
|
1549
2655
|
catch (error) {
|
|
2656
|
+
// console.error(`[handleNpmDeprecated] General error: ${error}`);
|
|
2657
|
+
const errorResponse = JSON.stringify({
|
|
2658
|
+
results: [],
|
|
2659
|
+
error: `General error checking deprecated packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2660
|
+
}, null, 2);
|
|
1550
2661
|
return {
|
|
1551
|
-
content: [
|
|
1552
|
-
{
|
|
1553
|
-
type: 'text',
|
|
1554
|
-
text: `Error checking deprecated packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1555
|
-
},
|
|
1556
|
-
],
|
|
2662
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1557
2663
|
isError: true,
|
|
1558
2664
|
};
|
|
1559
2665
|
}
|
|
1560
2666
|
}
|
|
1561
2667
|
export async function handleNpmChangelogAnalysis(args) {
|
|
1562
2668
|
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
|
-
}
|
|
2669
|
+
const packagesToProcess = args.packages || [];
|
|
2670
|
+
if (packagesToProcess.length === 0) {
|
|
2671
|
+
throw new Error('No package names provided for changelog analysis.');
|
|
2672
|
+
}
|
|
2673
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2674
|
+
let name = '';
|
|
2675
|
+
let versionQueried = undefined;
|
|
2676
|
+
if (typeof pkgInput === 'string') {
|
|
2677
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2678
|
+
if (atIdx > 0) {
|
|
2679
|
+
name = pkgInput.slice(0, atIdx);
|
|
2680
|
+
versionQueried = pkgInput.slice(atIdx + 1);
|
|
1604
2681
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
2682
|
+
else {
|
|
2683
|
+
name = pkgInput;
|
|
1607
2684
|
}
|
|
1608
2685
|
}
|
|
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
2686
|
else {
|
|
1635
|
-
|
|
2687
|
+
return {
|
|
2688
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2689
|
+
packageName: 'unknown_package_input',
|
|
2690
|
+
versionQueried: versionQueried,
|
|
2691
|
+
status: 'error',
|
|
2692
|
+
error: 'Invalid package input type',
|
|
2693
|
+
data: null,
|
|
2694
|
+
message: 'Package input was not a string.',
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
if (!name) {
|
|
2698
|
+
return {
|
|
2699
|
+
packageInput: pkgInput,
|
|
2700
|
+
packageName: 'empty_package_name',
|
|
2701
|
+
versionQueried: versionQueried,
|
|
2702
|
+
status: 'error',
|
|
2703
|
+
error: 'Empty package name derived from input',
|
|
2704
|
+
data: null,
|
|
2705
|
+
message: 'Package name could not be determined from input.',
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
const cacheKey = generateCacheKey('handleNpmChangelogAnalysis', name);
|
|
2709
|
+
const cachedResult = cacheGet(cacheKey); // Expects the full result object to be cached
|
|
2710
|
+
if (cachedResult) {
|
|
2711
|
+
return {
|
|
2712
|
+
...cachedResult,
|
|
2713
|
+
packageInput: pkgInput, // Ensure these are current for this specific call
|
|
2714
|
+
packageName: name,
|
|
2715
|
+
versionQueried: versionQueried,
|
|
2716
|
+
status: `${cachedResult.status}_cache`,
|
|
2717
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2718
|
+
};
|
|
1636
2719
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
2720
|
+
try {
|
|
2721
|
+
const npmResponse = await fetch(`https://registry.npmjs.org/${name}`);
|
|
2722
|
+
if (!npmResponse.ok) {
|
|
2723
|
+
const errorResult = {
|
|
2724
|
+
packageInput: pkgInput,
|
|
2725
|
+
packageName: name,
|
|
2726
|
+
versionQueried: versionQueried,
|
|
2727
|
+
status: 'error',
|
|
2728
|
+
error: `Failed to fetch npm info for ${name}: ${npmResponse.status} ${npmResponse.statusText}`,
|
|
2729
|
+
data: null,
|
|
2730
|
+
message: `Could not retrieve NPM package data for ${name}.`,
|
|
2731
|
+
};
|
|
2732
|
+
return errorResult; // Do not cache this type of error
|
|
2733
|
+
}
|
|
2734
|
+
const npmData = await npmResponse.json();
|
|
2735
|
+
if (!isNpmPackageInfo(npmData)) {
|
|
2736
|
+
const errorResult = {
|
|
2737
|
+
packageInput: pkgInput,
|
|
2738
|
+
packageName: name,
|
|
2739
|
+
versionQueried: versionQueried,
|
|
2740
|
+
status: 'error',
|
|
2741
|
+
error: 'Invalid NPM package info data received',
|
|
2742
|
+
data: null,
|
|
2743
|
+
message: `Received malformed NPM package data for ${name}.`,
|
|
2744
|
+
};
|
|
2745
|
+
return errorResult; // Do not cache this type of error
|
|
2746
|
+
}
|
|
2747
|
+
const repositoryUrl = npmData.repository?.url;
|
|
2748
|
+
if (!repositoryUrl) {
|
|
2749
|
+
const resultNoRepo = {
|
|
2750
|
+
packageInput: pkgInput,
|
|
2751
|
+
packageName: name,
|
|
2752
|
+
versionQueried: versionQueried,
|
|
2753
|
+
status: 'no_repo_found',
|
|
2754
|
+
error: null,
|
|
2755
|
+
data: null,
|
|
2756
|
+
message: `No repository URL found in package data for ${name}.`,
|
|
2757
|
+
};
|
|
2758
|
+
cacheSet(cacheKey, resultNoRepo, CACHE_TTL_MEDIUM);
|
|
2759
|
+
return resultNoRepo;
|
|
2760
|
+
}
|
|
2761
|
+
const githubMatch = repositoryUrl.match(/github\.com[:\/]([^\/]+)\/([^\/.]+)/);
|
|
2762
|
+
if (!githubMatch) {
|
|
2763
|
+
const resultNotGitHub = {
|
|
2764
|
+
packageInput: pkgInput,
|
|
2765
|
+
packageName: name,
|
|
2766
|
+
versionQueried: versionQueried,
|
|
2767
|
+
status: 'not_github_repo',
|
|
2768
|
+
error: null,
|
|
2769
|
+
data: { repositoryUrl: repositoryUrl },
|
|
2770
|
+
message: `Repository URL (${repositoryUrl}) is not a standard GitHub URL.`,
|
|
2771
|
+
};
|
|
2772
|
+
cacheSet(cacheKey, resultNotGitHub, CACHE_TTL_MEDIUM);
|
|
2773
|
+
return resultNotGitHub;
|
|
2774
|
+
}
|
|
2775
|
+
const [, owner, repo] = githubMatch;
|
|
2776
|
+
const repoNameForUrl = repo.replace(/\.git$/, '');
|
|
2777
|
+
const changelogFiles = [
|
|
2778
|
+
'CHANGELOG.md',
|
|
2779
|
+
'changelog.md',
|
|
2780
|
+
'CHANGES.md',
|
|
2781
|
+
'changes.md',
|
|
2782
|
+
'HISTORY.md',
|
|
2783
|
+
'history.md',
|
|
2784
|
+
'NEWS.md',
|
|
2785
|
+
'news.md',
|
|
2786
|
+
'RELEASES.md',
|
|
2787
|
+
'releases.md',
|
|
2788
|
+
];
|
|
2789
|
+
let changelogContent = null;
|
|
2790
|
+
let changelogSourceUrl = null;
|
|
2791
|
+
let hasChangelogFile = false;
|
|
2792
|
+
for (const file of changelogFiles) {
|
|
2793
|
+
try {
|
|
2794
|
+
const rawChangelogUrl = `https://raw.githubusercontent.com/${owner}/${repoNameForUrl}/master/${file}`;
|
|
2795
|
+
const response = await fetch(rawChangelogUrl);
|
|
2796
|
+
if (response.ok) {
|
|
2797
|
+
changelogContent = await response.text();
|
|
2798
|
+
changelogSourceUrl = rawChangelogUrl;
|
|
2799
|
+
hasChangelogFile = true;
|
|
2800
|
+
break;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
catch (error) {
|
|
2804
|
+
console.debug(`Error fetching changelog file ${file} for ${name}: ${error}`);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
let githubReleases = [];
|
|
2808
|
+
try {
|
|
2809
|
+
const githubApiResponse = await fetch(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
|
|
2810
|
+
headers: {
|
|
2811
|
+
Accept: 'application/vnd.github.v3+json',
|
|
2812
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
2813
|
+
},
|
|
2814
|
+
});
|
|
2815
|
+
if (githubApiResponse.ok) {
|
|
2816
|
+
const releasesData = (await githubApiResponse.json());
|
|
2817
|
+
githubReleases = releasesData.map((r) => ({
|
|
2818
|
+
tag_name: r.tag_name || null,
|
|
2819
|
+
name: r.name || null,
|
|
2820
|
+
published_at: r.published_at || null,
|
|
2821
|
+
}));
|
|
2822
|
+
}
|
|
1646
2823
|
}
|
|
2824
|
+
catch (error) {
|
|
2825
|
+
console.debug(`Error fetching GitHub releases for ${name}: ${error}`);
|
|
2826
|
+
}
|
|
2827
|
+
const versions = Object.keys(npmData.versions || {});
|
|
2828
|
+
const npmVersionHistory = {
|
|
2829
|
+
totalVersions: versions.length,
|
|
2830
|
+
latestVersion: npmData['dist-tags']?.latest || (versions.length > 0 ? versions.sort().pop() : null),
|
|
2831
|
+
firstVersion: versions.length > 0 ? versions.sort()[0] : null,
|
|
2832
|
+
};
|
|
2833
|
+
const status = changelogContent || githubReleases.length > 0 ? 'success' : 'no_changelog_found';
|
|
2834
|
+
const message = status === 'success'
|
|
2835
|
+
? `Changelog and release information retrieved for ${name}.`
|
|
2836
|
+
: status === 'no_changelog_found'
|
|
2837
|
+
? `No changelog file or GitHub releases found for ${name}.`
|
|
2838
|
+
: `Changelog analysis for ${name}.`;
|
|
2839
|
+
const resultToCache = {
|
|
2840
|
+
packageInput: pkgInput, // This might differ on subsequent cache hits, so store the original reference for this specific cache entry
|
|
2841
|
+
packageName: name,
|
|
2842
|
+
versionQueried: versionQueried,
|
|
2843
|
+
status: status,
|
|
2844
|
+
error: null,
|
|
2845
|
+
data: {
|
|
2846
|
+
repositoryUrl: repositoryUrl,
|
|
2847
|
+
changelogSourceUrl: changelogSourceUrl,
|
|
2848
|
+
changelogContent: changelogContent
|
|
2849
|
+
? `${changelogContent.split('\n').slice(0, 50).join('\n')}...`
|
|
2850
|
+
: null,
|
|
2851
|
+
hasChangelogFile: hasChangelogFile,
|
|
2852
|
+
githubReleases: githubReleases,
|
|
2853
|
+
npmVersionHistory: npmVersionHistory,
|
|
2854
|
+
},
|
|
2855
|
+
message: message,
|
|
2856
|
+
};
|
|
2857
|
+
cacheSet(cacheKey, resultToCache, CACHE_TTL_MEDIUM);
|
|
2858
|
+
return resultToCache;
|
|
1647
2859
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
2860
|
+
catch (error) {
|
|
2861
|
+
const errorResult = {
|
|
2862
|
+
packageInput: pkgInput,
|
|
2863
|
+
packageName: name,
|
|
2864
|
+
versionQueried: versionQueried,
|
|
2865
|
+
status: 'error',
|
|
2866
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
2867
|
+
data: null,
|
|
2868
|
+
message: `An unexpected error occurred while analyzing changelog for ${name}.`,
|
|
2869
|
+
};
|
|
2870
|
+
return errorResult; // Do not cache general errors
|
|
1650
2871
|
}
|
|
1651
|
-
return { name: pkg, text };
|
|
1652
2872
|
}));
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
2873
|
+
const finalResponse = {
|
|
2874
|
+
queryPackages: args.packages,
|
|
2875
|
+
results: processedResults,
|
|
2876
|
+
};
|
|
2877
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2878
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1658
2879
|
}
|
|
1659
2880
|
catch (error) {
|
|
2881
|
+
const errorResponse = JSON.stringify({
|
|
2882
|
+
queryPackages: args.packages,
|
|
2883
|
+
results: [],
|
|
2884
|
+
error: `General error analyzing changelogs: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2885
|
+
}, null, 2);
|
|
1660
2886
|
return {
|
|
1661
|
-
content: [
|
|
1662
|
-
{
|
|
1663
|
-
type: 'text',
|
|
1664
|
-
text: `Error analyzing changelog: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1665
|
-
},
|
|
1666
|
-
],
|
|
2887
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1667
2888
|
isError: true,
|
|
1668
2889
|
};
|
|
1669
2890
|
}
|
|
1670
2891
|
}
|
|
1671
2892
|
export async function handleNpmAlternatives(args) {
|
|
1672
2893
|
try {
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2894
|
+
const packagesToProcess = args.packages || [];
|
|
2895
|
+
if (packagesToProcess.length === 0) {
|
|
2896
|
+
throw new Error('No package names provided to find alternatives.');
|
|
2897
|
+
}
|
|
2898
|
+
const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
|
|
2899
|
+
let originalPackageName = '';
|
|
2900
|
+
let versionQueried = undefined;
|
|
2901
|
+
if (typeof pkgInput === 'string') {
|
|
2902
|
+
const atIdx = pkgInput.lastIndexOf('@');
|
|
2903
|
+
if (atIdx > 0) {
|
|
2904
|
+
originalPackageName = pkgInput.slice(0, atIdx);
|
|
2905
|
+
versionQueried = pkgInput.slice(atIdx + 1);
|
|
2906
|
+
}
|
|
2907
|
+
else {
|
|
2908
|
+
originalPackageName = pkgInput;
|
|
2909
|
+
}
|
|
1677
2910
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2911
|
+
else {
|
|
2912
|
+
return {
|
|
2913
|
+
packageInput: JSON.stringify(pkgInput),
|
|
2914
|
+
packageName: 'unknown_package_input',
|
|
2915
|
+
status: 'error',
|
|
2916
|
+
error: 'Invalid package input type',
|
|
2917
|
+
data: null,
|
|
2918
|
+
message: 'Package input was not a string.',
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
if (!originalPackageName) {
|
|
2922
|
+
return {
|
|
2923
|
+
packageInput: pkgInput,
|
|
2924
|
+
packageName: 'empty_package_name',
|
|
2925
|
+
status: 'error',
|
|
2926
|
+
error: 'Empty package name derived from input',
|
|
2927
|
+
data: null,
|
|
2928
|
+
message: 'Package name could not be determined from input.',
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
const cacheKey = generateCacheKey('handleNpmAlternatives', originalPackageName);
|
|
2932
|
+
const cachedResult = cacheGet(cacheKey); // Expects the full result object
|
|
2933
|
+
if (cachedResult) {
|
|
2934
|
+
return {
|
|
2935
|
+
...cachedResult,
|
|
2936
|
+
packageInput: pkgInput, // current input context
|
|
2937
|
+
packageName: originalPackageName, // current name context
|
|
2938
|
+
// versionQueried is part of cachedResult.data or similar if stored, or add if needed
|
|
2939
|
+
status: `${cachedResult.status}_cache`,
|
|
2940
|
+
message: `${cachedResult.message} (from cache)`,
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
try {
|
|
2944
|
+
const searchResponse = await fetch(`https://registry.npmjs.org/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=10`);
|
|
2945
|
+
if (!searchResponse.ok) {
|
|
2946
|
+
const errorResult = {
|
|
2947
|
+
packageInput: pkgInput,
|
|
2948
|
+
packageName: originalPackageName,
|
|
2949
|
+
status: 'error',
|
|
2950
|
+
error: `Failed to search for alternatives: ${searchResponse.status} ${searchResponse.statusText}`,
|
|
2951
|
+
data: null,
|
|
2952
|
+
message: 'Could not perform search for alternatives.',
|
|
2953
|
+
};
|
|
2954
|
+
return errorResult; // Do not cache API errors for search
|
|
2955
|
+
}
|
|
2956
|
+
const searchData = (await searchResponse.json());
|
|
2957
|
+
const alternativePackagesRaw = searchData.objects || [];
|
|
2958
|
+
let originalPackageDownloads = 0;
|
|
1681
2959
|
try {
|
|
1682
|
-
const
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2960
|
+
const dlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
|
|
2961
|
+
if (dlResponse.ok) {
|
|
2962
|
+
originalPackageDownloads =
|
|
2963
|
+
(await dlResponse.json()).downloads || 0;
|
|
2964
|
+
}
|
|
1687
2965
|
}
|
|
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
|
-
|
|
2966
|
+
catch (e) {
|
|
2967
|
+
console.debug(`Failed to fetch downloads for original package ${originalPackageName}: ${e}`);
|
|
2968
|
+
}
|
|
2969
|
+
const originalPackageKeywords = alternativePackagesRaw.find((p) => p.package.name === originalPackageName)?.package
|
|
2970
|
+
.keywords || [];
|
|
2971
|
+
const originalPackageStats = {
|
|
2972
|
+
name: originalPackageName,
|
|
2973
|
+
monthlyDownloads: originalPackageDownloads,
|
|
2974
|
+
keywords: originalPackageKeywords,
|
|
2975
|
+
};
|
|
2976
|
+
if (alternativePackagesRaw.length === 0 ||
|
|
2977
|
+
(alternativePackagesRaw.length === 1 &&
|
|
2978
|
+
alternativePackagesRaw[0].package.name === originalPackageName)) {
|
|
2979
|
+
const resultNoAlternatives = {
|
|
2980
|
+
packageInput: pkgInput,
|
|
2981
|
+
packageName: originalPackageName,
|
|
2982
|
+
status: 'no_alternatives_found',
|
|
2983
|
+
error: null,
|
|
2984
|
+
data: { originalPackageStats, alternatives: [] },
|
|
2985
|
+
message: `No significant alternatives found for ${originalPackageName} based on keyword search.`,
|
|
2986
|
+
};
|
|
2987
|
+
cacheSet(cacheKey, resultNoAlternatives, CACHE_TTL_MEDIUM);
|
|
2988
|
+
return resultNoAlternatives;
|
|
2989
|
+
}
|
|
2990
|
+
const alternativesData = await Promise.all(alternativePackagesRaw
|
|
2991
|
+
.filter((alt) => alt.package.name !== originalPackageName)
|
|
2992
|
+
.slice(0, 5)
|
|
2993
|
+
.map(async (alt) => {
|
|
2994
|
+
let altDownloads = 0;
|
|
2995
|
+
try {
|
|
2996
|
+
const altDlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
|
|
2997
|
+
if (altDlResponse.ok) {
|
|
2998
|
+
altDownloads = (await altDlResponse.json()).downloads || 0;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
catch (e) {
|
|
3002
|
+
console.debug(`Failed to fetch downloads for alternative ${alt.package.name}: ${e}`);
|
|
3003
|
+
}
|
|
3004
|
+
return {
|
|
3005
|
+
name: alt.package.name,
|
|
3006
|
+
description: alt.package.description || null,
|
|
3007
|
+
version: alt.package.version,
|
|
3008
|
+
monthlyDownloads: altDownloads,
|
|
3009
|
+
score: alt.score.final,
|
|
3010
|
+
repositoryUrl: alt.package.links?.repository || null,
|
|
3011
|
+
keywords: alt.package.keywords || [],
|
|
3012
|
+
};
|
|
3013
|
+
}));
|
|
3014
|
+
const successResult = {
|
|
3015
|
+
packageInput: pkgInput,
|
|
3016
|
+
packageName: originalPackageName,
|
|
3017
|
+
status: 'success',
|
|
3018
|
+
error: null,
|
|
3019
|
+
data: {
|
|
3020
|
+
originalPackageStats: originalPackageStats,
|
|
3021
|
+
alternatives: alternativesData,
|
|
3022
|
+
},
|
|
3023
|
+
message: `Found ${alternativesData.length} alternative(s) for ${originalPackageName}.`,
|
|
3024
|
+
};
|
|
3025
|
+
cacheSet(cacheKey, successResult, CACHE_TTL_MEDIUM);
|
|
3026
|
+
return successResult;
|
|
3027
|
+
}
|
|
3028
|
+
catch (error) {
|
|
3029
|
+
const errorResult = {
|
|
3030
|
+
packageInput: pkgInput,
|
|
3031
|
+
packageName: originalPackageName,
|
|
3032
|
+
status: 'error',
|
|
3033
|
+
error: error instanceof Error ? error.message : 'Unknown processing error',
|
|
3034
|
+
data: null,
|
|
3035
|
+
message: `An unexpected error occurred while finding alternatives for ${originalPackageName}.`,
|
|
3036
|
+
};
|
|
3037
|
+
return errorResult; // Do not cache general errors
|
|
3038
|
+
}
|
|
1719
3039
|
}));
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
}
|
|
1724
|
-
|
|
3040
|
+
const finalResponse = {
|
|
3041
|
+
queryPackages: args.packages,
|
|
3042
|
+
results: processedResults,
|
|
3043
|
+
};
|
|
3044
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
3045
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1725
3046
|
}
|
|
1726
3047
|
catch (error) {
|
|
3048
|
+
const errorResponse = JSON.stringify({
|
|
3049
|
+
queryPackages: args.packages,
|
|
3050
|
+
results: [],
|
|
3051
|
+
error: `General error finding alternatives: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
3052
|
+
}, null, 2);
|
|
1727
3053
|
return {
|
|
1728
|
-
content: [
|
|
1729
|
-
{
|
|
1730
|
-
type: 'text',
|
|
1731
|
-
text: `Error finding alternatives: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1732
|
-
},
|
|
1733
|
-
],
|
|
3054
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1734
3055
|
isError: true,
|
|
1735
3056
|
};
|
|
1736
3057
|
}
|
|
1737
3058
|
}
|
|
1738
3059
|
// Create server instance
|
|
1739
3060
|
const server = new McpServer({
|
|
1740
|
-
name: '
|
|
1741
|
-
version: '1.
|
|
3061
|
+
name: 'npm-sentinel-mcp',
|
|
3062
|
+
version: '1.5.7',
|
|
1742
3063
|
});
|
|
1743
3064
|
// Add NPM tools
|
|
1744
3065
|
server.tool('npmVersions', 'Get all available versions of an NPM package', {
|
|
@@ -1867,4 +3188,16 @@ process.on('unhandledRejection', (error) => {
|
|
|
1867
3188
|
server.close();
|
|
1868
3189
|
process.exit(1);
|
|
1869
3190
|
});
|
|
3191
|
+
// Type guard for NpmPackageVersionSchema
|
|
3192
|
+
function isNpmPackageVersionData(data) {
|
|
3193
|
+
try {
|
|
3194
|
+
// Use safeParse for type guards to avoid throwing errors on invalid data
|
|
3195
|
+
return NpmPackageVersionSchema.safeParse(data).success;
|
|
3196
|
+
}
|
|
3197
|
+
catch (e) {
|
|
3198
|
+
// This catch block might not be strictly necessary with safeParse but kept for safety
|
|
3199
|
+
// console.error("isNpmPackageVersionData validation failed unexpectedly:", e);
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
1870
3203
|
//# sourceMappingURL=index.js.map
|