@nekzus/mcp-server 1.5.6 → 1.6.1

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