@nekzus/mcp-server 1.5.5 → 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 +134 -95
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +453 -69
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2556 -1231
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
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' &&
|
|
@@ -487,1266 +276,2790 @@ function isNpmDownloadsData(data) {
|
|
|
487
276
|
return false;
|
|
488
277
|
}
|
|
489
278
|
}
|
|
490
|
-
async function handleNpmVersions(args) {
|
|
279
|
+
export async function handleNpmVersions(args) {
|
|
491
280
|
try {
|
|
492
281
|
const packagesToProcess = args.packages || [];
|
|
493
282
|
if (packagesToProcess.length === 0) {
|
|
494
283
|
throw new Error('No package names provided');
|
|
495
284
|
}
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const [aMajor = 0, aMinor = 0, aPatch = 0] = a.split('.').map(Number);
|
|
507
|
-
const [bMajor = 0, bMinor = 0, bPatch = 0] = b.split('.').map(Number);
|
|
508
|
-
if (aMajor !== bMajor)
|
|
509
|
-
return aMajor - bMajor;
|
|
510
|
-
if (aMinor !== bMinor)
|
|
511
|
-
return aMinor - bMinor;
|
|
512
|
-
return aPatch - bPatch;
|
|
513
|
-
});
|
|
514
|
-
return { name: pkg, versions };
|
|
515
|
-
}));
|
|
516
|
-
let text = '';
|
|
517
|
-
for (const result of results) {
|
|
518
|
-
if ('error' in result) {
|
|
519
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
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
|
+
}
|
|
520
295
|
}
|
|
521
296
|
else {
|
|
522
|
-
|
|
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
|
+
};
|
|
523
305
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const response = await fetch(`https://registry.npmjs.org/${name}`, {
|
|
330
|
+
headers: {
|
|
331
|
+
Accept: 'application/json',
|
|
332
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
if (!response.ok) {
|
|
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
|
+
};
|
|
344
|
+
}
|
|
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
|
+
};
|
|
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);
|
|
365
|
+
return {
|
|
366
|
+
packageInput: pkgInput,
|
|
367
|
+
packageName: name,
|
|
368
|
+
status: 'success',
|
|
369
|
+
error: null,
|
|
370
|
+
data: resultData,
|
|
371
|
+
message: `Successfully fetched versions for ${name}.`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
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
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}));
|
|
385
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
386
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
529
387
|
}
|
|
530
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);
|
|
531
393
|
return {
|
|
532
|
-
content: [
|
|
533
|
-
{
|
|
534
|
-
type: 'text',
|
|
535
|
-
text: `Error fetching package versions: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
536
|
-
},
|
|
537
|
-
],
|
|
394
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
538
395
|
isError: true,
|
|
539
396
|
};
|
|
540
397
|
}
|
|
541
398
|
}
|
|
542
|
-
async function handleNpmLatest(args) {
|
|
399
|
+
export async function handleNpmLatest(args) {
|
|
543
400
|
try {
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`);
|
|
548
|
-
if (!response.ok) {
|
|
549
|
-
throw new Error(`Failed to fetch latest version for ${pkg}: ${response.statusText}`);
|
|
550
|
-
}
|
|
551
|
-
const data = (await response.json());
|
|
552
|
-
text += `📦 Latest version of ${pkg}\n`;
|
|
553
|
-
text += `Version: ${data.version}\n`;
|
|
554
|
-
text += `Description: ${data.description || 'No description available'}\n`;
|
|
555
|
-
text += `Author: ${data.author?.name || 'Unknown'}\n`;
|
|
556
|
-
text += `License: ${data.license || 'Unknown'}\n`;
|
|
557
|
-
text += `Homepage: ${data.homepage || 'Not specified'}\n\n`;
|
|
558
|
-
text += '---\n\n';
|
|
401
|
+
const packagesToProcess = args.packages || [];
|
|
402
|
+
if (packagesToProcess.length === 0) {
|
|
403
|
+
throw new Error('No package names provided');
|
|
559
404
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
+
}
|
|
417
|
+
}
|
|
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
|
+
};
|
|
439
|
+
}
|
|
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
|
+
};
|
|
452
|
+
}
|
|
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
|
+
};
|
|
512
|
+
}
|
|
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 };
|
|
569
527
|
}
|
|
570
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);
|
|
571
533
|
return {
|
|
572
|
-
content: [
|
|
573
|
-
{
|
|
574
|
-
type: 'text',
|
|
575
|
-
text: `Error fetching latest version: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
576
|
-
},
|
|
577
|
-
],
|
|
534
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
578
535
|
isError: true,
|
|
579
536
|
};
|
|
580
537
|
}
|
|
581
538
|
}
|
|
582
|
-
async function handleNpmDeps(args) {
|
|
539
|
+
export async function handleNpmDeps(args) {
|
|
583
540
|
try {
|
|
584
541
|
const packagesToProcess = args.packages || [];
|
|
585
542
|
if (packagesToProcess.length === 0) {
|
|
586
543
|
throw new Error('No package names provided');
|
|
587
544
|
}
|
|
588
|
-
const
|
|
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
|
+
};
|
|
566
|
+
}
|
|
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
|
+
};
|
|
582
|
+
}
|
|
589
583
|
try {
|
|
590
|
-
const response = await fetch(`https://registry.npmjs.org/${
|
|
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
|
+
});
|
|
591
590
|
if (!response.ok) {
|
|
592
|
-
return {
|
|
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
|
+
};
|
|
593
598
|
}
|
|
594
599
|
const rawData = await response.json();
|
|
595
600
|
if (!isNpmPackageData(rawData)) {
|
|
596
|
-
return {
|
|
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
|
+
};
|
|
597
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);
|
|
598
626
|
return {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
627
|
+
package: finalPackageName,
|
|
628
|
+
status: 'success',
|
|
629
|
+
error: null,
|
|
630
|
+
data: depData,
|
|
631
|
+
message: `Dependencies for ${finalPackageName}`,
|
|
604
632
|
};
|
|
605
633
|
}
|
|
606
634
|
catch (error) {
|
|
607
|
-
return {
|
|
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
|
+
};
|
|
608
642
|
}
|
|
609
643
|
}));
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if ('error' in result) {
|
|
613
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
text += `📦 Dependencies for ${result.name}@${result.version}\n\n`;
|
|
617
|
-
if (Object.keys(result.dependencies).length > 0) {
|
|
618
|
-
text += 'Dependencies:\n';
|
|
619
|
-
for (const [dep, version] of Object.entries(result.dependencies)) {
|
|
620
|
-
text += `• ${dep}: ${version}\n`;
|
|
621
|
-
}
|
|
622
|
-
text += '\n';
|
|
623
|
-
}
|
|
624
|
-
if (Object.keys(result.devDependencies).length > 0) {
|
|
625
|
-
text += 'Dev Dependencies:\n';
|
|
626
|
-
for (const [dep, version] of Object.entries(result.devDependencies)) {
|
|
627
|
-
text += `• ${dep}: ${version}\n`;
|
|
628
|
-
}
|
|
629
|
-
text += '\n';
|
|
630
|
-
}
|
|
631
|
-
if (Object.keys(result.peerDependencies).length > 0) {
|
|
632
|
-
text += 'Peer Dependencies:\n';
|
|
633
|
-
for (const [dep, version] of Object.entries(result.peerDependencies)) {
|
|
634
|
-
text += `• ${dep}: ${version}\n`;
|
|
635
|
-
}
|
|
636
|
-
text += '\n';
|
|
637
|
-
}
|
|
638
|
-
text += '---\n\n';
|
|
639
|
-
}
|
|
640
|
-
return {
|
|
641
|
-
content: [{ type: 'text', text }],
|
|
642
|
-
isError: false,
|
|
643
|
-
};
|
|
644
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
645
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
644
646
|
}
|
|
645
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);
|
|
646
652
|
return {
|
|
647
|
-
content: [
|
|
648
|
-
{
|
|
649
|
-
type: 'text',
|
|
650
|
-
text: `Error fetching dependencies: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
651
|
-
},
|
|
652
|
-
],
|
|
653
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
653
654
|
isError: true,
|
|
654
655
|
};
|
|
655
656
|
}
|
|
656
657
|
}
|
|
657
|
-
async function handleNpmTypes(args) {
|
|
658
|
+
export async function handleNpmTypes(args) {
|
|
658
659
|
try {
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
660
|
+
const packagesToProcess = args.packages || [];
|
|
661
|
+
if (packagesToProcess.length === 0) {
|
|
662
|
+
throw new Error('No package names provided');
|
|
663
|
+
}
|
|
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
|
+
}
|
|
663
676
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const typesResponse = await fetch(`https://registry.npmjs.org/${typesPackage}/latest`).catch(() => null);
|
|
673
|
-
if (typesResponse?.ok) {
|
|
674
|
-
const typesData = (await typesResponse.json());
|
|
675
|
-
text += `📦 DefinitelyTyped package available: ${typesPackage}@${typesData.version}\n`;
|
|
676
|
-
text += `Install with: npm install -D ${typesPackage}`;
|
|
677
|
-
}
|
|
678
|
-
else if (!hasTypes) {
|
|
679
|
-
text += '❌ No TypeScript type definitions found';
|
|
680
|
-
}
|
|
681
|
-
return { name: pkg, text };
|
|
682
|
-
}));
|
|
683
|
-
let text = '';
|
|
684
|
-
for (const result of results) {
|
|
685
|
-
text += `${result.text}\n\n`;
|
|
686
|
-
if (results.indexOf(result) < results.length - 1) {
|
|
687
|
-
text += '---\n\n';
|
|
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
|
+
};
|
|
688
685
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
+
};
|
|
698
|
+
}
|
|
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
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
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);
|
|
756
|
+
return {
|
|
757
|
+
package: finalPackageName,
|
|
758
|
+
status: 'success',
|
|
759
|
+
error: null,
|
|
760
|
+
data: resultData,
|
|
761
|
+
message: `TypeScript information for ${finalPackageName}`,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
return {
|
|
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}.`,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}));
|
|
774
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
775
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
694
776
|
}
|
|
695
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);
|
|
696
782
|
return {
|
|
697
|
-
content: [
|
|
698
|
-
{ type: 'text', text: `Error checking TypeScript types: ${error.message}` },
|
|
699
|
-
],
|
|
783
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
700
784
|
isError: true,
|
|
701
785
|
};
|
|
702
786
|
}
|
|
703
787
|
}
|
|
704
|
-
async function handleNpmSize(args) {
|
|
788
|
+
export async function handleNpmSize(args) {
|
|
705
789
|
try {
|
|
706
790
|
const packagesToProcess = args.packages || [];
|
|
707
791
|
if (packagesToProcess.length === 0) {
|
|
708
792
|
throw new Error('No package names provided');
|
|
709
793
|
}
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
+
};
|
|
714
815
|
}
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
};
|
|
718
828
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
+
};
|
|
730
884
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
+
};
|
|
735
893
|
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
isError: false,
|
|
740
|
-
};
|
|
894
|
+
}));
|
|
895
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
896
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
741
897
|
}
|
|
742
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);
|
|
743
903
|
return {
|
|
744
|
-
content: [
|
|
745
|
-
{
|
|
746
|
-
type: 'text',
|
|
747
|
-
text: `Error fetching package sizes: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
748
|
-
},
|
|
749
|
-
],
|
|
904
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
750
905
|
isError: true,
|
|
751
906
|
};
|
|
752
907
|
}
|
|
753
908
|
}
|
|
754
|
-
async function handleNpmVulnerabilities(args) {
|
|
909
|
+
export async function handleNpmVulnerabilities(args) {
|
|
755
910
|
try {
|
|
756
911
|
const packagesToProcess = args.packages || [];
|
|
757
912
|
if (packagesToProcess.length === 0) {
|
|
758
913
|
throw new Error('No package names provided');
|
|
759
914
|
}
|
|
760
|
-
const
|
|
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
|
+
}
|
|
761
953
|
const response = await fetch('https://api.osv.dev/v1/query', {
|
|
762
954
|
method: 'POST',
|
|
763
955
|
headers: {
|
|
764
956
|
'Content-Type': 'application/json',
|
|
765
957
|
},
|
|
766
|
-
body: JSON.stringify(
|
|
767
|
-
package: {
|
|
768
|
-
name: pkg,
|
|
769
|
-
ecosystem: 'npm',
|
|
770
|
-
},
|
|
771
|
-
}),
|
|
958
|
+
body: JSON.stringify(osvBody),
|
|
772
959
|
});
|
|
960
|
+
const queryVersionSpecified = !!version;
|
|
773
961
|
if (!response.ok) {
|
|
774
|
-
|
|
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;
|
|
775
971
|
}
|
|
776
972
|
const data = (await response.json());
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if ('error' in result) {
|
|
782
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
text += `📦 ${result.name}\n`;
|
|
786
|
-
if (result.vulns.length === 0) {
|
|
787
|
-
text += '✅ No known vulnerabilities\n\n';
|
|
973
|
+
const vulns = data.vulns || [];
|
|
974
|
+
let message;
|
|
975
|
+
if (vulns.length === 0) {
|
|
976
|
+
message = `No known vulnerabilities found${queryVersionSpecified ? ' for the specified version' : ''}.`;
|
|
788
977
|
}
|
|
789
978
|
else {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
}
|
|
799
1028
|
}
|
|
800
|
-
text += '\n';
|
|
801
1029
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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);
|
|
1055
|
+
return {
|
|
1056
|
+
package: packageNameForOutput,
|
|
1057
|
+
versionQueried: version || null,
|
|
1058
|
+
status: 'success',
|
|
1059
|
+
vulnerabilities: processedVulns,
|
|
1060
|
+
message: message,
|
|
1061
|
+
};
|
|
1062
|
+
}));
|
|
1063
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
1064
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
809
1065
|
}
|
|
810
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);
|
|
811
1071
|
return {
|
|
812
|
-
content: [
|
|
813
|
-
{
|
|
814
|
-
type: 'text',
|
|
815
|
-
text: `Error checking vulnerabilities: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
816
|
-
},
|
|
817
|
-
],
|
|
1072
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
818
1073
|
isError: true,
|
|
819
1074
|
};
|
|
820
1075
|
}
|
|
821
1076
|
}
|
|
822
|
-
async function handleNpmTrends(args) {
|
|
1077
|
+
export async function handleNpmTrends(args) {
|
|
823
1078
|
try {
|
|
824
|
-
const
|
|
825
|
-
|
|
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 = {
|
|
826
1087
|
'last-week': 7,
|
|
827
1088
|
'last-month': 30,
|
|
828
1089
|
'last-year': 365,
|
|
829
1090
|
};
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
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;
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
833
1099
|
return {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1100
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1101
|
+
packageName: 'unknown_package_input',
|
|
1102
|
+
status: 'error',
|
|
1103
|
+
error: 'Invalid package input type',
|
|
1104
|
+
data: null,
|
|
837
1105
|
};
|
|
838
1106
|
}
|
|
839
|
-
|
|
840
|
-
if (!isNpmDownloadsData(data)) {
|
|
1107
|
+
if (!name) {
|
|
841
1108
|
return {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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,
|
|
845
1182
|
};
|
|
846
1183
|
}
|
|
847
|
-
return {
|
|
848
|
-
name: pkg,
|
|
849
|
-
downloads: data.downloads,
|
|
850
|
-
success: true,
|
|
851
|
-
};
|
|
852
1184
|
}));
|
|
853
|
-
let
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
text += `📦 ${result.name}\n`;
|
|
862
|
-
text += `Total downloads: ${result.downloads.toLocaleString()}\n`;
|
|
863
|
-
text += `Average daily downloads: ${Math.round(result.downloads / periodDays[period]).toLocaleString()}\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
|
+
}
|
|
864
1192
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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,
|
|
877
1209
|
};
|
|
1210
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1211
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
878
1212
|
}
|
|
879
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);
|
|
880
1220
|
return {
|
|
881
|
-
content: [
|
|
882
|
-
{ type: 'text', text: `Error fetching download trends: ${error.message}` },
|
|
883
|
-
],
|
|
1221
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
884
1222
|
isError: true,
|
|
885
1223
|
};
|
|
886
1224
|
}
|
|
887
1225
|
}
|
|
888
|
-
async function handleNpmCompare(args) {
|
|
1226
|
+
export async function handleNpmCompare(args) {
|
|
889
1227
|
try {
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
+
}
|
|
1244
|
+
}
|
|
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
|
+
};
|
|
902
1349
|
}
|
|
903
|
-
return {
|
|
904
|
-
name: pkg,
|
|
905
|
-
version: info.version,
|
|
906
|
-
description: info.description,
|
|
907
|
-
downloads: downloads.downloads,
|
|
908
|
-
license: info.license,
|
|
909
|
-
dependencies: Object.keys(info.dependencies || {}).length,
|
|
910
|
-
};
|
|
911
1350
|
}));
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
// Table rows
|
|
917
|
-
for (const pkg of results) {
|
|
918
|
-
text += `${pkg.name} | ${pkg.version} | ${pkg.downloads.toLocaleString()} | ${pkg.dependencies} | ${pkg.license || 'N/A'}\n`;
|
|
919
|
-
}
|
|
920
|
-
return {
|
|
921
|
-
content: [{ type: 'text', text }],
|
|
922
|
-
isError: false,
|
|
1351
|
+
const finalResponse = {
|
|
1352
|
+
queryPackages: args.packages,
|
|
1353
|
+
results: processedResults,
|
|
1354
|
+
message: `Comparison data for ${args.packages.length} package(s).`,
|
|
923
1355
|
};
|
|
1356
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1357
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
924
1358
|
}
|
|
925
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);
|
|
926
1365
|
return {
|
|
927
|
-
content: [{ type: 'text', text:
|
|
1366
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
928
1367
|
isError: true,
|
|
929
1368
|
};
|
|
930
1369
|
}
|
|
931
1370
|
}
|
|
932
1371
|
// Function to get package quality metrics
|
|
933
|
-
async function handleNpmQuality(args) {
|
|
1372
|
+
export async function handleNpmQuality(args) {
|
|
934
1373
|
try {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
|
939
1383
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1384
|
+
else {
|
|
1385
|
+
return {
|
|
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.',
|
|
1392
|
+
};
|
|
943
1393
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}),
|
|
954
|
-
};
|
|
955
|
-
}));
|
|
956
|
-
let text = '📊 Quality Metrics\n\n';
|
|
957
|
-
for (const result of results) {
|
|
958
|
-
if ('error' in result) {
|
|
959
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
text += `📦 ${result.name}\n`;
|
|
963
|
-
text += `- Overall Score: ${result.score}\n`;
|
|
964
|
-
text +=
|
|
965
|
-
'- Note: Detailed metrics (tests, coverage, linting, types) are no longer provided by the API\n\n';
|
|
966
|
-
}
|
|
967
|
-
return {
|
|
968
|
-
content: [{ type: 'text', text }],
|
|
969
|
-
isError: false,
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
catch (error) {
|
|
973
|
-
return {
|
|
974
|
-
content: [
|
|
975
|
-
{
|
|
976
|
-
type: 'text',
|
|
977
|
-
text: `Error fetching quality metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
978
|
-
},
|
|
979
|
-
],
|
|
980
|
-
isError: true,
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
async function handleNpmMaintenance(args) {
|
|
985
|
-
try {
|
|
986
|
-
const results = await Promise.all(args.packages.map(async (pkg) => {
|
|
987
|
-
const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(pkg)}`);
|
|
988
|
-
if (!response.ok) {
|
|
989
|
-
return { name: pkg, error: `Failed to fetch maintenance data: ${response.statusText}` };
|
|
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
|
+
};
|
|
990
1403
|
}
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
|
|
1404
|
+
const cacheKey = generateCacheKey('handleNpmQuality', name);
|
|
1405
|
+
const cachedData = cacheGet(cacheKey);
|
|
1406
|
+
if (cachedData) {
|
|
1407
|
+
return {
|
|
1408
|
+
packageInput: pkgInput,
|
|
1409
|
+
packageName: name, // Or cachedData.packageName if stored differently
|
|
1410
|
+
status: 'success_cache',
|
|
1411
|
+
error: null,
|
|
1412
|
+
data: cachedData,
|
|
1413
|
+
message: `Quality score for ${name} (version analyzed: ${cachedData.versionInScore}) from cache.`,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
try {
|
|
1417
|
+
const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`, {
|
|
1418
|
+
headers: {
|
|
1419
|
+
Accept: 'application/json',
|
|
1420
|
+
'User-Agent': 'NPM-Sentinel-MCP',
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1423
|
+
if (!response.ok) {
|
|
1424
|
+
let errorMsg = `Failed to fetch quality data: ${response.status} ${response.statusText}`;
|
|
1425
|
+
if (response.status === 404) {
|
|
1426
|
+
errorMsg = `Package ${name} not found on npms.io.`;
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
packageInput: pkgInput,
|
|
1430
|
+
packageName: name,
|
|
1431
|
+
status: 'error',
|
|
1432
|
+
error: errorMsg,
|
|
1433
|
+
data: null,
|
|
1434
|
+
message: `Could not retrieve quality information for ${name}.`,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
const rawData = await response.json();
|
|
1438
|
+
if (!isValidNpmsResponse(rawData)) {
|
|
1439
|
+
return {
|
|
1440
|
+
packageInput: pkgInput,
|
|
1441
|
+
packageName: name,
|
|
1442
|
+
status: 'error',
|
|
1443
|
+
error: 'Invalid or incomplete response from npms.io API for quality data',
|
|
1444
|
+
data: null,
|
|
1445
|
+
message: `Received malformed quality data for ${name}.`,
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
const { score, collected, analyzedAt } = rawData;
|
|
1449
|
+
const qualityScore = score.detail.quality;
|
|
1450
|
+
const qualityData = {
|
|
1451
|
+
analyzedAt: analyzedAt,
|
|
1452
|
+
versionInScore: collected.metadata.version,
|
|
1453
|
+
qualityScore: qualityScore,
|
|
1454
|
+
// Detailed sub-metrics like tests, coverage, linting, types are no longer directly provided
|
|
1455
|
+
// by the npms.io v2 API in the same way. The overall quality score is the primary metric.
|
|
1456
|
+
};
|
|
1457
|
+
const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/) ? 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
|
+
};
|
|
994
1477
|
}
|
|
995
|
-
const maintenance = rawData.score.detail.maintenance;
|
|
996
|
-
return {
|
|
997
|
-
name: pkg,
|
|
998
|
-
score: Math.round(maintenance * 100) / 100,
|
|
999
|
-
};
|
|
1000
1478
|
}));
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
text += `📦 ${result.name}\n`;
|
|
1008
|
-
text += `- Maintenance Score: ${result.score}\n\n`;
|
|
1009
|
-
}
|
|
1010
|
-
return {
|
|
1011
|
-
content: [{ type: 'text', text }],
|
|
1012
|
-
isError: false,
|
|
1479
|
+
const finalResponse = {
|
|
1480
|
+
queryPackages: args.packages,
|
|
1481
|
+
results: processedResults,
|
|
1013
1482
|
};
|
|
1483
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1484
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1014
1485
|
}
|
|
1015
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);
|
|
1016
1492
|
return {
|
|
1017
|
-
content: [
|
|
1018
|
-
{
|
|
1019
|
-
type: 'text',
|
|
1020
|
-
text: `Error fetching maintenance metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1493
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1023
1494
|
isError: true,
|
|
1024
1495
|
};
|
|
1025
1496
|
}
|
|
1026
1497
|
}
|
|
1027
|
-
async function
|
|
1498
|
+
export async function handleNpmMaintenance(args) {
|
|
1028
1499
|
try {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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;
|
|
1033
1509
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1510
|
+
else {
|
|
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
|
+
};
|
|
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
|
+
};
|
|
1591
|
+
}
|
|
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
|
+
};
|
|
1037
1601
|
}
|
|
1038
|
-
const popularityScore = data.score.detail.popularity;
|
|
1039
|
-
return {
|
|
1040
|
-
name: pkg,
|
|
1041
|
-
...NpmPopularitySchema.parse({
|
|
1042
|
-
score: Math.round(popularityScore * 100) / 100,
|
|
1043
|
-
stars: 0,
|
|
1044
|
-
downloads: 0,
|
|
1045
|
-
dependents: 0,
|
|
1046
|
-
communityInterest: 0,
|
|
1047
|
-
}),
|
|
1048
|
-
};
|
|
1049
1602
|
}));
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
text += `❌ ${result.name}: ${result.error}\n\n`;
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
text += `📦 ${result.name}\n`;
|
|
1057
|
-
text += `- Overall Score: ${result.score}\n`;
|
|
1058
|
-
text += '- Note: Detailed metrics are no longer provided by the API\n\n';
|
|
1059
|
-
}
|
|
1060
|
-
return {
|
|
1061
|
-
content: [{ type: 'text', text }],
|
|
1062
|
-
isError: false,
|
|
1603
|
+
const finalResponse = {
|
|
1604
|
+
queryPackages: args.packages,
|
|
1605
|
+
results: processedResults,
|
|
1063
1606
|
};
|
|
1607
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1608
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1064
1609
|
}
|
|
1065
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);
|
|
1066
1616
|
return {
|
|
1067
|
-
content: [
|
|
1068
|
-
{
|
|
1069
|
-
type: 'text',
|
|
1070
|
-
text: `Error fetching popularity metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1071
|
-
},
|
|
1072
|
-
],
|
|
1617
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1073
1618
|
isError: true,
|
|
1074
1619
|
};
|
|
1075
1620
|
}
|
|
1076
1621
|
}
|
|
1077
|
-
async function handleNpmMaintainers(args) {
|
|
1622
|
+
export async function handleNpmMaintainers(args) {
|
|
1078
1623
|
try {
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
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 {
|
|
1082
1635
|
return {
|
|
1083
|
-
|
|
1084
|
-
|
|
1636
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1637
|
+
packageName: 'unknown_package_input',
|
|
1638
|
+
status: 'error',
|
|
1639
|
+
error: 'Invalid package input type',
|
|
1640
|
+
data: null,
|
|
1085
1641
|
};
|
|
1086
1642
|
}
|
|
1087
|
-
if (!
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
+
};
|
|
1093
1651
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
text += `📦 ${result.name}\n`;
|
|
1106
|
-
text += `${'-'.repeat(40)}\n`;
|
|
1107
|
-
const maintainers = result.maintainers || [];
|
|
1108
|
-
if (maintainers.length === 0) {
|
|
1109
|
-
text += '⚠️ No maintainers found.\n';
|
|
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
|
+
};
|
|
1110
1663
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
+
};
|
|
1116
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
|
+
};
|
|
1117
1708
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
+
};
|
|
1120
1717
|
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
text,
|
|
1127
|
-
},
|
|
1128
|
-
],
|
|
1129
|
-
isError: false,
|
|
1718
|
+
}));
|
|
1719
|
+
const finalResponse = {
|
|
1720
|
+
queryPackages: args.packages,
|
|
1721
|
+
results: processedResults,
|
|
1722
|
+
message: `Maintainer information for ${args.packages.length} package(s).`,
|
|
1130
1723
|
};
|
|
1724
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1725
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1131
1726
|
}
|
|
1132
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);
|
|
1133
1733
|
return {
|
|
1134
|
-
content: [
|
|
1135
|
-
{
|
|
1136
|
-
type: 'text',
|
|
1137
|
-
text: `Error fetching package maintainers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1138
|
-
},
|
|
1139
|
-
],
|
|
1734
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1140
1735
|
isError: true,
|
|
1141
1736
|
};
|
|
1142
1737
|
}
|
|
1143
1738
|
}
|
|
1144
|
-
async function handleNpmScore(args) {
|
|
1739
|
+
export async function handleNpmScore(args) {
|
|
1145
1740
|
try {
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
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 {
|
|
1149
1752
|
return {
|
|
1150
|
-
|
|
1151
|
-
|
|
1753
|
+
packageInput: JSON.stringify(pkgInput),
|
|
1754
|
+
packageName: 'unknown_package_input',
|
|
1755
|
+
status: 'error',
|
|
1756
|
+
error: 'Invalid package input type',
|
|
1757
|
+
data: null,
|
|
1152
1758
|
};
|
|
1153
1759
|
}
|
|
1154
|
-
if (!
|
|
1155
|
-
|
|
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
|
+
};
|
|
1156
1768
|
}
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1769
|
+
const cacheKey = generateCacheKey('handleNpmScore', name);
|
|
1770
|
+
const cachedData = cacheGet(cacheKey);
|
|
1771
|
+
if (cachedData) {
|
|
1159
1772
|
return {
|
|
1160
|
-
|
|
1161
|
-
|
|
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.`,
|
|
1162
1779
|
};
|
|
1163
1780
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
+
};
|
|
1201
1861
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
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).`,
|
|
1212
1876
|
};
|
|
1877
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
1878
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1213
1879
|
}
|
|
1214
1880
|
catch (error) {
|
|
1215
|
-
|
|
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);
|
|
1216
1886
|
return {
|
|
1217
|
-
content: [
|
|
1218
|
-
{
|
|
1219
|
-
type: 'text',
|
|
1220
|
-
text: `Error fetching package scores: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1221
|
-
},
|
|
1222
|
-
],
|
|
1887
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1223
1888
|
isError: true,
|
|
1224
1889
|
};
|
|
1225
1890
|
}
|
|
1226
1891
|
}
|
|
1227
|
-
async function handleNpmPackageReadme(args) {
|
|
1892
|
+
export async function handleNpmPackageReadme(args) {
|
|
1228
1893
|
try {
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
+
}
|
|
1911
|
+
}
|
|
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
|
+
};
|
|
1922
|
+
}
|
|
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
|
+
};
|
|
1233
1933
|
}
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
+
};
|
|
1237
1947
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
+
};
|
|
1241
2009
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
+
};
|
|
1245
2020
|
}
|
|
1246
|
-
return { name: pkg, version: latestVersion, text: readme };
|
|
1247
2021
|
}));
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
text += `${'='.repeat(80)}\n\n`;
|
|
1253
|
-
text += result.text;
|
|
1254
|
-
if (results.indexOf(result) < results.length - 1) {
|
|
1255
|
-
text += '\n\n';
|
|
1256
|
-
text += `${'='.repeat(80)}\n\n`;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
return {
|
|
1260
|
-
content: [{ type: 'text', text }],
|
|
1261
|
-
isError: false,
|
|
2022
|
+
const finalResponse = {
|
|
2023
|
+
queryPackages: args.packages,
|
|
2024
|
+
results: processedResults,
|
|
2025
|
+
message: `README fetching status for ${args.packages.length} package(s).`,
|
|
1262
2026
|
};
|
|
2027
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2028
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1263
2029
|
}
|
|
1264
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);
|
|
1265
2036
|
return {
|
|
1266
|
-
content: [
|
|
1267
|
-
{
|
|
1268
|
-
type: 'text',
|
|
1269
|
-
text: `Error fetching READMEs: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1270
|
-
},
|
|
1271
|
-
],
|
|
2037
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1272
2038
|
isError: true,
|
|
1273
2039
|
};
|
|
1274
2040
|
}
|
|
1275
2041
|
}
|
|
1276
|
-
async function handleNpmSearch(args) {
|
|
2042
|
+
export async function handleNpmSearch(args) {
|
|
1277
2043
|
try {
|
|
2044
|
+
const query = args.query;
|
|
1278
2045
|
const limit = args.limit || 10;
|
|
1279
|
-
|
|
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}`);
|
|
1280
2057
|
if (!response.ok) {
|
|
1281
|
-
throw new Error(`Failed to search packages: ${response.statusText}`);
|
|
2058
|
+
throw new Error(`Failed to search packages: ${response.status} ${response.statusText}`);
|
|
1282
2059
|
}
|
|
1283
2060
|
const rawData = await response.json();
|
|
1284
2061
|
const parseResult = NpmSearchResultSchema.safeParse(rawData);
|
|
1285
2062
|
if (!parseResult.success) {
|
|
1286
|
-
|
|
2063
|
+
console.error('Invalid search results data received:', parseResult.error.issues);
|
|
2064
|
+
throw new Error('Invalid search results data received from NPM registry.');
|
|
1287
2065
|
}
|
|
1288
2066
|
const { objects, total } = parseResult.data;
|
|
1289
|
-
|
|
1290
|
-
text += `Found ${total.toLocaleString()} packages (showing top ${limit})\n\n`;
|
|
1291
|
-
for (const result of objects) {
|
|
2067
|
+
const resultsData = objects.map((result) => {
|
|
1292
2068
|
const pkg = result.package;
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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}.`,
|
|
1318
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 };
|
|
1319
2107
|
}
|
|
1320
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);
|
|
1321
2117
|
return {
|
|
1322
|
-
content: [
|
|
1323
|
-
{
|
|
1324
|
-
type: 'text',
|
|
1325
|
-
text: `Error searching packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1326
|
-
},
|
|
1327
|
-
],
|
|
2118
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1328
2119
|
isError: true,
|
|
1329
2120
|
};
|
|
1330
2121
|
}
|
|
1331
2122
|
}
|
|
1332
2123
|
// License compatibility checker
|
|
1333
|
-
async function handleNpmLicenseCompatibility(args) {
|
|
2124
|
+
export async function handleNpmLicenseCompatibility(args) {
|
|
1334
2125
|
try {
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
+
};
|
|
1339
2236
|
}
|
|
1340
|
-
const data = (await response.json());
|
|
1341
|
-
return {
|
|
1342
|
-
package: pkg,
|
|
1343
|
-
license: data.license || 'UNKNOWN',
|
|
1344
|
-
};
|
|
1345
2237
|
}));
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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.');
|
|
1350
2251
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const hasGPL = licenses.some(({ license }) => license?.includes('GPL'));
|
|
1354
|
-
const hasMIT = licenses.some(({ license }) => license === 'MIT');
|
|
1355
|
-
const hasApache = licenses.some(({ license }) => license?.includes('Apache'));
|
|
1356
|
-
const hasUnknown = licenses.some(({ license }) => license === 'UNKNOWN');
|
|
1357
|
-
text += 'Compatibility Analysis:\n';
|
|
1358
|
-
if (hasUnknown) {
|
|
1359
|
-
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.');
|
|
1360
2254
|
}
|
|
1361
2255
|
if (hasGPL) {
|
|
1362
|
-
|
|
2256
|
+
warnings.push('Contains GPL licensed code. Resulting work may need to be GPL licensed.');
|
|
1363
2257
|
if (hasMIT || hasApache) {
|
|
1364
|
-
|
|
2258
|
+
warnings.push('Mixed GPL with potentially incompatible licenses (e.g., MIT, Apache). Review carefully for compliance.');
|
|
1365
2259
|
}
|
|
1366
2260
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
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.';
|
|
1369
2265
|
}
|
|
1370
|
-
else if (
|
|
1371
|
-
|
|
2266
|
+
else if (licensesFound.length === 0 && allSuccess) {
|
|
2267
|
+
summary = 'No license information found for the queried packages.';
|
|
1372
2268
|
}
|
|
1373
|
-
else if (
|
|
1374
|
-
|
|
2269
|
+
else if (licensesFound.length > 0 && !hasGPL && !hasUnknown) {
|
|
2270
|
+
summary = 'Licenses found appear to be generally compatible (non-GPL, known licenses).';
|
|
1375
2271
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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.`,
|
|
1381
2282
|
};
|
|
2283
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2284
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1382
2285
|
}
|
|
1383
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);
|
|
1384
2293
|
return {
|
|
1385
|
-
content: [
|
|
1386
|
-
{
|
|
1387
|
-
type: 'text',
|
|
1388
|
-
text: `Error analyzing license compatibility: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1389
|
-
},
|
|
1390
|
-
],
|
|
2294
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1391
2295
|
isError: true,
|
|
1392
2296
|
};
|
|
1393
2297
|
}
|
|
1394
2298
|
}
|
|
1395
2299
|
// Repository statistics analyzer
|
|
1396
|
-
async function handleNpmRepoStats(args) {
|
|
2300
|
+
export async function handleNpmRepoStats(args) {
|
|
1397
2301
|
try {
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
if (!npmResponse.ok) {
|
|
1402
|
-
throw new Error(`Failed to fetch npm info for ${pkg}: ${npmResponse.statusText}`);
|
|
1403
|
-
}
|
|
1404
|
-
const npmData = (await npmResponse.json());
|
|
1405
|
-
if (!npmData.repository?.url) {
|
|
1406
|
-
return { name: pkg, text: `No repository URL found for package ${pkg}` };
|
|
1407
|
-
}
|
|
1408
|
-
// Extract GitHub repo info from URL
|
|
1409
|
-
const repoUrl = npmData.repository.url;
|
|
1410
|
-
const match = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
1411
|
-
if (!match) {
|
|
1412
|
-
return { name: pkg, text: `Could not parse GitHub repository URL: ${repoUrl}` };
|
|
1413
|
-
}
|
|
1414
|
-
const [, owner, repo] = match;
|
|
1415
|
-
// Fetch repository stats from GitHub API
|
|
1416
|
-
const githubResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
1417
|
-
headers: {
|
|
1418
|
-
Accept: 'application/vnd.github.v3+json',
|
|
1419
|
-
'User-Agent': 'MCP-Server',
|
|
1420
|
-
},
|
|
1421
|
-
});
|
|
1422
|
-
if (!githubResponse.ok) {
|
|
1423
|
-
throw new Error(`Failed to fetch GitHub stats: ${githubResponse.statusText}`);
|
|
1424
|
-
}
|
|
1425
|
-
const data = (await githubResponse.json());
|
|
1426
|
-
const text = [
|
|
1427
|
-
`${'='.repeat(80)}`,
|
|
1428
|
-
`📊 Repository Statistics for ${pkg}`,
|
|
1429
|
-
`${'='.repeat(80)}\n`,
|
|
1430
|
-
'🌟 Engagement Metrics',
|
|
1431
|
-
`${'─'.repeat(40)}`,
|
|
1432
|
-
`• Stars: ${data.stargazers_count.toLocaleString().padEnd(10)} ⭐`,
|
|
1433
|
-
`• Forks: ${data.forks_count.toLocaleString().padEnd(10)} 🔄`,
|
|
1434
|
-
`• Watchers: ${data.watchers_count.toLocaleString().padEnd(10)} 👀`,
|
|
1435
|
-
`• Open Issues: ${data.open_issues_count.toLocaleString().padEnd(10)} 🔍\n`,
|
|
1436
|
-
'📅 Timeline',
|
|
1437
|
-
`${'─'.repeat(40)}`,
|
|
1438
|
-
`• Created: ${new Date(data.created_at).toLocaleDateString()}`,
|
|
1439
|
-
`• Last Updated: ${new Date(data.updated_at).toLocaleDateString()}\n`,
|
|
1440
|
-
'🔧 Repository Details',
|
|
1441
|
-
`${'─'.repeat(40)}`,
|
|
1442
|
-
`• Default Branch: ${data.default_branch}`,
|
|
1443
|
-
`• Wiki Enabled: ${data.has_wiki ? 'Yes' : 'No'}\n`,
|
|
1444
|
-
'🏷️ Topics',
|
|
1445
|
-
`${'─'.repeat(40)}`,
|
|
1446
|
-
data.topics.length
|
|
1447
|
-
? data.topics.map((topic) => `• ${topic}`).join('\n')
|
|
1448
|
-
: '• No topics found',
|
|
1449
|
-
'',
|
|
1450
|
-
].join('\n');
|
|
1451
|
-
return { name: pkg, text };
|
|
1452
|
-
}));
|
|
1453
|
-
let text = '';
|
|
1454
|
-
for (const result of results) {
|
|
1455
|
-
text += result.text;
|
|
1456
|
-
if (results.indexOf(result) < results.length - 1) {
|
|
1457
|
-
text += '\n\n';
|
|
1458
|
-
}
|
|
2302
|
+
const packagesToProcess = args.packages || [];
|
|
2303
|
+
if (packagesToProcess.length === 0) {
|
|
2304
|
+
throw new Error('No package names provided for repository statistics analysis.');
|
|
1459
2305
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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).`,
|
|
1463
2454
|
};
|
|
2455
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2456
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1464
2457
|
}
|
|
1465
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);
|
|
1466
2464
|
return {
|
|
1467
|
-
content: [
|
|
1468
|
-
{
|
|
1469
|
-
type: 'text',
|
|
1470
|
-
text: `Error analyzing repository stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1471
|
-
},
|
|
1472
|
-
],
|
|
2465
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1473
2466
|
isError: true,
|
|
1474
2467
|
};
|
|
1475
2468
|
}
|
|
1476
2469
|
}
|
|
1477
|
-
async function handleNpmDeprecated(args) {
|
|
2470
|
+
export async function handleNpmDeprecated(args) {
|
|
1478
2471
|
try {
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
throw new Error('No latest version found');
|
|
1492
|
-
}
|
|
1493
|
-
const latestVersionInfo = rawData.versions[latestVersion];
|
|
1494
|
-
const dependencies = {
|
|
1495
|
-
...(latestVersionInfo.dependencies || {}),
|
|
1496
|
-
...(latestVersionInfo.devDependencies || {}),
|
|
1497
|
-
...(latestVersionInfo.peerDependencies || {}),
|
|
1498
|
-
};
|
|
1499
|
-
// Check each dependency
|
|
1500
|
-
const deprecatedDeps = [];
|
|
1501
|
-
await Promise.all(Object.entries(dependencies).map(async ([dep, version]) => {
|
|
1502
|
-
try {
|
|
1503
|
-
const depResponse = await fetch(`https://registry.npmjs.org/${dep}`);
|
|
1504
|
-
if (!depResponse.ok)
|
|
1505
|
-
return;
|
|
1506
|
-
const depData = (await depResponse.json());
|
|
1507
|
-
const depVersion = version.replace(/[^0-9.]/g, '');
|
|
1508
|
-
if (depData.versions?.[depVersion]?.deprecated) {
|
|
1509
|
-
deprecatedDeps.push({
|
|
1510
|
-
name: dep,
|
|
1511
|
-
version: depVersion,
|
|
1512
|
-
message: depData.versions[depVersion].deprecated || 'No message provided',
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
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);
|
|
1515
2484
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
2485
|
+
else {
|
|
2486
|
+
name = pkgInput;
|
|
1518
2487
|
}
|
|
1519
|
-
}));
|
|
1520
|
-
// Check if the package itself is deprecated
|
|
1521
|
-
const isDeprecated = latestVersionInfo.deprecated;
|
|
1522
|
-
let text = `📦 Deprecation Check for ${pkg}@${latestVersion}\n\n`;
|
|
1523
|
-
if (isDeprecated) {
|
|
1524
|
-
text += '⚠️ WARNING: This package is deprecated!\n';
|
|
1525
|
-
text += `Deprecation message: ${latestVersionInfo.deprecated}\n\n`;
|
|
1526
2488
|
}
|
|
1527
2489
|
else {
|
|
1528
|
-
|
|
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
|
+
};
|
|
1529
2497
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
+
};
|
|
2510
|
+
}
|
|
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
|
+
};
|
|
1535
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
|
+
}
|
|
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
|
+
};
|
|
1536
2640
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
+
};
|
|
1539
2650
|
}
|
|
1540
|
-
return { name: pkg, text };
|
|
1541
2651
|
}));
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
text += result.text;
|
|
1545
|
-
}
|
|
1546
|
-
return {
|
|
1547
|
-
content: [{ type: 'text', text }],
|
|
1548
|
-
isError: false,
|
|
1549
|
-
};
|
|
2652
|
+
const responseJson = JSON.stringify({ results: processedResults }, null, 2);
|
|
2653
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1550
2654
|
}
|
|
1551
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);
|
|
1552
2661
|
return {
|
|
1553
|
-
content: [
|
|
1554
|
-
{
|
|
1555
|
-
type: 'text',
|
|
1556
|
-
text: `Error checking deprecated packages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1557
|
-
},
|
|
1558
|
-
],
|
|
2662
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1559
2663
|
isError: true,
|
|
1560
2664
|
};
|
|
1561
2665
|
}
|
|
1562
2666
|
}
|
|
1563
|
-
async function handleNpmChangelogAnalysis(args) {
|
|
2667
|
+
export async function handleNpmChangelogAnalysis(args) {
|
|
1564
2668
|
try {
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
return { name: pkg, text: `No repository found for package ${pkg}` };
|
|
1578
|
-
}
|
|
1579
|
-
// Extract GitHub repo info from URL
|
|
1580
|
-
const match = repository.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
1581
|
-
if (!match) {
|
|
1582
|
-
return { name: pkg, text: `Could not parse GitHub repository URL: ${repository}` };
|
|
1583
|
-
}
|
|
1584
|
-
const [, owner, repo] = match;
|
|
1585
|
-
// Check common changelog file names
|
|
1586
|
-
const changelogFiles = [
|
|
1587
|
-
'CHANGELOG.md',
|
|
1588
|
-
'changelog.md',
|
|
1589
|
-
'CHANGES.md',
|
|
1590
|
-
'changes.md',
|
|
1591
|
-
'HISTORY.md',
|
|
1592
|
-
'history.md',
|
|
1593
|
-
'NEWS.md',
|
|
1594
|
-
'news.md',
|
|
1595
|
-
'RELEASES.md',
|
|
1596
|
-
'releases.md',
|
|
1597
|
-
];
|
|
1598
|
-
let changelog = null;
|
|
1599
|
-
for (const file of changelogFiles) {
|
|
1600
|
-
try {
|
|
1601
|
-
const response = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/master/${file}`);
|
|
1602
|
-
if (response.ok) {
|
|
1603
|
-
changelog = await response.text();
|
|
1604
|
-
break;
|
|
1605
|
-
}
|
|
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);
|
|
1606
2681
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
2682
|
+
else {
|
|
2683
|
+
name = pkgInput;
|
|
1609
2684
|
}
|
|
1610
2685
|
}
|
|
1611
|
-
// Get release information from GitHub API
|
|
1612
|
-
const githubResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`, {
|
|
1613
|
-
headers: {
|
|
1614
|
-
Accept: 'application/vnd.github.v3+json',
|
|
1615
|
-
'User-Agent': 'MCP-Server',
|
|
1616
|
-
},
|
|
1617
|
-
});
|
|
1618
|
-
const releases = (githubResponse.ok ? await githubResponse.json() : []);
|
|
1619
|
-
let text = `📋 Changelog Analysis for ${pkg}\n\n`;
|
|
1620
|
-
// Analyze version history from npm
|
|
1621
|
-
const versions = Object.keys(npmData.versions || {}).sort((a, b) => {
|
|
1622
|
-
const [aMajor = 0, aMinor = 0] = a.split('.').map(Number);
|
|
1623
|
-
const [bMajor = 0, bMinor = 0] = b.split('.').map(Number);
|
|
1624
|
-
return bMajor - aMajor || bMinor - aMinor;
|
|
1625
|
-
});
|
|
1626
|
-
text += '📦 Version History:\n';
|
|
1627
|
-
text += `• Total versions: ${versions.length}\n`;
|
|
1628
|
-
text += `• Latest version: ${versions[0]}\n`;
|
|
1629
|
-
text += `• First version: ${versions[versions.length - 1]}\n\n`;
|
|
1630
|
-
if (changelog) {
|
|
1631
|
-
text += '📝 Changelog found!\n\n';
|
|
1632
|
-
// Extract and analyze the last few versions from changelog
|
|
1633
|
-
const recentChanges = changelog.split('\n').slice(0, 20).join('\n');
|
|
1634
|
-
text += `Recent changes:\n${recentChanges}\n...\n\n`;
|
|
1635
|
-
}
|
|
1636
2686
|
else {
|
|
1637
|
-
|
|
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
|
+
};
|
|
1638
2696
|
}
|
|
1639
|
-
if (
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
+
};
|
|
2719
|
+
}
|
|
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;
|
|
1648
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
|
+
}
|
|
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;
|
|
1649
2859
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
|
1652
2871
|
}
|
|
1653
|
-
return { name: pkg, text };
|
|
1654
2872
|
}));
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
}
|
|
1659
|
-
return {
|
|
1660
|
-
content: [{ type: 'text', text }],
|
|
1661
|
-
isError: false,
|
|
2873
|
+
const finalResponse = {
|
|
2874
|
+
queryPackages: args.packages,
|
|
2875
|
+
results: processedResults,
|
|
1662
2876
|
};
|
|
2877
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
2878
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1663
2879
|
}
|
|
1664
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);
|
|
1665
2886
|
return {
|
|
1666
|
-
content: [
|
|
1667
|
-
{
|
|
1668
|
-
type: 'text',
|
|
1669
|
-
text: `Error analyzing changelog: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1670
|
-
},
|
|
1671
|
-
],
|
|
2887
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1672
2888
|
isError: true,
|
|
1673
2889
|
};
|
|
1674
2890
|
}
|
|
1675
2891
|
}
|
|
1676
|
-
async function handleNpmAlternatives(args) {
|
|
2892
|
+
export async function handleNpmAlternatives(args) {
|
|
1677
2893
|
try {
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
+
}
|
|
1682
2910
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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;
|
|
1686
2959
|
try {
|
|
1687
|
-
const
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
+
}
|
|
1692
2965
|
}
|
|
1693
|
-
catch (
|
|
1694
|
-
console.
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
+
}
|
|
1724
3039
|
}));
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
}
|
|
1729
|
-
return {
|
|
1730
|
-
content: [{ type: 'text', text }],
|
|
1731
|
-
isError: false,
|
|
3040
|
+
const finalResponse = {
|
|
3041
|
+
queryPackages: args.packages,
|
|
3042
|
+
results: processedResults,
|
|
1732
3043
|
};
|
|
3044
|
+
const responseJson = JSON.stringify(finalResponse, null, 2);
|
|
3045
|
+
return { content: [{ type: 'text', text: responseJson }], isError: false };
|
|
1733
3046
|
}
|
|
1734
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);
|
|
1735
3053
|
return {
|
|
1736
|
-
content: [
|
|
1737
|
-
{
|
|
1738
|
-
type: 'text',
|
|
1739
|
-
text: `Error finding alternatives: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1740
|
-
},
|
|
1741
|
-
],
|
|
3054
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
1742
3055
|
isError: true,
|
|
1743
3056
|
};
|
|
1744
3057
|
}
|
|
1745
3058
|
}
|
|
1746
3059
|
// Create server instance
|
|
1747
3060
|
const server = new McpServer({
|
|
1748
|
-
name: '
|
|
1749
|
-
version: '1.
|
|
3061
|
+
name: 'npm-sentinel-mcp',
|
|
3062
|
+
version: '1.5.7',
|
|
1750
3063
|
});
|
|
1751
3064
|
// Add NPM tools
|
|
1752
3065
|
server.tool('npmVersions', 'Get all available versions of an NPM package', {
|
|
@@ -1875,4 +3188,16 @@ process.on('unhandledRejection', (error) => {
|
|
|
1875
3188
|
server.close();
|
|
1876
3189
|
process.exit(1);
|
|
1877
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
|
+
}
|
|
1878
3203
|
//# sourceMappingURL=index.js.map
|