@nekzus/mcp-server 1.5.5 → 1.6.0

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