@nekzus/mcp-server 1.18.3 → 1.19.0-alpha.10

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.
Files changed (49) hide show
  1. package/README.md +162 -283
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/index.d.ts +60 -40
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +488 -785
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/cache.d.ts +22 -0
  8. package/dist/src/cache.d.ts.map +1 -0
  9. package/dist/src/cache.js +68 -0
  10. package/dist/src/cache.js.map +1 -0
  11. package/dist/src/config.d.ts +11 -0
  12. package/dist/src/config.d.ts.map +1 -0
  13. package/dist/src/config.js +45 -0
  14. package/dist/src/config.js.map +1 -0
  15. package/dist/src/handlers/index.d.ts +2 -0
  16. package/dist/src/handlers/index.d.ts.map +1 -0
  17. package/dist/src/handlers/index.js +2 -0
  18. package/dist/src/handlers/index.js.map +1 -0
  19. package/dist/src/icons.d.ts +21 -0
  20. package/dist/src/icons.d.ts.map +1 -0
  21. package/dist/src/icons.js +29 -0
  22. package/dist/src/icons.js.map +1 -0
  23. package/dist/src/prompts/index.d.ts +3 -0
  24. package/dist/src/prompts/index.d.ts.map +1 -0
  25. package/dist/src/prompts/index.js +22 -0
  26. package/dist/src/prompts/index.js.map +1 -0
  27. package/dist/src/resources/index.d.ts +3 -0
  28. package/dist/src/resources/index.d.ts.map +1 -0
  29. package/dist/src/resources/index.js +62 -0
  30. package/dist/src/resources/index.js.map +1 -0
  31. package/dist/src/schemas.d.ts +143 -0
  32. package/dist/src/schemas.d.ts.map +1 -0
  33. package/dist/src/schemas.js +189 -0
  34. package/dist/src/schemas.js.map +1 -0
  35. package/dist/src/server.d.ts +7 -0
  36. package/dist/src/server.d.ts.map +1 -0
  37. package/dist/src/server.js +21 -0
  38. package/dist/src/server.js.map +1 -0
  39. package/dist/src/tools/index.d.ts +3 -0
  40. package/dist/src/tools/index.d.ts.map +1 -0
  41. package/dist/src/tools/index.js +411 -0
  42. package/dist/src/tools/index.js.map +1 -0
  43. package/dist/src/utils/fetch-retry.d.ts +5 -0
  44. package/dist/src/utils/fetch-retry.d.ts.map +1 -0
  45. package/dist/src/utils/fetch-retry.js +69 -0
  46. package/dist/src/utils/fetch-retry.js.map +1 -0
  47. package/llms-full.txt +312 -422
  48. package/package.json +16 -17
  49. package/smithery.yaml +10 -2
package/dist/index.js CHANGED
@@ -2,15 +2,16 @@
2
2
  import * as crypto from 'node:crypto';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
8
6
  import fetch from 'node-fetch';
9
7
  import { z } from 'zod';
8
+ import createServer from './src/server.js';
10
9
  // Cache configuration
11
- let NPM_REGISTRY_URL = (process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org').replace(/\/$/, '');
10
+ export let NPM_REGISTRY_URL = (process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org').replace(/\/$/, '');
11
+ export function setNpmRegistryUrl(url) {
12
+ NPM_REGISTRY_URL = url.replace(/\/$/, '');
13
+ }
12
14
  // Cache configuration
13
- const CACHE_TTL_SHORT = 15 * 60 * 1000; // 15 minutes
14
15
  const CACHE_TTL_MEDIUM = 60 * 60 * 1000; // 1 hour
15
16
  const CACHE_TTL_LONG = 6 * 60 * 60 * 1000; // 6 hours
16
17
  const CACHE_TTL_VERY_LONG = 24 * 60 * 60 * 1000; // 24 hours
@@ -73,6 +74,76 @@ function cacheSet(key, value, ttlMilliseconds) {
73
74
  }
74
75
  }
75
76
  }
77
+ // HTTP wrapper configuration
78
+ const HTTP_MAX_RETRIES = 3;
79
+ const HTTP_INITIAL_BACKOFF_MS = 1000;
80
+ const HTTP_MAX_CONCURRENT_REQUESTS = 5;
81
+ const DEFAULT_HEADERS = {
82
+ Accept: 'application/json',
83
+ 'User-Agent': 'NPM-Sentinel-MCP/1.x',
84
+ };
85
+ // Semaphore for concurrency control
86
+ let activeRequests = 0;
87
+ const requestQueue = [];
88
+ function acquireSlot() {
89
+ if (activeRequests < HTTP_MAX_CONCURRENT_REQUESTS) {
90
+ activeRequests++;
91
+ return Promise.resolve();
92
+ }
93
+ return new Promise((resolve) => {
94
+ requestQueue.push(() => {
95
+ activeRequests++;
96
+ resolve();
97
+ });
98
+ });
99
+ }
100
+ function releaseSlot() {
101
+ activeRequests--;
102
+ const next = requestQueue.shift();
103
+ if (next)
104
+ next();
105
+ }
106
+ export async function fetchWithRetry(url, options = {}, config = {}) {
107
+ const maxRetries = config.maxRetries ?? HTTP_MAX_RETRIES;
108
+ const mergedHeaders = { ...DEFAULT_HEADERS, ...(options.headers || {}) };
109
+ const mergedOptions = { ...options, headers: mergedHeaders };
110
+ if (!config.skipThrottle) {
111
+ await acquireSlot();
112
+ }
113
+ try {
114
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
115
+ try {
116
+ const response = await fetch(url, mergedOptions);
117
+ if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
118
+ if (attempt < maxRetries) {
119
+ const retryAfter = response.headers.get('retry-after');
120
+ const waitMs = retryAfter
121
+ ? parseInt(retryAfter, 10) * 1000
122
+ : HTTP_INITIAL_BACKOFF_MS * 2 ** attempt;
123
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
124
+ continue;
125
+ }
126
+ }
127
+ return response;
128
+ }
129
+ catch (err) {
130
+ if (attempt < maxRetries) {
131
+ const waitMs = HTTP_INITIAL_BACKOFF_MS * 2 ** attempt;
132
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
133
+ continue;
134
+ }
135
+ throw err;
136
+ }
137
+ }
138
+ // Fallback
139
+ return await fetch(url, mergedOptions);
140
+ }
141
+ finally {
142
+ if (!config.skipThrottle) {
143
+ releaseSlot();
144
+ }
145
+ }
146
+ }
76
147
  // Zod schemas for npm package data
77
148
  export const NpmMaintainerSchema = z
78
149
  .object({
@@ -171,64 +242,7 @@ export const NpmDownloadsDataSchema = z.object({
171
242
  end: z.string(),
172
243
  package: z.string(),
173
244
  });
174
- function isValidNpmsResponse(data) {
175
- if (typeof data !== 'object' || data === null) {
176
- console.debug('NpmsApiResponse validation: Response is not an object or is null');
177
- return false;
178
- }
179
- const response = data;
180
- // Check score structure
181
- if (!response.score ||
182
- typeof response.score !== 'object' ||
183
- !('final' in response.score) ||
184
- typeof response.score.final !== 'number' ||
185
- !('detail' in response.score) ||
186
- typeof response.score.detail !== 'object') {
187
- console.debug('NpmsApiResponse validation: Invalid score structure');
188
- return false;
189
- }
190
- // Check score detail metrics
191
- const detail = response.score.detail;
192
- if (typeof detail.quality !== 'number' ||
193
- typeof detail.popularity !== 'number' ||
194
- typeof detail.maintenance !== 'number') {
195
- console.debug('NpmsApiResponse validation: Invalid score detail metrics');
196
- return false;
197
- }
198
- // Check collected data structure
199
- if (!response.collected ||
200
- typeof response.collected !== 'object' ||
201
- !response.collected.metadata ||
202
- typeof response.collected.metadata !== 'object' ||
203
- typeof response.collected.metadata.name !== 'string' ||
204
- typeof response.collected.metadata.version !== 'string') {
205
- console.debug('NpmsApiResponse validation: Invalid collected data structure');
206
- return false;
207
- }
208
- // Check npm data
209
- if (!response.collected.npm ||
210
- typeof response.collected.npm !== 'object' ||
211
- !Array.isArray(response.collected.npm.downloads) ||
212
- typeof response.collected.npm.starsCount !== 'number') {
213
- console.debug('NpmsApiResponse validation: Invalid npm data structure');
214
- return false;
215
- }
216
- // Optional github data check
217
- if (response.collected.github) {
218
- if (typeof response.collected.github !== 'object' ||
219
- typeof response.collected.github.starsCount !== 'number' ||
220
- typeof response.collected.github.forksCount !== 'number' ||
221
- typeof response.collected.github.subscribersCount !== 'number' ||
222
- !response.collected.github.issues ||
223
- typeof response.collected.github.issues !== 'object' ||
224
- typeof response.collected.github.issues.count !== 'number' ||
225
- typeof response.collected.github.issues.openCount !== 'number') {
226
- console.debug('NpmsApiResponse validation: Invalid github data structure');
227
- return false;
228
- }
229
- }
230
- return true;
231
- }
245
+ // Schema for search results from npm registry v1 search api
232
246
  export const NpmSearchResultSchema = z
233
247
  .object({
234
248
  objects: z.array(z.object({
@@ -266,15 +280,6 @@ export const NpmSearchResultSchema = z
266
280
  total: z.number(), // total is a sibling of objects
267
281
  })
268
282
  .loose();
269
- // Logger function that uses stderr - only for critical errors
270
- const log = (...args) => {
271
- // Filter out server status messages
272
- const message = args[0];
273
- if (typeof message === 'string' &&
274
- (!message.startsWith('[Server]') || message.includes('error') || message.includes('Error'))) {
275
- console.error(...args);
276
- }
277
- };
278
283
  // Type guards for API responses
279
284
  function isNpmPackageInfo(data) {
280
285
  return (typeof data === 'object' &&
@@ -331,11 +336,24 @@ function isValidNpmPackageName(name) {
331
336
  !name.startsWith('_') &&
332
337
  !name.startsWith('.'));
333
338
  }
339
+ export function createEmptyArrayErrorResponse(toolName) {
340
+ const errorResponse = JSON.stringify({
341
+ queryPackages: [],
342
+ results: [],
343
+ status: 'error',
344
+ error: 'No package names provided in request',
345
+ message: `The packages parameter for ${toolName} must contain at least one package name.`,
346
+ }, null, 2);
347
+ return {
348
+ content: [{ type: 'text', text: errorResponse }],
349
+ isError: true,
350
+ };
351
+ }
334
352
  export async function handleNpmVersions(args) {
335
353
  try {
336
354
  const packagesToProcess = args.packages || [];
337
355
  if (packagesToProcess.length === 0) {
338
- throw new Error('No package names provided');
356
+ return createEmptyArrayErrorResponse('npmVersions');
339
357
  }
340
358
  const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
341
359
  let name = '';
@@ -391,12 +409,7 @@ export async function handleNpmVersions(args) {
391
409
  };
392
410
  }
393
411
  try {
394
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}`, {
395
- headers: {
396
- Accept: 'application/json',
397
- 'User-Agent': 'NPM-Sentinel-MCP',
398
- },
399
- });
412
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
400
413
  if (!response.ok) {
401
414
  return {
402
415
  packageInput: pkgInput,
@@ -465,7 +478,7 @@ export async function handleNpmLatest(args) {
465
478
  try {
466
479
  const packagesToProcess = args.packages || [];
467
480
  if (packagesToProcess.length === 0) {
468
- throw new Error('No package names provided');
481
+ return createEmptyArrayErrorResponse('npmLatest');
469
482
  }
470
483
  const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
471
484
  let name = '';
@@ -527,12 +540,7 @@ export async function handleNpmLatest(args) {
527
540
  };
528
541
  }
529
542
  try {
530
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}/${versionTag}`, {
531
- headers: {
532
- Accept: 'application/json',
533
- 'User-Agent': 'NPM-Sentinel-MCP',
534
- },
535
- });
543
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
536
544
  if (!response.ok) {
537
545
  let errorMsg = `Failed to fetch package version: ${response.status} ${response.statusText}`;
538
546
  if (response.status === 404) {
@@ -666,12 +674,7 @@ export async function handleNpmDeps(args) {
666
674
  };
667
675
  }
668
676
  try {
669
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}/${version}`, {
670
- headers: {
671
- Accept: 'application/json',
672
- 'User-Agent': 'NPM-Sentinel-MCP',
673
- },
674
- });
677
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
675
678
  if (!response.ok) {
676
679
  return {
677
680
  package: packageNameForOutput,
@@ -797,12 +800,7 @@ export async function handleNpmTypes(args) {
797
800
  };
798
801
  }
799
802
  try {
800
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}/${version}`, {
801
- headers: {
802
- Accept: 'application/json',
803
- 'User-Agent': 'NPM-Sentinel-MCP',
804
- },
805
- });
803
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
806
804
  if (!response.ok) {
807
805
  return {
808
806
  package: packageNameForOutput,
@@ -824,12 +822,7 @@ export async function handleNpmTypes(args) {
824
822
  isAvailable: false,
825
823
  };
826
824
  try {
827
- const typesResponse = await fetch(`${NPM_REGISTRY_URL}/${typesPackageName}/latest`, {
828
- headers: {
829
- Accept: 'application/json',
830
- 'User-Agent': 'NPM-Sentinel-MCP',
831
- },
832
- });
825
+ const typesResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${typesPackageName}/latest`);
833
826
  if (typesResponse.ok) {
834
827
  const typesData = (await typesResponse.json());
835
828
  typesPackageInfo = {
@@ -936,12 +929,7 @@ export async function handleNpmSize(args) {
936
929
  };
937
930
  }
938
931
  try {
939
- const response = await fetch(`https://bundlephobia.com/api/size?package=${bundlephobiaQuery}`, {
940
- headers: {
941
- Accept: 'application/json',
942
- 'User-Agent': 'NPM-Sentinel-MCP',
943
- },
944
- });
932
+ const response = await fetchWithRetry(`https://bundlephobia.com/api/size?package=${bundlephobiaQuery}`);
945
933
  if (!response.ok) {
946
934
  let errorMsg = `Failed to fetch package size: ${response.status} ${response.statusText}`;
947
935
  if (response.status === 404) {
@@ -1021,9 +1009,7 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
1021
1009
  const encodedName = encodeURIComponent(pkgName);
1022
1010
  const encodedVersion = encodeURIComponent(version);
1023
1011
  const url = `https://api.deps.dev/v3/systems/npm/packages/${encodedName}/versions/${encodedVersion}:dependencies`;
1024
- const response = await fetch(url, {
1025
- headers: { Accept: 'application/json', 'User-Agent': 'NPM-Sentinel-MCP' },
1026
- });
1012
+ const response = await fetchWithRetry(url);
1027
1013
  if (!response.ok) {
1028
1014
  console.warn(`deps.dev API returned ${response.status} for ${pkgName}@${version}`);
1029
1015
  return [];
@@ -1043,12 +1029,46 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
1043
1029
  return [];
1044
1030
  }
1045
1031
  }
1032
+ export async function fetchRepoStatsFromDepsDev(owner, repo, ignoreCache = false) {
1033
+ const projectKey = encodeURIComponent(`github.com/${owner}/${repo}`);
1034
+ const cacheKey = generateCacheKey('depsDevProject', owner, repo);
1035
+ const cached = ignoreCache ? undefined : cacheGet(cacheKey);
1036
+ if (cached)
1037
+ return cached;
1038
+ try {
1039
+ const response = await fetchWithRetry(`https://api.deps.dev/v3alpha/projects/${projectKey}`, {}, { maxRetries: 1 });
1040
+ if (!response.ok)
1041
+ return null;
1042
+ const data = (await response.json());
1043
+ const result = {
1044
+ starsCount: data.starsCount ?? 0,
1045
+ forksCount: data.forksCount ?? 0,
1046
+ openIssuesCount: data.openIssuesCount ?? 0,
1047
+ license: data.license ?? 'unknown',
1048
+ description: data.description ?? '',
1049
+ homepage: data.homepage ?? '',
1050
+ scorecard: data.scorecard
1051
+ ? {
1052
+ overallScore: data.scorecard.overallScore,
1053
+ checks: (data.scorecard.checks || []).map((c) => ({
1054
+ name: c.name,
1055
+ score: c.score,
1056
+ reason: c.reason,
1057
+ })),
1058
+ }
1059
+ : undefined,
1060
+ };
1061
+ cacheSet(cacheKey, result, CACHE_TTL_VERY_LONG);
1062
+ return result;
1063
+ }
1064
+ catch {
1065
+ return null;
1066
+ }
1067
+ }
1046
1068
  // Helper to resolve 'latest' tag to actual version number
1047
1069
  async function resolveLatestVersion(packageName) {
1048
1070
  try {
1049
- const response = await fetch(`${NPM_REGISTRY_URL}/${packageName}/latest`, {
1050
- headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
1051
- });
1071
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${packageName}/latest`);
1052
1072
  if (!response.ok)
1053
1073
  return null;
1054
1074
  const data = (await response.json());
@@ -1069,9 +1089,7 @@ async function enrichVulnerabilityData(vulnId, ignoreCache = false) {
1069
1089
  if (cached)
1070
1090
  return cached;
1071
1091
  try {
1072
- const response = await fetch(`https://api.osv.dev/v1/vulns/${vulnId}`, {
1073
- headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
1074
- });
1092
+ const response = await fetchWithRetry(`https://api.osv.dev/v1/vulns/${vulnId}`);
1075
1093
  if (!response.ok)
1076
1094
  return null;
1077
1095
  const data = await response.json();
@@ -1218,7 +1236,7 @@ export async function handleNpmVulnerabilities(args) {
1218
1236
  let apiResults = [];
1219
1237
  if (finalBatchQueries.length > 0) {
1220
1238
  // Perform Batch API call to OSV for non-cached items
1221
- const response = await fetch('https://api.osv.dev/v1/querybatch', {
1239
+ const response = await fetchWithRetry('https://api.osv.dev/v1/querybatch', {
1222
1240
  method: 'POST',
1223
1241
  headers: { 'Content-Type': 'application/json' },
1224
1242
  body: JSON.stringify({ queries: finalBatchQueries }),
@@ -1370,12 +1388,7 @@ export async function handleNpmTrends(args) {
1370
1388
  };
1371
1389
  }
1372
1390
  try {
1373
- const response = await fetch(`https://api.npmjs.org/downloads/point/${period}/${name}`, {
1374
- headers: {
1375
- Accept: 'application/json',
1376
- 'User-Agent': 'NPM-Sentinel-MCP',
1377
- },
1378
- });
1391
+ const response = await fetchWithRetry(`https://api.npmjs.org/downloads/point/${period}/${name}`);
1379
1392
  if (!response.ok) {
1380
1393
  let errorMsg = `Failed to fetch download trends: ${response.status} ${response.statusText}`;
1381
1394
  if (response.status === 404) {
@@ -1532,7 +1545,7 @@ export async function handleNpmCompare(args) {
1532
1545
  }
1533
1546
  try {
1534
1547
  // Fetch package version details from registry
1535
- const pkgResponse = await fetch(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
1548
+ const pkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
1536
1549
  if (!pkgResponse.ok) {
1537
1550
  throw new Error(`Failed to fetch package info for ${name}@${versionTag}: ${pkgResponse.status} ${pkgResponse.statusText}`);
1538
1551
  }
@@ -1543,7 +1556,7 @@ export async function handleNpmCompare(args) {
1543
1556
  // Fetch monthly downloads
1544
1557
  let monthlyDownloads = null;
1545
1558
  try {
1546
- const downloadsResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${name}`);
1559
+ const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
1547
1560
  if (downloadsResponse.ok) {
1548
1561
  const downloadsData = await downloadsResponse.json();
1549
1562
  if (isNpmDownloadsData(downloadsData)) {
@@ -1558,7 +1571,7 @@ export async function handleNpmCompare(args) {
1558
1571
  // Need to fetch the full package info to get to the 'time' field for specific version
1559
1572
  let publishDate = null;
1560
1573
  try {
1561
- const fullPkgInfoResponse = await fetch(`${NPM_REGISTRY_URL}/${name}`);
1574
+ const fullPkgInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
1562
1575
  if (fullPkgInfoResponse.ok) {
1563
1576
  const fullPkgInfo = await fullPkgInfoResponse.json();
1564
1577
  if (isNpmPackageInfo(fullPkgInfo) && fullPkgInfo.time) {
@@ -1623,6 +1636,122 @@ export async function handleNpmCompare(args) {
1623
1636
  };
1624
1637
  }
1625
1638
  }
1639
+ export async function getLocalPackageMetrics(name, ignoreCache = false) {
1640
+ const cacheKey = generateCacheKey('localMetrics', name);
1641
+ const cached = ignoreCache ? undefined : cacheGet(cacheKey);
1642
+ if (cached)
1643
+ return cached;
1644
+ // 1. Fetch full packument from registry
1645
+ const registryResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
1646
+ if (!registryResponse.ok) {
1647
+ throw new Error(`Package ${name} not found on registry.`);
1648
+ }
1649
+ const packageInfo = await registryResponse.json();
1650
+ if (!isNpmPackageInfo(packageInfo)) {
1651
+ throw new Error(`Invalid registry data received for ${name}.`);
1652
+ }
1653
+ const latestVersion = packageInfo['dist-tags']?.latest;
1654
+ if (!latestVersion || !packageInfo.versions?.[latestVersion]) {
1655
+ throw new Error(`No latest version found for ${name}.`);
1656
+ }
1657
+ const versionData = packageInfo.versions[latestVersion];
1658
+ const hasTypes = Boolean(versionData.types || versionData.typings || versionData.dependencies?.[`@types/${name}`]);
1659
+ const hasReadme = Boolean(versionData.readme || packageInfo.readme);
1660
+ const dependencyCount = Object.keys(versionData.dependencies || {}).length;
1661
+ // 2. Fetch downloads for last month
1662
+ let downloadsLastMonth = 0;
1663
+ try {
1664
+ const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
1665
+ if (downloadsResponse.ok) {
1666
+ const dlData = await downloadsResponse.json();
1667
+ downloadsLastMonth = dlData.downloads || 0;
1668
+ }
1669
+ }
1670
+ catch (dlError) {
1671
+ console.debug(`Could not fetch downloads for ${name}: ${dlError}`);
1672
+ }
1673
+ // 3. Fetch GitHub stats via deps.dev (optional, graceful degradation)
1674
+ let github = {
1675
+ starsCount: 0,
1676
+ forksCount: 0,
1677
+ subscribersCount: 0,
1678
+ openIssuesCount: 0,
1679
+ hasGitHubData: false,
1680
+ };
1681
+ let scorecard;
1682
+ const repoUrl = versionData.repository?.url || packageInfo.repository?.url;
1683
+ if (repoUrl?.includes('github.com')) {
1684
+ const githubMatch = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
1685
+ if (githubMatch) {
1686
+ const [, owner, repo] = githubMatch;
1687
+ const cleanRepo = repo.replace(/\.git$/, '');
1688
+ try {
1689
+ const depsDevData = await fetchRepoStatsFromDepsDev(owner, cleanRepo, ignoreCache);
1690
+ if (depsDevData) {
1691
+ github = {
1692
+ starsCount: depsDevData.starsCount,
1693
+ forksCount: depsDevData.forksCount,
1694
+ subscribersCount: 0,
1695
+ openIssuesCount: depsDevData.openIssuesCount,
1696
+ hasGitHubData: true,
1697
+ };
1698
+ scorecard = depsDevData.scorecard;
1699
+ }
1700
+ }
1701
+ catch (ghError) {
1702
+ console.debug(`Could not fetch deps.dev project data for ${owner}/${cleanRepo}: ${ghError}`);
1703
+ }
1704
+ }
1705
+ }
1706
+ // 4. Calculate publish age
1707
+ let lastPublishDaysAgo = 365;
1708
+ const timeObj = packageInfo.time;
1709
+ if (timeObj?.[latestVersion]) {
1710
+ const publishDate = new Date(timeObj[latestVersion]);
1711
+ const diffTime = Math.abs(Date.now() - publishDate.getTime());
1712
+ lastPublishDaysAgo = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
1713
+ }
1714
+ // 5. Scoring Algorithm
1715
+ const popularityNPM = Math.min(1, Math.log10(Math.max(1, downloadsLastMonth)) / 8);
1716
+ let popularity = popularityNPM;
1717
+ if (github.hasGitHubData) {
1718
+ const popularityGH = Math.min(1, github.starsCount / 50000);
1719
+ popularity = popularityNPM * 0.7 + popularityGH * 0.3;
1720
+ }
1721
+ const quality = (hasTypes ? 0.3 : 0) + (hasReadme ? 0.2 : 0) + Math.max(0, 0.5 - dependencyCount * 0.005);
1722
+ const maintenancePublish = lastPublishDaysAgo < 30
1723
+ ? 1.0
1724
+ : lastPublishDaysAgo < 90
1725
+ ? 0.8
1726
+ : lastPublishDaysAgo < 365
1727
+ ? 0.5
1728
+ : 0.2;
1729
+ let maintenance = maintenancePublish;
1730
+ if (github.hasGitHubData) {
1731
+ const issuesScore = github.openIssuesCount < 50 ? 0.3 : github.openIssuesCount < 200 ? 0.2 : 0.1;
1732
+ maintenance = maintenancePublish * 0.7 + issuesScore;
1733
+ }
1734
+ const finalScore = popularity * 0.4 + quality * 0.3 + maintenance * 0.3;
1735
+ const result = {
1736
+ packageName: name,
1737
+ version: latestVersion,
1738
+ description: versionData.description || null,
1739
+ analyzedAt: new Date().toISOString(),
1740
+ downloadsLastMonth,
1741
+ github,
1742
+ score: {
1743
+ final: finalScore,
1744
+ detail: {
1745
+ quality,
1746
+ popularity,
1747
+ maintenance,
1748
+ },
1749
+ },
1750
+ scorecard,
1751
+ };
1752
+ cacheSet(cacheKey, result, CACHE_TTL_LONG);
1753
+ return result;
1754
+ }
1626
1755
  // Function to get package quality metrics
1627
1756
  export async function handleNpmQuality(args) {
1628
1757
  try {
@@ -1634,7 +1763,7 @@ export async function handleNpmQuality(args) {
1634
1763
  let name = '';
1635
1764
  if (typeof pkgInput === 'string') {
1636
1765
  const atIdx = pkgInput.lastIndexOf('@');
1637
- name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput; // Version is ignored by npms.io API endpoint for the main query
1766
+ name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
1638
1767
  }
1639
1768
  else {
1640
1769
  return {
@@ -1679,57 +1808,20 @@ export async function handleNpmQuality(args) {
1679
1808
  };
1680
1809
  }
1681
1810
  try {
1682
- const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`, {
1683
- headers: {
1684
- Accept: 'application/json',
1685
- 'User-Agent': 'NPM-Sentinel-MCP',
1686
- },
1687
- });
1688
- if (!response.ok) {
1689
- let errorMsg = `Failed to fetch quality data: ${response.status} ${response.statusText}`;
1690
- if (response.status === 404) {
1691
- errorMsg = `Package ${name} not found on npms.io.`;
1692
- }
1693
- return {
1694
- packageInput: pkgInput,
1695
- packageName: name,
1696
- status: 'error',
1697
- error: errorMsg,
1698
- data: null,
1699
- message: `Could not retrieve quality information for ${name}.`,
1700
- };
1701
- }
1702
- const rawData = await response.json();
1703
- if (!isValidNpmsResponse(rawData)) {
1704
- return {
1705
- packageInput: pkgInput,
1706
- packageName: name,
1707
- status: 'error',
1708
- error: 'Invalid or incomplete response from npms.io API for quality data',
1709
- data: null,
1710
- message: `Received malformed quality data for ${name}.`,
1711
- };
1712
- }
1713
- const { score, collected, analyzedAt } = rawData;
1714
- const qualityScore = score.detail.quality;
1811
+ const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
1715
1812
  const qualityData = {
1716
- analyzedAt: analyzedAt,
1717
- versionInScore: collected.metadata.version,
1718
- qualityScore: qualityScore,
1719
- // Detailed sub-metrics like tests, coverage, linting, types are no longer directly provided
1720
- // by the npms.io v2 API in the same way. The overall quality score is the primary metric.
1813
+ analyzedAt: metrics.analyzedAt,
1814
+ versionInScore: metrics.version,
1815
+ qualityScore: metrics.score.detail.quality,
1721
1816
  };
1722
- const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
1723
- ? CACHE_TTL_SHORT
1724
- : CACHE_TTL_LONG;
1725
- cacheSet(cacheKey, qualityData, ttl);
1817
+ cacheSet(cacheKey, qualityData, CACHE_TTL_LONG);
1726
1818
  return {
1727
1819
  packageInput: pkgInput,
1728
1820
  packageName: name,
1729
1821
  status: 'success',
1730
1822
  error: null,
1731
1823
  data: qualityData,
1732
- message: `Successfully fetched quality score for ${name} (version analyzed: ${collected.metadata.version}).`,
1824
+ message: `Successfully fetched quality score for ${name} (version analyzed: ${metrics.version}).`,
1733
1825
  };
1734
1826
  }
1735
1827
  catch (error) {
@@ -1762,6 +1854,7 @@ export async function handleNpmQuality(args) {
1762
1854
  };
1763
1855
  }
1764
1856
  }
1857
+ // Function to get package maintenance metrics
1765
1858
  export async function handleNpmMaintenance(args) {
1766
1859
  try {
1767
1860
  const packagesToProcess = args.packages || [];
@@ -1817,55 +1910,20 @@ export async function handleNpmMaintenance(args) {
1817
1910
  };
1818
1911
  }
1819
1912
  try {
1820
- const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`, {
1821
- headers: {
1822
- Accept: 'application/json',
1823
- 'User-Agent': 'NPM-Sentinel-MCP',
1824
- },
1825
- });
1826
- if (!response.ok) {
1827
- let errorMsg = `Failed to fetch maintenance data: ${response.status} ${response.statusText}`;
1828
- if (response.status === 404) {
1829
- errorMsg = `Package ${name} not found on npms.io.`;
1830
- }
1831
- return {
1832
- packageInput: pkgInput,
1833
- packageName: name,
1834
- status: 'error',
1835
- error: errorMsg,
1836
- data: null,
1837
- message: `Could not retrieve maintenance information for ${name}.`,
1838
- };
1839
- }
1840
- const rawData = await response.json();
1841
- if (!isValidNpmsResponse(rawData)) {
1842
- return {
1843
- packageInput: pkgInput,
1844
- packageName: name,
1845
- status: 'error',
1846
- error: 'Invalid or incomplete response from npms.io API for maintenance data',
1847
- data: null,
1848
- message: `Received malformed maintenance data for ${name}.`,
1849
- };
1850
- }
1851
- const { score, collected, analyzedAt } = rawData;
1852
- const maintenanceScoreValue = score.detail.maintenance;
1913
+ const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
1853
1914
  const maintenanceData = {
1854
- analyzedAt: analyzedAt,
1855
- versionInScore: collected.metadata.version,
1856
- maintenanceScore: maintenanceScoreValue,
1915
+ analyzedAt: metrics.analyzedAt,
1916
+ versionInScore: metrics.version,
1917
+ maintenanceScore: metrics.score.detail.maintenance,
1857
1918
  };
1858
- const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
1859
- ? CACHE_TTL_SHORT
1860
- : CACHE_TTL_LONG;
1861
- cacheSet(cacheKey, maintenanceData, ttl);
1919
+ cacheSet(cacheKey, maintenanceData, CACHE_TTL_LONG);
1862
1920
  return {
1863
1921
  packageInput: pkgInput,
1864
1922
  packageName: name,
1865
1923
  status: 'success',
1866
1924
  error: null,
1867
1925
  data: maintenanceData,
1868
- message: `Successfully fetched maintenance score for ${name} (version analyzed: ${collected.metadata.version}).`,
1926
+ message: `Successfully fetched maintenance score for ${name} (version analyzed: ${metrics.version}).`,
1869
1927
  };
1870
1928
  }
1871
1929
  catch (error) {
@@ -2025,6 +2083,7 @@ export async function handleNpmMaintainers(args) {
2025
2083
  };
2026
2084
  }
2027
2085
  }
2086
+ // Function to get package score
2028
2087
  export async function handleNpmScore(args) {
2029
2088
  try {
2030
2089
  const packagesToProcess = args.packages || [];
@@ -2078,86 +2137,41 @@ export async function handleNpmScore(args) {
2078
2137
  };
2079
2138
  }
2080
2139
  try {
2081
- const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(name)}`);
2082
- if (!response.ok) {
2083
- let errorMsg = `Failed to fetch package score: ${response.status} ${response.statusText}`;
2084
- if (response.status === 404) {
2085
- errorMsg = `Package ${name} not found on npms.io.`;
2086
- }
2087
- return {
2088
- packageInput: pkgInput,
2089
- packageName: name,
2090
- status: 'error',
2091
- error: errorMsg,
2092
- data: null,
2093
- };
2094
- }
2095
- const rawData = await response.json();
2096
- if (!isValidNpmsResponse(rawData)) {
2097
- return {
2098
- packageInput: pkgInput,
2099
- packageName: name,
2100
- status: 'error',
2101
- error: 'Invalid or incomplete response from npms.io API',
2102
- data: null,
2103
- };
2104
- }
2105
- const { score, collected, analyzedAt } = rawData;
2106
- const { detail } = score;
2107
- // Calculate total downloads for the last month from the typically first entry in downloads array
2108
- const lastMonthDownloads = collected.npm?.downloads?.find((d) => {
2109
- // Heuristic: find a download period that is roughly 30 days
2110
- const from = new Date(d.from);
2111
- const to = new Date(d.to);
2112
- const diffTime = Math.abs(to.getTime() - from.getTime());
2113
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
2114
- return diffDays >= 28 && diffDays <= 31; // Common range for monthly data
2115
- })?.count ||
2116
- collected.npm?.downloads?.[0]?.count ||
2117
- 0;
2140
+ const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
2118
2141
  const scoreData = {
2119
- analyzedAt: analyzedAt,
2120
- versionInScore: collected.metadata.version,
2121
- score: {
2122
- final: score.final,
2123
- detail: {
2124
- quality: detail.quality,
2125
- popularity: detail.popularity,
2126
- maintenance: detail.maintenance,
2127
- },
2128
- },
2142
+ analyzedAt: metrics.analyzedAt,
2143
+ versionInScore: metrics.version,
2144
+ score: metrics.score,
2129
2145
  packageInfoFromScore: {
2130
- name: collected.metadata.name,
2131
- version: collected.metadata.version,
2132
- description: collected.metadata.description || null,
2146
+ name: metrics.packageName,
2147
+ version: metrics.version,
2148
+ description: metrics.description,
2133
2149
  },
2134
2150
  npmStats: {
2135
- downloadsLastMonth: lastMonthDownloads,
2136
- starsCount: collected.npm.starsCount,
2151
+ downloadsLastMonth: metrics.downloadsLastMonth,
2152
+ starsCount: metrics.github.hasGitHubData ? metrics.github.starsCount : 0, // Fallback best effort
2137
2153
  },
2138
- githubStats: collected.github
2154
+ githubStats: metrics.github.hasGitHubData
2139
2155
  ? {
2140
- starsCount: collected.github.starsCount,
2141
- forksCount: collected.github.forksCount,
2142
- subscribersCount: collected.github.subscribersCount,
2156
+ starsCount: metrics.github.starsCount,
2157
+ forksCount: metrics.github.forksCount,
2158
+ subscribersCount: metrics.github.subscribersCount,
2143
2159
  issues: {
2144
- count: collected.github.issues.count,
2145
- openCount: collected.github.issues.openCount,
2160
+ count: metrics.github.openIssuesCount, // Heuristic default count = open issues
2161
+ openCount: metrics.github.openIssuesCount,
2146
2162
  },
2147
2163
  }
2148
2164
  : null,
2165
+ scorecard: metrics.scorecard,
2149
2166
  };
2150
- const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
2151
- ? CACHE_TTL_SHORT
2152
- : CACHE_TTL_LONG;
2153
- cacheSet(cacheKey, scoreData, ttl);
2167
+ cacheSet(cacheKey, scoreData, CACHE_TTL_LONG);
2154
2168
  return {
2155
2169
  packageInput: pkgInput,
2156
2170
  packageName: name,
2157
2171
  status: 'success',
2158
2172
  error: null,
2159
2173
  data: scoreData,
2160
- message: `Successfully fetched score data for ${name} (version analyzed: ${collected.metadata.version}).`,
2174
+ message: `Successfully fetched score data for ${name} (version analyzed: ${metrics.version}).`,
2161
2175
  };
2162
2176
  }
2163
2177
  catch (error) {
@@ -2190,6 +2204,27 @@ export async function handleNpmScore(args) {
2190
2204
  };
2191
2205
  }
2192
2206
  }
2207
+ export async function fetchReadmeFromCDN(packageName, version) {
2208
+ const cdnUrls = [
2209
+ `https://cdn.jsdelivr.net/npm/${packageName}@${version}/README.md`,
2210
+ `https://unpkg.com/${packageName}@${version}/README.md`,
2211
+ ];
2212
+ for (const cdnUrl of cdnUrls) {
2213
+ try {
2214
+ const response = await fetchWithRetry(cdnUrl, { headers: { Accept: 'text/plain' } }, { maxRetries: 1, skipThrottle: true });
2215
+ if (response.ok) {
2216
+ const text = await response.text();
2217
+ if (text && text.length > 10) {
2218
+ return text;
2219
+ }
2220
+ }
2221
+ }
2222
+ catch {
2223
+ // Sigue intentando con la próxima URL
2224
+ }
2225
+ }
2226
+ return null;
2227
+ }
2193
2228
  export async function handleNpmPackageReadme(args) {
2194
2229
  try {
2195
2230
  const packagesToProcess = args.packages || [];
@@ -2259,7 +2294,7 @@ export async function handleNpmPackageReadme(args) {
2259
2294
  };
2260
2295
  }
2261
2296
  try {
2262
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}`);
2297
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
2263
2298
  if (!response.ok) {
2264
2299
  let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
2265
2300
  if (response.status === 404) {
@@ -2288,7 +2323,7 @@ export async function handleNpmPackageReadme(args) {
2288
2323
  };
2289
2324
  }
2290
2325
  const versionToUse = versionTag === 'latest' ? packageInfo['dist-tags']?.latest : versionTag;
2291
- if (!versionToUse || !packageInfo.versions || !packageInfo.versions[versionToUse]) {
2326
+ if (!versionToUse || !packageInfo.versions?.[versionToUse]) {
2292
2327
  return {
2293
2328
  packageInput: pkgInput,
2294
2329
  packageName: name,
@@ -2301,11 +2336,19 @@ export async function handleNpmPackageReadme(args) {
2301
2336
  }
2302
2337
  const versionData = packageInfo.versions[versionToUse];
2303
2338
  // README can be in version-specific data or at the root of packageInfo
2304
- const readmeContent = versionData.readme || packageInfo.readme || null;
2339
+ let readmeContent = versionData.readme || packageInfo.readme || null;
2340
+ let readmeSource = readmeContent ? 'registry' : null;
2341
+ if (!readmeContent && versionToUse) {
2342
+ readmeContent = await fetchReadmeFromCDN(name, versionToUse);
2343
+ if (readmeContent) {
2344
+ readmeSource = 'cdn';
2345
+ }
2346
+ }
2305
2347
  const hasReadme = !!readmeContent;
2306
2348
  const readmeResultData = {
2307
2349
  readme: readmeContent,
2308
2350
  hasReadme: hasReadme,
2351
+ readmeSource: readmeSource,
2309
2352
  versionFetched: versionToUse, // Store the actually fetched version
2310
2353
  };
2311
2354
  cacheSet(cacheKey, readmeResultData, CACHE_TTL_LONG);
@@ -2316,7 +2359,7 @@ export async function handleNpmPackageReadme(args) {
2316
2359
  versionFetched: versionToUse,
2317
2360
  status: 'success',
2318
2361
  error: null,
2319
- data: { readme: readmeContent, hasReadme: hasReadme }, // Return only readme and hasReadme in data field for consistency
2362
+ data: { readme: readmeContent, hasReadme: hasReadme, readmeSource: readmeSource }, // Return only readme and hasReadme in data field for consistency
2320
2363
  message: `Successfully fetched README for ${name}@${versionToUse}.`,
2321
2364
  };
2322
2365
  }
@@ -2370,7 +2413,7 @@ export async function handleNpmSearch(args) {
2370
2413
  message: `Search results for query '${query}' with limit ${limit} from cache.`,
2371
2414
  };
2372
2415
  }
2373
- const response = await fetch(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
2416
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
2374
2417
  if (!response.ok) {
2375
2418
  throw new Error(`Failed to search packages: ${response.status} ${response.statusText}`);
2376
2419
  }
@@ -2508,7 +2551,7 @@ export async function handleNpmLicenseCompatibility(args) {
2508
2551
  };
2509
2552
  }
2510
2553
  try {
2511
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
2554
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
2512
2555
  if (!response.ok) {
2513
2556
  let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
2514
2557
  if (response.status === 404) {
@@ -2683,7 +2726,7 @@ export async function handleNpmRepoStats(args) {
2683
2726
  };
2684
2727
  }
2685
2728
  try {
2686
- const npmResponse = await fetch(`${NPM_REGISTRY_URL}/${name}/latest`);
2729
+ const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/latest`);
2687
2730
  if (!npmResponse.ok) {
2688
2731
  const errorData = {
2689
2732
  packageInput: pkgInput,
@@ -2735,44 +2778,38 @@ export async function handleNpmRepoStats(args) {
2735
2778
  return resultNotGitHub;
2736
2779
  }
2737
2780
  const [, owner, repo] = githubMatch;
2738
- const githubRepoApiUrl = `https://api.github.com/repos/${owner}/${repo.replace(/\.git$/, '')}`;
2739
- const githubResponse = await fetch(githubRepoApiUrl, {
2740
- headers: {
2741
- Accept: 'application/vnd.github.v3+json',
2742
- 'User-Agent': 'NPM-Sentinel-MCP',
2743
- },
2744
- });
2745
- if (!githubResponse.ok) {
2781
+ const cleanRepo = repo.replace(/\.git$/, '');
2782
+ const depsDevData = await fetchRepoStatsFromDepsDev(owner, cleanRepo, args.ignoreCache);
2783
+ if (!depsDevData) {
2746
2784
  const errorData = {
2747
2785
  packageInput: pkgInput,
2748
2786
  packageName: name,
2749
2787
  status: 'error',
2750
- error: `Failed to fetch GitHub repo stats for ${owner}/${repo}: ${githubResponse.status} ${githubResponse.statusText}`,
2751
- data: { githubRepoUrl: githubRepoApiUrl },
2752
- message: `Could not retrieve GitHub repository statistics from ${githubRepoApiUrl}.`,
2788
+ error: `Failed to fetch project stats for github.com/${owner}/${cleanRepo} from deps.dev`,
2789
+ data: { repositoryUrl: repoUrl },
2790
+ message: `Could not retrieve repository statistics from deps.dev.`,
2753
2791
  };
2754
- // Do not cache GitHub API call failures for now
2755
2792
  return errorData;
2756
2793
  }
2757
- const githubData = (await githubResponse.json());
2758
2794
  const successResult = {
2759
2795
  packageInput: pkgInput,
2760
2796
  packageName: name,
2761
2797
  status: 'success',
2762
2798
  error: null,
2763
2799
  data: {
2764
- githubRepoUrl: `https://github.com/${owner}/${repo.replace(/\.git$/, '')}`,
2765
- stars: githubData.stargazers_count,
2766
- forks: githubData.forks_count,
2767
- openIssues: githubData.open_issues_count,
2768
- watchers: githubData.watchers_count,
2769
- createdAt: githubData.created_at,
2770
- updatedAt: githubData.updated_at,
2771
- defaultBranch: githubData.default_branch,
2772
- hasWiki: githubData.has_wiki,
2773
- topics: githubData.topics || [],
2800
+ githubRepoUrl: `https://github.com/${owner}/${cleanRepo}`,
2801
+ stars: depsDevData.starsCount,
2802
+ forks: depsDevData.forksCount,
2803
+ openIssues: depsDevData.openIssuesCount,
2804
+ watchers: 0,
2805
+ createdAt: null,
2806
+ updatedAt: null,
2807
+ defaultBranch: null,
2808
+ hasWiki: null,
2809
+ topics: [],
2810
+ scorecard: depsDevData.scorecard,
2774
2811
  },
2775
- message: 'GitHub repository statistics fetched successfully.',
2812
+ message: 'Repository statistics fetched successfully from deps.dev.',
2776
2813
  };
2777
2814
  cacheSet(cacheKey, successResult, CACHE_TTL_LONG);
2778
2815
  return successResult;
@@ -2860,7 +2897,7 @@ export async function handleNpmDeprecated(args) {
2860
2897
  }
2861
2898
  // console.debug(`[handleNpmDeprecated] Cache miss for ${cacheKey}`);
2862
2899
  try {
2863
- const mainPkgResponse = await fetch(`${NPM_REGISTRY_URL}/${name}`);
2900
+ const mainPkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
2864
2901
  if (!mainPkgResponse.ok) {
2865
2902
  return {
2866
2903
  package: initialPackageNameForOutput,
@@ -2902,7 +2939,7 @@ export async function handleNpmDeprecated(args) {
2902
2939
  let statusMessage = '';
2903
2940
  try {
2904
2941
  // console.debug(`[handleNpmDeprecated] Checking dependency: ${depName}@${depSemVer}`);
2905
- const depInfoResponse = await fetch(`${NPM_REGISTRY_URL}/${encodeURIComponent(depName)}`);
2942
+ const depInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${encodeURIComponent(depName)}`);
2906
2943
  if (!depInfoResponse.ok) {
2907
2944
  statusMessage = `Could not fetch dependency info for '${depName}' (status: ${depInfoResponse.status}). Deprecation status unknown.`;
2908
2945
  // console.warn(`[handleNpmDeprecated] ${statusMessage}`);
@@ -3079,7 +3116,7 @@ export async function handleNpmChangelogAnalysis(args) {
3079
3116
  };
3080
3117
  }
3081
3118
  try {
3082
- const npmResponse = await fetch(`${NPM_REGISTRY_URL}/${name}`);
3119
+ const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
3083
3120
  if (!npmResponse.ok) {
3084
3121
  const errorResult = {
3085
3122
  packageInput: pkgInput,
@@ -3150,41 +3187,59 @@ export async function handleNpmChangelogAnalysis(args) {
3150
3187
  let changelogContent = null;
3151
3188
  let changelogSourceUrl = null;
3152
3189
  let hasChangelogFile = false;
3153
- for (const file of changelogFiles) {
3154
- try {
3155
- const rawChangelogUrl = `https://raw.githubusercontent.com/${owner}/${repoNameForUrl}/master/${file}`;
3156
- const response = await fetch(rawChangelogUrl);
3157
- if (response.ok) {
3158
- changelogContent = await response.text();
3159
- changelogSourceUrl = rawChangelogUrl;
3160
- hasChangelogFile = true;
3161
- break;
3190
+ for (const branch of ['main', 'master']) {
3191
+ for (const file of changelogFiles) {
3192
+ try {
3193
+ const rawChangelogUrl = `https://raw.githubusercontent.com/${owner}/${repoNameForUrl}/${branch}/${file}`;
3194
+ const response = await fetchWithRetry(rawChangelogUrl, {}, { maxRetries: 0 });
3195
+ if (response.ok) {
3196
+ changelogContent = await response.text();
3197
+ changelogSourceUrl = rawChangelogUrl;
3198
+ hasChangelogFile = true;
3199
+ break;
3200
+ }
3201
+ }
3202
+ catch {
3203
+ // Sigue intentando
3162
3204
  }
3163
3205
  }
3164
- catch (error) {
3165
- console.debug(`Error fetching changelog file ${file} for ${name}: ${error}`);
3166
- }
3206
+ if (hasChangelogFile)
3207
+ break;
3167
3208
  }
3168
3209
  let githubReleases = [];
3169
3210
  try {
3170
- const githubApiResponse = await fetch(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
3211
+ const githubApiResponse = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
3171
3212
  headers: {
3172
3213
  Accept: 'application/vnd.github.v3+json',
3173
- 'User-Agent': 'NPM-Sentinel-MCP',
3174
3214
  },
3175
- });
3215
+ }, { maxRetries: 0 });
3176
3216
  if (githubApiResponse.ok) {
3177
3217
  const releasesData = (await githubApiResponse.json());
3178
3218
  githubReleases = releasesData.map((r) => ({
3179
3219
  tag_name: r.tag_name || null,
3180
3220
  name: r.name || null,
3181
3221
  published_at: r.published_at || null,
3222
+ source: 'github-api',
3182
3223
  }));
3183
3224
  }
3184
3225
  }
3185
3226
  catch (error) {
3186
3227
  console.debug(`Error fetching GitHub releases for ${name}: ${error}`);
3187
3228
  }
3229
+ // Fallback to NPM Registry time/publish history if GitHub releases couldn't be fetched
3230
+ const publishTime = npmData.time;
3231
+ if (githubReleases.length === 0 && publishTime) {
3232
+ const timeKeys = Object.keys(publishTime).filter((k) => k !== 'modified' && k !== 'created');
3233
+ const sortedVersions = timeKeys.sort((a, b) => {
3234
+ return new Date(publishTime[b]).getTime() - new Date(publishTime[a]).getTime();
3235
+ });
3236
+ githubReleases = sortedVersions.slice(0, 5).map((v) => ({
3237
+ tag_name: `v${v}`,
3238
+ name: `Release ${v} (NPM Publish)`,
3239
+ published_at: publishTime[v],
3240
+ source: 'npm-registry',
3241
+ }));
3242
+ }
3188
3243
  const versions = Object.keys(npmData.versions || {});
3189
3244
  const npmVersionHistory = {
3190
3245
  totalVersions: versions.length,
@@ -3198,7 +3253,7 @@ export async function handleNpmChangelogAnalysis(args) {
3198
3253
  ? `No changelog file or GitHub releases found for ${name}.`
3199
3254
  : `Changelog analysis for ${name}.`;
3200
3255
  const resultToCache = {
3201
- packageInput: pkgInput, // This might differ on subsequent cache hits, so store the original reference for this specific cache entry
3256
+ packageInput: pkgInput,
3202
3257
  packageName: name,
3203
3258
  versionQueried: versionQueried,
3204
3259
  status: status,
@@ -3250,20 +3305,45 @@ export async function handleNpmChangelogAnalysis(args) {
3250
3305
  };
3251
3306
  }
3252
3307
  }
3308
+ function isAlternativeCandidate(candidateName, candidateDesc, originalName) {
3309
+ const lowerCandidate = candidateName.toLowerCase();
3310
+ const lowerOriginal = originalName.toLowerCase();
3311
+ const lowerDesc = (candidateDesc || '').toLowerCase();
3312
+ if (lowerCandidate === lowerOriginal)
3313
+ return false;
3314
+ if (lowerCandidate.startsWith('@types/'))
3315
+ return false;
3316
+ if (lowerCandidate.startsWith(`${lowerOriginal}-`) ||
3317
+ lowerCandidate.startsWith(`${lowerOriginal}_`) ||
3318
+ lowerCandidate.startsWith(`${lowerOriginal}.`) ||
3319
+ lowerCandidate.endsWith(`-${lowerOriginal}`) ||
3320
+ lowerCandidate.endsWith(`_${lowerOriginal}`) ||
3321
+ lowerCandidate.includes(lowerOriginal)) {
3322
+ return false;
3323
+ }
3324
+ if (lowerDesc.includes(`middleware for ${lowerOriginal}`) ||
3325
+ lowerDesc.includes(`plugin for ${lowerOriginal}`) ||
3326
+ lowerDesc.includes(`adapter for ${lowerOriginal}`) ||
3327
+ lowerDesc.includes(`wrapper for ${lowerOriginal}`) ||
3328
+ lowerDesc.includes(`extension for ${lowerOriginal}`) ||
3329
+ lowerDesc.includes(`${lowerOriginal} middleware`) ||
3330
+ lowerDesc.includes(`${lowerOriginal} plugin`)) {
3331
+ return false;
3332
+ }
3333
+ return true;
3334
+ }
3253
3335
  export async function handleNpmAlternatives(args) {
3254
3336
  try {
3255
3337
  const packagesToProcess = args.packages || [];
3256
3338
  if (packagesToProcess.length === 0) {
3257
- throw new Error('No package names provided to find alternatives.');
3339
+ return createEmptyArrayErrorResponse('npmAlternatives');
3258
3340
  }
3259
3341
  const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
3260
3342
  let originalPackageName = '';
3261
- let versionQueried;
3262
3343
  if (typeof pkgInput === 'string') {
3263
3344
  const atIdx = pkgInput.lastIndexOf('@');
3264
3345
  if (atIdx > 0) {
3265
3346
  originalPackageName = pkgInput.slice(0, atIdx);
3266
- versionQueried = pkgInput.slice(atIdx + 1);
3267
3347
  }
3268
3348
  else {
3269
3349
  originalPackageName = pkgInput;
@@ -3300,35 +3380,34 @@ export async function handleNpmAlternatives(args) {
3300
3380
  };
3301
3381
  }
3302
3382
  const cacheKey = generateCacheKey('handleNpmAlternatives', originalPackageName);
3303
- const cachedResult = args.ignoreCache ? undefined : cacheGet(cacheKey); // Expects the full result object
3383
+ const cachedResult = args.ignoreCache ? undefined : cacheGet(cacheKey);
3304
3384
  if (cachedResult) {
3305
3385
  return {
3306
3386
  ...cachedResult,
3307
- packageInput: pkgInput, // current input context
3308
- packageName: originalPackageName, // current name context
3309
- // versionQueried is part of cachedResult.data or similar if stored, or add if needed
3387
+ packageInput: pkgInput,
3388
+ packageName: originalPackageName,
3310
3389
  status: `${cachedResult.status}_cache`,
3311
3390
  message: `${cachedResult.message} (from cache)`,
3312
3391
  };
3313
3392
  }
3314
3393
  try {
3315
- const searchResponse = await fetch(`${NPM_REGISTRY_URL}/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=10`);
3316
- if (!searchResponse.ok) {
3317
- const errorResult = {
3318
- packageInput: pkgInput,
3319
- packageName: originalPackageName,
3320
- status: 'error',
3321
- error: `Failed to search for alternatives: ${searchResponse.status} ${searchResponse.statusText}`,
3322
- data: null,
3323
- message: 'Could not perform search for alternatives.',
3324
- };
3325
- return errorResult; // Do not cache API errors for search
3326
- }
3327
- const searchData = (await searchResponse.json());
3328
- const alternativePackagesRaw = searchData.objects || [];
3394
+ let originalPackageKeywords = [];
3329
3395
  let originalPackageDownloads = 0;
3330
3396
  try {
3331
- const dlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
3397
+ const initSearchResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=5`);
3398
+ if (initSearchResponse.ok) {
3399
+ const initSearchData = (await initSearchResponse.json());
3400
+ const match = (initSearchData.objects || []).find((p) => p.package.name === originalPackageName);
3401
+ if (match?.package?.keywords) {
3402
+ originalPackageKeywords = match.package.keywords;
3403
+ }
3404
+ }
3405
+ }
3406
+ catch {
3407
+ // Optional keyword retrieval failure
3408
+ }
3409
+ try {
3410
+ const dlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
3332
3411
  if (dlResponse.ok) {
3333
3412
  originalPackageDownloads =
3334
3413
  (await dlResponse.json()).downloads || 0;
@@ -3337,34 +3416,80 @@ export async function handleNpmAlternatives(args) {
3337
3416
  catch (e) {
3338
3417
  console.debug(`Failed to fetch downloads for original package ${originalPackageName}: ${e}`);
3339
3418
  }
3340
- const originalPackageKeywords = alternativePackagesRaw.find((p) => p.package.name === originalPackageName)?.package
3341
- .keywords || [];
3342
3419
  const originalPackageStats = {
3343
3420
  name: originalPackageName,
3344
3421
  monthlyDownloads: originalPackageDownloads,
3345
3422
  keywords: originalPackageKeywords,
3346
3423
  };
3347
- if (alternativePackagesRaw.length === 0 ||
3348
- (alternativePackagesRaw.length === 1 &&
3349
- alternativePackagesRaw[0].package.name === originalPackageName)) {
3424
+ const genericKeywords = originalPackageKeywords.filter((kw) => kw.toLowerCase() !== originalPackageName.toLowerCase() &&
3425
+ !kw.toLowerCase().includes(originalPackageName.toLowerCase()) &&
3426
+ kw.length > 2);
3427
+ const domainKeywords = genericKeywords.filter((kw) => ![
3428
+ 'mobile',
3429
+ 'ionic',
3430
+ 'component',
3431
+ 'components',
3432
+ 'ui',
3433
+ 'css',
3434
+ 'react',
3435
+ 'vue',
3436
+ 'angular',
3437
+ 'stencil',
3438
+ 'storybook',
3439
+ 'icon',
3440
+ 'icons',
3441
+ ].includes(kw.toLowerCase()));
3442
+ const selectedKeywords = domainKeywords.length > 0 ? domainKeywords : genericKeywords;
3443
+ let searchQuery = originalPackageName;
3444
+ if (selectedKeywords.length >= 2) {
3445
+ searchQuery = selectedKeywords.slice(0, 3).join(' ');
3446
+ }
3447
+ else if (selectedKeywords.length === 1) {
3448
+ searchQuery = selectedKeywords[0];
3449
+ }
3450
+ const searchResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(searchQuery)}&size=30`);
3451
+ if (!searchResponse.ok) {
3452
+ const errorResult = {
3453
+ packageInput: pkgInput,
3454
+ packageName: originalPackageName,
3455
+ status: 'error',
3456
+ error: `Failed to search for alternatives: ${searchResponse.status} ${searchResponse.statusText}`,
3457
+ data: null,
3458
+ message: 'Could not perform search for alternatives.',
3459
+ };
3460
+ return errorResult;
3461
+ }
3462
+ const searchData = (await searchResponse.json());
3463
+ const alternativePackagesRaw = searchData.objects || [];
3464
+ let validAlternativesRaw = alternativePackagesRaw.filter((alt) => isAlternativeCandidate(alt.package.name, alt.package.description || '', originalPackageName));
3465
+ if (validAlternativesRaw.length === 0 && searchQuery !== originalPackageName) {
3466
+ try {
3467
+ const fallbackResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(originalPackageName)}&size=30`);
3468
+ if (fallbackResponse.ok) {
3469
+ const fallbackData = (await fallbackResponse.json());
3470
+ validAlternativesRaw = (fallbackData.objects || []).filter((alt) => isAlternativeCandidate(alt.package.name, alt.package.description || '', originalPackageName));
3471
+ }
3472
+ }
3473
+ catch {
3474
+ // Fallback failure
3475
+ }
3476
+ }
3477
+ if (validAlternativesRaw.length === 0) {
3350
3478
  const resultNoAlternatives = {
3351
3479
  packageInput: pkgInput,
3352
3480
  packageName: originalPackageName,
3353
3481
  status: 'no_alternatives_found',
3354
3482
  error: null,
3355
3483
  data: { originalPackageStats, alternatives: [] },
3356
- message: `No significant alternatives found for ${originalPackageName} based on keyword search.`,
3484
+ message: `No significant alternatives found for ${originalPackageName} based on search.`,
3357
3485
  };
3358
3486
  cacheSet(cacheKey, resultNoAlternatives, CACHE_TTL_MEDIUM);
3359
3487
  return resultNoAlternatives;
3360
3488
  }
3361
- const alternativesData = await Promise.all(alternativePackagesRaw
3362
- .filter((alt) => alt.package.name !== originalPackageName)
3363
- .slice(0, 5)
3364
- .map(async (alt) => {
3489
+ const alternativesData = await Promise.all(validAlternativesRaw.slice(0, 5).map(async (alt) => {
3365
3490
  let altDownloads = 0;
3366
3491
  try {
3367
- const altDlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
3492
+ const altDlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
3368
3493
  if (altDlResponse.ok) {
3369
3494
  altDownloads = (await altDlResponse.json()).downloads || 0;
3370
3495
  }
@@ -3427,430 +3552,8 @@ export async function handleNpmAlternatives(args) {
3427
3552
  };
3428
3553
  }
3429
3554
  }
3430
- // Get __dirname in an ES module environment
3431
- // Define session configuration schema
3432
- export const configSchema = z.object({
3433
- NPM_REGISTRY_URL: z.string().optional().describe('URL of the NPM registry to use'),
3434
- });
3435
- // Create server function for Smithery CLI
3436
- export default function createServer({ config }) {
3437
- // Apply config overrides
3438
- if (config.NPM_REGISTRY_URL) {
3439
- NPM_REGISTRY_URL = config.NPM_REGISTRY_URL.replace(/\/$/, '');
3440
- }
3441
- // Handle both ESM and CJS environments
3442
- let __filename;
3443
- let __dirname;
3444
- try {
3445
- __filename = fileURLToPath(import.meta.url);
3446
- __dirname = path.dirname(__filename);
3447
- }
3448
- catch (error) {
3449
- // Fallback for CJS environment (Smithery HTTP build)
3450
- __filename = process.argv[1] || '.';
3451
- __dirname = path.dirname(__filename);
3452
- }
3453
- // Determine package root
3454
- let packageRoot = __dirname;
3455
- if (fs.existsSync(path.join(__dirname, 'package.json'))) {
3456
- packageRoot = __dirname;
3457
- }
3458
- else if (fs.existsSync(path.join(__dirname, '..', 'package.json'))) {
3459
- packageRoot = path.join(__dirname, '..');
3460
- }
3461
- // Read version from package.json
3462
- let serverVersion = '1.0.0';
3463
- try {
3464
- const packageJsonPath = path.join(packageRoot, 'package.json');
3465
- if (fs.existsSync(packageJsonPath)) {
3466
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
3467
- serverVersion = packageJson.version;
3468
- }
3469
- }
3470
- catch (error) {
3471
- console.error('Error reading package.json version:', error);
3472
- }
3473
- // Create server instance
3474
- const server = new McpServer({
3475
- name: 'npm-sentinel-mcp',
3476
- version: serverVersion,
3477
- });
3478
- // Update paths to be relative to the package
3479
- const README_PATH = path.join(packageRoot, 'README.md');
3480
- const LLMS_FULL_TEXT_PATH = path.join(packageRoot, 'llms-full.txt');
3481
- // Register README.md resource
3482
- server.registerResource('serverReadme', 'doc://server/readme', {
3483
- description: 'Main documentation and usage guide for this NPM Info Server.',
3484
- mimeType: 'text/markdown',
3485
- }, async (uri) => {
3486
- try {
3487
- const readmeContent = fs.readFileSync(README_PATH, 'utf-8');
3488
- return {
3489
- contents: [
3490
- {
3491
- uri: uri.href,
3492
- text: readmeContent,
3493
- mimeType: 'text/markdown',
3494
- },
3495
- ],
3496
- };
3497
- }
3498
- catch (error) {
3499
- console.error(`Error reading README.md for resource ${uri.href}:`, error.message);
3500
- throw {
3501
- code: -32002,
3502
- message: `Resource not found or unreadable: ${uri.href}`,
3503
- data: { uri: uri.href, cause: error.message },
3504
- };
3505
- }
3506
- });
3507
- // Register llms-full.txt resource (MCP Specification)
3508
- server.registerResource('mcpSpecification', 'doc://mcp/specification', {
3509
- description: 'The llms-full.txt content providing a comprehensive overview of the Model Context Protocol.',
3510
- mimeType: 'text/plain',
3511
- }, async (uri) => {
3512
- try {
3513
- const specContent = fs.readFileSync(LLMS_FULL_TEXT_PATH, 'utf-8');
3514
- return {
3515
- contents: [
3516
- {
3517
- uri: uri.href,
3518
- text: specContent,
3519
- mimeType: 'text/plain',
3520
- },
3521
- ],
3522
- };
3523
- }
3524
- catch (error) {
3525
- console.error(`Error reading llms-full.txt for resource ${uri.href}:`, error.message);
3526
- throw {
3527
- code: -32002,
3528
- message: `Resource not found or unreadable: ${uri.href}`,
3529
- data: { uri: uri.href, cause: error.message },
3530
- };
3531
- }
3532
- });
3533
- // Register prompts
3534
- server.registerPrompt('analyze-package', {
3535
- description: 'Analyze an NPM package for security and quality',
3536
- argsSchema: {
3537
- package: z.string().describe('Name of the npm package to analyze'),
3538
- },
3539
- }, ({ package: pkgName }) => ({
3540
- messages: [
3541
- {
3542
- role: 'user',
3543
- content: {
3544
- type: 'text',
3545
- text: `Please analyze the npm package "${pkgName}". Check for vulnerabilities, maintenance status, and recent issues. Use the available tools to gather information.`,
3546
- },
3547
- },
3548
- ],
3549
- }));
3550
- // Add NPM tools - Ensuring each tool registration is complete and correct
3551
- server.registerTool('npmVersions', {
3552
- description: 'Get all available versions of an NPM package',
3553
- inputSchema: {
3554
- packages: z.array(z.string()).describe('List of package names to get versions for'),
3555
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3556
- },
3557
- annotations: {
3558
- title: 'Get All Package Versions',
3559
- readOnlyHint: true,
3560
- openWorldHint: true,
3561
- idempotentHint: true,
3562
- },
3563
- }, async (args) => {
3564
- return await handleNpmVersions(args);
3565
- });
3566
- server.registerTool('npmLatest', {
3567
- description: 'Get the latest version and changelog of an NPM package',
3568
- inputSchema: {
3569
- packages: z.array(z.string()).describe('List of package names to get latest versions for'),
3570
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3571
- },
3572
- annotations: {
3573
- title: 'Get Latest Package Information',
3574
- readOnlyHint: true,
3575
- openWorldHint: true,
3576
- idempotentHint: true, // Result for 'latest' tag can change, but call itself is idempotent
3577
- },
3578
- }, async (args) => {
3579
- return await handleNpmLatest(args);
3580
- });
3581
- server.registerTool('npmDeps', {
3582
- description: 'Analyze dependencies and devDependencies of an NPM package',
3583
- inputSchema: {
3584
- packages: z.array(z.string()).describe('List of package names to analyze dependencies for'),
3585
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3586
- },
3587
- annotations: {
3588
- title: 'Get Package Dependencies',
3589
- readOnlyHint: true,
3590
- openWorldHint: true,
3591
- idempotentHint: true,
3592
- },
3593
- }, async (args) => {
3594
- return await handleNpmDeps(args);
3595
- });
3596
- server.registerTool('npmTypes', {
3597
- description: 'Check TypeScript types availability and version for a package',
3598
- inputSchema: {
3599
- packages: z.array(z.string()).describe('List of package names to check types for'),
3600
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3601
- },
3602
- annotations: {
3603
- title: 'Check TypeScript Type Availability',
3604
- readOnlyHint: true,
3605
- openWorldHint: true,
3606
- idempotentHint: true,
3607
- },
3608
- }, async (args) => {
3609
- return await handleNpmTypes(args);
3610
- });
3611
- server.registerTool('npmSize', {
3612
- description: 'Get package size information including dependencies and bundle size',
3613
- inputSchema: {
3614
- packages: z.array(z.string()).describe('List of package names to get size information for'),
3615
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3616
- },
3617
- annotations: {
3618
- title: 'Get Package Size (Bundlephobia)',
3619
- readOnlyHint: true,
3620
- openWorldHint: true,
3621
- idempotentHint: true,
3622
- },
3623
- }, async (args) => {
3624
- return await handleNpmSize(args);
3625
- });
3626
- server.registerTool('npmVulnerabilities', {
3627
- description: 'Check for known vulnerabilities in packages',
3628
- inputSchema: {
3629
- packages: z
3630
- .array(z.string())
3631
- .describe('List of package names to check for vulnerabilities'),
3632
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3633
- },
3634
- annotations: {
3635
- title: 'Check Package Vulnerabilities (OSV.dev)',
3636
- readOnlyHint: true,
3637
- openWorldHint: true,
3638
- idempotentHint: false, // Vulnerability data can change frequently
3639
- },
3640
- }, async (args) => {
3641
- return await handleNpmVulnerabilities(args);
3642
- });
3643
- server.registerTool('npmTrends', {
3644
- description: 'Get download trends and popularity metrics for packages',
3645
- inputSchema: {
3646
- packages: z.array(z.string()).describe('List of package names to get trends for'),
3647
- period: z
3648
- .enum(['last-week', 'last-month', 'last-year'])
3649
- .describe('Time period for trends. Options: "last-week", "last-month", "last-year"')
3650
- .optional()
3651
- .default('last-month'),
3652
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3653
- },
3654
- annotations: {
3655
- title: 'Get NPM Package Download Trends',
3656
- readOnlyHint: true,
3657
- openWorldHint: true,
3658
- idempotentHint: true, // Trends for a fixed past period are idempotent
3659
- },
3660
- }, async (args) => {
3661
- return await handleNpmTrends(args);
3662
- });
3663
- server.registerTool('npmCompare', {
3664
- description: 'Compare multiple NPM packages based on various metrics',
3665
- inputSchema: {
3666
- packages: z.array(z.string()).describe('List of package names to compare'),
3667
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3668
- },
3669
- annotations: {
3670
- title: 'Compare NPM Packages',
3671
- readOnlyHint: true,
3672
- openWorldHint: true,
3673
- idempotentHint: true,
3674
- },
3675
- }, async (args) => {
3676
- return await handleNpmCompare(args);
3677
- });
3678
- server.registerTool('npmMaintainers', {
3679
- description: 'Get maintainers information for NPM packages',
3680
- inputSchema: {
3681
- packages: z.array(z.string()).describe('List of package names to get maintainers for'),
3682
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3683
- },
3684
- annotations: {
3685
- title: 'Get NPM Package Maintainers',
3686
- readOnlyHint: true,
3687
- openWorldHint: true,
3688
- idempotentHint: true,
3689
- },
3690
- }, async (args) => {
3691
- return await handleNpmMaintainers(args);
3692
- });
3693
- server.registerTool('npmScore', {
3694
- description: 'Get consolidated package score based on quality, maintenance, and popularity metrics',
3695
- inputSchema: {
3696
- packages: z.array(z.string()).describe('List of package names to get scores for'),
3697
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3698
- },
3699
- annotations: {
3700
- title: 'Get NPM Package Score (NPMS.io)',
3701
- readOnlyHint: true,
3702
- openWorldHint: true,
3703
- idempotentHint: true, // Score for a version is stable, for 'latest' can change
3704
- },
3705
- }, async (args) => {
3706
- return await handleNpmScore(args);
3707
- });
3708
- server.registerTool('npmPackageReadme', {
3709
- description: 'Get the README content for NPM packages',
3710
- inputSchema: {
3711
- packages: z.array(z.string()).describe('List of package names to get READMEs for'),
3712
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3713
- },
3714
- annotations: {
3715
- title: 'Get NPM Package README',
3716
- readOnlyHint: true,
3717
- openWorldHint: true,
3718
- idempotentHint: true,
3719
- },
3720
- }, async (args) => {
3721
- return await handleNpmPackageReadme(args);
3722
- });
3723
- server.registerTool('npmSearch', {
3724
- description: 'Search for NPM packages with optional limit',
3725
- inputSchema: {
3726
- query: z.string().describe('Search query for packages'),
3727
- limit: z
3728
- .number()
3729
- .min(1)
3730
- .max(50)
3731
- .optional()
3732
- .describe('Maximum number of results to return (default: 10)'),
3733
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3734
- },
3735
- annotations: {
3736
- title: 'Search NPM Packages',
3737
- readOnlyHint: true,
3738
- openWorldHint: true,
3739
- idempotentHint: false, // Search results can change
3740
- },
3741
- }, async (args) => {
3742
- return await handleNpmSearch(args);
3743
- });
3744
- server.registerTool('npmLicenseCompatibility', {
3745
- description: 'Check license compatibility between multiple packages',
3746
- inputSchema: {
3747
- packages: z
3748
- .array(z.string())
3749
- .min(1)
3750
- .describe('List of package names to check for license compatibility'),
3751
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3752
- },
3753
- annotations: {
3754
- title: 'Check NPM License Compatibility',
3755
- readOnlyHint: true,
3756
- openWorldHint: true,
3757
- idempotentHint: true,
3758
- },
3759
- }, async (args) => {
3760
- return await handleNpmLicenseCompatibility(args);
3761
- });
3762
- server.registerTool('npmRepoStats', {
3763
- description: 'Get repository statistics for NPM packages',
3764
- inputSchema: {
3765
- packages: z.array(z.string()).describe('List of package names to get repository stats for'),
3766
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3767
- },
3768
- annotations: {
3769
- title: 'Get NPM Package Repository Stats (GitHub)',
3770
- readOnlyHint: true,
3771
- openWorldHint: true,
3772
- idempotentHint: true, // Stats for a repo at a point in time, though they change over time
3773
- },
3774
- }, async (args) => {
3775
- return await handleNpmRepoStats(args);
3776
- });
3777
- server.registerTool('npmDeprecated', {
3778
- description: 'Check if packages are deprecated',
3779
- inputSchema: {
3780
- packages: z.array(z.string()).describe('List of package names to check for deprecation'),
3781
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3782
- },
3783
- annotations: {
3784
- title: 'Check NPM Package Deprecation Status',
3785
- readOnlyHint: true,
3786
- openWorldHint: true,
3787
- idempotentHint: true, // Deprecation status is generally stable for a version
3788
- },
3789
- }, async (args) => {
3790
- return await handleNpmDeprecated(args);
3791
- });
3792
- server.registerTool('npmChangelogAnalysis', {
3793
- description: 'Analyze changelog and release history of packages',
3794
- inputSchema: {
3795
- packages: z.array(z.string()).describe('List of package names to analyze changelogs for'),
3796
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3797
- },
3798
- annotations: {
3799
- title: 'Analyze NPM Package Changelog (GitHub)',
3800
- readOnlyHint: true,
3801
- openWorldHint: true,
3802
- idempotentHint: true,
3803
- },
3804
- }, async (args) => {
3805
- return await handleNpmChangelogAnalysis(args);
3806
- });
3807
- server.registerTool('npmAlternatives', {
3808
- description: 'Find alternative packages with similar functionality',
3809
- inputSchema: {
3810
- packages: z.array(z.string()).describe('List of package names to find alternatives for'),
3811
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3812
- },
3813
- annotations: {
3814
- title: 'Find NPM Package Alternatives',
3815
- readOnlyHint: true,
3816
- openWorldHint: true,
3817
- idempotentHint: false, // Search-based, results can change
3818
- },
3819
- }, async (args) => {
3820
- return await handleNpmAlternatives(args);
3821
- });
3822
- server.registerTool('npmQuality', {
3823
- description: 'Analyze package quality metrics',
3824
- inputSchema: {
3825
- packages: z.array(z.string()).describe('List of package names to analyze'),
3826
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3827
- },
3828
- annotations: {
3829
- title: 'Analyze NPM Package Quality (NPMS.io)',
3830
- readOnlyHint: true,
3831
- openWorldHint: true,
3832
- idempotentHint: true, // Score for a version is stable, for 'latest' can change
3833
- },
3834
- }, async (args) => {
3835
- return await handleNpmQuality(args);
3836
- });
3837
- server.registerTool('npmMaintenance', {
3838
- description: 'Analyze package maintenance metrics',
3839
- inputSchema: {
3840
- packages: z.array(z.string()).describe('List of package names to analyze'),
3841
- ignoreCache: z.boolean().optional().describe('Force a fresh lookup, ignoring the cache'),
3842
- },
3843
- annotations: {
3844
- title: 'Analyze NPM Package Maintenance (NPMS.io)',
3845
- readOnlyHint: true,
3846
- openWorldHint: true,
3847
- idempotentHint: true, // Score for a version is stable, for 'latest' can change
3848
- },
3849
- }, async (args) => {
3850
- return await handleNpmMaintenance(args);
3851
- });
3852
- return server.server;
3853
- }
3555
+ export { createServer };
3556
+ export default createServer;
3854
3557
  // STDIO compatibility for backward compatibility
3855
3558
  async function main() {
3856
3559
  // Create server with empty configuration
@@ -3884,7 +3587,7 @@ function isNpmPackageVersionData(data) {
3884
3587
  }
3885
3588
  return result.success;
3886
3589
  }
3887
- catch (e) {
3590
+ catch {
3888
3591
  // This catch block might not be strictly necessary with safeParse but kept for safety
3889
3592
  // console.error("isNpmPackageVersionData validation failed unexpectedly:", e);
3890
3593
  return false;