@nekzus/mcp-server 1.18.1 → 1.19.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,7 +10,6 @@ import { z } from 'zod';
10
10
  // Cache configuration
11
11
  let NPM_REGISTRY_URL = (process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org').replace(/\/$/, '');
12
12
  // Cache configuration
13
- const CACHE_TTL_SHORT = 15 * 60 * 1000; // 15 minutes
14
13
  const CACHE_TTL_MEDIUM = 60 * 60 * 1000; // 1 hour
15
14
  const CACHE_TTL_LONG = 6 * 60 * 60 * 1000; // 6 hours
16
15
  const CACHE_TTL_VERY_LONG = 24 * 60 * 60 * 1000; // 24 hours
@@ -73,6 +72,76 @@ function cacheSet(key, value, ttlMilliseconds) {
73
72
  }
74
73
  }
75
74
  }
75
+ // HTTP wrapper configuration
76
+ const HTTP_MAX_RETRIES = 3;
77
+ const HTTP_INITIAL_BACKOFF_MS = 1000;
78
+ const HTTP_MAX_CONCURRENT_REQUESTS = 5;
79
+ const DEFAULT_HEADERS = {
80
+ Accept: 'application/json',
81
+ 'User-Agent': 'NPM-Sentinel-MCP/1.x',
82
+ };
83
+ // Semaphore for concurrency control
84
+ let activeRequests = 0;
85
+ const requestQueue = [];
86
+ function acquireSlot() {
87
+ if (activeRequests < HTTP_MAX_CONCURRENT_REQUESTS) {
88
+ activeRequests++;
89
+ return Promise.resolve();
90
+ }
91
+ return new Promise((resolve) => {
92
+ requestQueue.push(() => {
93
+ activeRequests++;
94
+ resolve();
95
+ });
96
+ });
97
+ }
98
+ function releaseSlot() {
99
+ activeRequests--;
100
+ const next = requestQueue.shift();
101
+ if (next)
102
+ next();
103
+ }
104
+ export async function fetchWithRetry(url, options = {}, config = {}) {
105
+ const maxRetries = config.maxRetries ?? HTTP_MAX_RETRIES;
106
+ const mergedHeaders = { ...DEFAULT_HEADERS, ...(options.headers || {}) };
107
+ const mergedOptions = { ...options, headers: mergedHeaders };
108
+ if (!config.skipThrottle) {
109
+ await acquireSlot();
110
+ }
111
+ try {
112
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
113
+ try {
114
+ const response = await fetch(url, mergedOptions);
115
+ if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
116
+ if (attempt < maxRetries) {
117
+ const retryAfter = response.headers.get('retry-after');
118
+ const waitMs = retryAfter
119
+ ? parseInt(retryAfter, 10) * 1000
120
+ : HTTP_INITIAL_BACKOFF_MS * (2 ** attempt);
121
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
122
+ continue;
123
+ }
124
+ }
125
+ return response;
126
+ }
127
+ catch (err) {
128
+ if (attempt < maxRetries) {
129
+ const waitMs = HTTP_INITIAL_BACKOFF_MS * (2 ** attempt);
130
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
131
+ continue;
132
+ }
133
+ throw err;
134
+ }
135
+ }
136
+ // Fallback
137
+ return await fetch(url, mergedOptions);
138
+ }
139
+ finally {
140
+ if (!config.skipThrottle) {
141
+ releaseSlot();
142
+ }
143
+ }
144
+ }
76
145
  // Zod schemas for npm package data
77
146
  export const NpmMaintainerSchema = z
78
147
  .object({
@@ -171,64 +240,7 @@ export const NpmDownloadsDataSchema = z.object({
171
240
  end: z.string(),
172
241
  package: z.string(),
173
242
  });
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
- }
243
+ // Schema for search results from npm registry v1 search api
232
244
  export const NpmSearchResultSchema = z
233
245
  .object({
234
246
  objects: z.array(z.object({
@@ -266,15 +278,6 @@ export const NpmSearchResultSchema = z
266
278
  total: z.number(), // total is a sibling of objects
267
279
  })
268
280
  .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
281
  // Type guards for API responses
279
282
  function isNpmPackageInfo(data) {
280
283
  return (typeof data === 'object' &&
@@ -391,12 +394,7 @@ export async function handleNpmVersions(args) {
391
394
  };
392
395
  }
393
396
  try {
394
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}`, {
395
- headers: {
396
- Accept: 'application/json',
397
- 'User-Agent': 'NPM-Sentinel-MCP',
398
- },
399
- });
397
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
400
398
  if (!response.ok) {
401
399
  return {
402
400
  packageInput: pkgInput,
@@ -527,12 +525,7 @@ export async function handleNpmLatest(args) {
527
525
  };
528
526
  }
529
527
  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
- });
528
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
536
529
  if (!response.ok) {
537
530
  let errorMsg = `Failed to fetch package version: ${response.status} ${response.statusText}`;
538
531
  if (response.status === 404) {
@@ -666,12 +659,7 @@ export async function handleNpmDeps(args) {
666
659
  };
667
660
  }
668
661
  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
- });
662
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
675
663
  if (!response.ok) {
676
664
  return {
677
665
  package: packageNameForOutput,
@@ -797,12 +785,7 @@ export async function handleNpmTypes(args) {
797
785
  };
798
786
  }
799
787
  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
- });
788
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${version}`);
806
789
  if (!response.ok) {
807
790
  return {
808
791
  package: packageNameForOutput,
@@ -824,12 +807,7 @@ export async function handleNpmTypes(args) {
824
807
  isAvailable: false,
825
808
  };
826
809
  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
- });
810
+ const typesResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${typesPackageName}/latest`);
833
811
  if (typesResponse.ok) {
834
812
  const typesData = (await typesResponse.json());
835
813
  typesPackageInfo = {
@@ -936,12 +914,7 @@ export async function handleNpmSize(args) {
936
914
  };
937
915
  }
938
916
  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
- });
917
+ const response = await fetchWithRetry(`https://bundlephobia.com/api/size?package=${bundlephobiaQuery}`);
945
918
  if (!response.ok) {
946
919
  let errorMsg = `Failed to fetch package size: ${response.status} ${response.statusText}`;
947
920
  if (response.status === 404) {
@@ -1021,9 +994,7 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
1021
994
  const encodedName = encodeURIComponent(pkgName);
1022
995
  const encodedVersion = encodeURIComponent(version);
1023
996
  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
- });
997
+ const response = await fetchWithRetry(url);
1027
998
  if (!response.ok) {
1028
999
  console.warn(`deps.dev API returned ${response.status} for ${pkgName}@${version}`);
1029
1000
  return [];
@@ -1046,9 +1017,7 @@ async function fetchTransitiveDependenciesFromDepsDev(pkgName, version) {
1046
1017
  // Helper to resolve 'latest' tag to actual version number
1047
1018
  async function resolveLatestVersion(packageName) {
1048
1019
  try {
1049
- const response = await fetch(`${NPM_REGISTRY_URL}/${packageName}/latest`, {
1050
- headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
1051
- });
1020
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${packageName}/latest`);
1052
1021
  if (!response.ok)
1053
1022
  return null;
1054
1023
  const data = (await response.json());
@@ -1069,9 +1038,7 @@ async function enrichVulnerabilityData(vulnId, ignoreCache = false) {
1069
1038
  if (cached)
1070
1039
  return cached;
1071
1040
  try {
1072
- const response = await fetch(`https://api.osv.dev/v1/vulns/${vulnId}`, {
1073
- headers: { 'User-Agent': 'NPM-Sentinel-MCP' },
1074
- });
1041
+ const response = await fetchWithRetry(`https://api.osv.dev/v1/vulns/${vulnId}`);
1075
1042
  if (!response.ok)
1076
1043
  return null;
1077
1044
  const data = await response.json();
@@ -1218,7 +1185,7 @@ export async function handleNpmVulnerabilities(args) {
1218
1185
  let apiResults = [];
1219
1186
  if (finalBatchQueries.length > 0) {
1220
1187
  // Perform Batch API call to OSV for non-cached items
1221
- const response = await fetch('https://api.osv.dev/v1/querybatch', {
1188
+ const response = await fetchWithRetry('https://api.osv.dev/v1/querybatch', {
1222
1189
  method: 'POST',
1223
1190
  headers: { 'Content-Type': 'application/json' },
1224
1191
  body: JSON.stringify({ queries: finalBatchQueries }),
@@ -1370,12 +1337,7 @@ export async function handleNpmTrends(args) {
1370
1337
  };
1371
1338
  }
1372
1339
  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
- });
1340
+ const response = await fetchWithRetry(`https://api.npmjs.org/downloads/point/${period}/${name}`);
1379
1341
  if (!response.ok) {
1380
1342
  let errorMsg = `Failed to fetch download trends: ${response.status} ${response.statusText}`;
1381
1343
  if (response.status === 404) {
@@ -1532,7 +1494,7 @@ export async function handleNpmCompare(args) {
1532
1494
  }
1533
1495
  try {
1534
1496
  // Fetch package version details from registry
1535
- const pkgResponse = await fetch(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
1497
+ const pkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
1536
1498
  if (!pkgResponse.ok) {
1537
1499
  throw new Error(`Failed to fetch package info for ${name}@${versionTag}: ${pkgResponse.status} ${pkgResponse.statusText}`);
1538
1500
  }
@@ -1543,7 +1505,7 @@ export async function handleNpmCompare(args) {
1543
1505
  // Fetch monthly downloads
1544
1506
  let monthlyDownloads = null;
1545
1507
  try {
1546
- const downloadsResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${name}`);
1508
+ const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
1547
1509
  if (downloadsResponse.ok) {
1548
1510
  const downloadsData = await downloadsResponse.json();
1549
1511
  if (isNpmDownloadsData(downloadsData)) {
@@ -1558,7 +1520,7 @@ export async function handleNpmCompare(args) {
1558
1520
  // Need to fetch the full package info to get to the 'time' field for specific version
1559
1521
  let publishDate = null;
1560
1522
  try {
1561
- const fullPkgInfoResponse = await fetch(`${NPM_REGISTRY_URL}/${name}`);
1523
+ const fullPkgInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
1562
1524
  if (fullPkgInfoResponse.ok) {
1563
1525
  const fullPkgInfo = await fullPkgInfoResponse.json();
1564
1526
  if (isNpmPackageInfo(fullPkgInfo) && fullPkgInfo.time) {
@@ -1623,6 +1585,119 @@ export async function handleNpmCompare(args) {
1623
1585
  };
1624
1586
  }
1625
1587
  }
1588
+ export async function getLocalPackageMetrics(name, ignoreCache = false) {
1589
+ const cacheKey = generateCacheKey('localMetrics', name);
1590
+ const cached = ignoreCache ? undefined : cacheGet(cacheKey);
1591
+ if (cached)
1592
+ return cached;
1593
+ // 1. Fetch full packument from registry
1594
+ const registryResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
1595
+ if (!registryResponse.ok) {
1596
+ throw new Error(`Package ${name} not found on registry.`);
1597
+ }
1598
+ const packageInfo = await registryResponse.json();
1599
+ if (!isNpmPackageInfo(packageInfo)) {
1600
+ throw new Error(`Invalid registry data received for ${name}.`);
1601
+ }
1602
+ const latestVersion = packageInfo['dist-tags']?.latest;
1603
+ if (!latestVersion || !packageInfo.versions?.[latestVersion]) {
1604
+ throw new Error(`No latest version found for ${name}.`);
1605
+ }
1606
+ const versionData = packageInfo.versions[latestVersion];
1607
+ const hasTypes = Boolean(versionData.types || versionData.typings || versionData.dependencies?.[`@types/${name}`]);
1608
+ const hasReadme = Boolean(versionData.readme || packageInfo.readme);
1609
+ const dependencyCount = Object.keys(versionData.dependencies || {}).length;
1610
+ // 2. Fetch downloads for last month
1611
+ let downloadsLastMonth = 0;
1612
+ try {
1613
+ const downloadsResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${name}`);
1614
+ if (downloadsResponse.ok) {
1615
+ const dlData = await downloadsResponse.json();
1616
+ downloadsLastMonth = dlData.downloads || 0;
1617
+ }
1618
+ }
1619
+ catch (dlError) {
1620
+ console.debug(`Could not fetch downloads for ${name}: ${dlError}`);
1621
+ }
1622
+ // 3. Fetch GitHub stats (optional, graceful degradation)
1623
+ let github = {
1624
+ starsCount: 0,
1625
+ forksCount: 0,
1626
+ subscribersCount: 0,
1627
+ openIssuesCount: 0,
1628
+ hasGitHubData: false,
1629
+ };
1630
+ const repoUrl = versionData.repository?.url || packageInfo.repository?.url;
1631
+ if (repoUrl?.includes('github.com')) {
1632
+ const githubMatch = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
1633
+ if (githubMatch) {
1634
+ const [, owner, repo] = githubMatch;
1635
+ const cleanRepo = repo.replace(/\.git$/, '');
1636
+ try {
1637
+ const ghResponse = await fetchWithRetry(`https://api.github.com/repos/${owner}/${cleanRepo}`, {}, { maxRetries: 0 });
1638
+ if (ghResponse.ok) {
1639
+ const ghData = await ghResponse.json();
1640
+ github = {
1641
+ starsCount: ghData.stargazers_count || 0,
1642
+ forksCount: ghData.forks_count || 0,
1643
+ subscribersCount: ghData.subscribers_count || 0,
1644
+ openIssuesCount: ghData.open_issues_count || 0,
1645
+ hasGitHubData: true,
1646
+ };
1647
+ }
1648
+ }
1649
+ catch (ghError) {
1650
+ console.debug(`Could not fetch GitHub data for ${owner}/${cleanRepo}: ${ghError}`);
1651
+ }
1652
+ }
1653
+ }
1654
+ // 4. Calculate publish age
1655
+ let lastPublishDaysAgo = 365;
1656
+ const timeObj = packageInfo.time;
1657
+ if (timeObj?.[latestVersion]) {
1658
+ const publishDate = new Date(timeObj[latestVersion]);
1659
+ const diffTime = Math.abs(Date.now() - publishDate.getTime());
1660
+ lastPublishDaysAgo = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
1661
+ }
1662
+ // 5. Scoring Algorithm
1663
+ const popularityNPM = Math.min(1, Math.log10(Math.max(1, downloadsLastMonth)) / 8);
1664
+ let popularity = popularityNPM;
1665
+ if (github.hasGitHubData) {
1666
+ const popularityGH = Math.min(1, github.starsCount / 50000);
1667
+ popularity = (popularityNPM * 0.7) + (popularityGH * 0.3);
1668
+ }
1669
+ const quality = ((hasTypes ? 0.3 : 0) +
1670
+ (hasReadme ? 0.2 : 0) +
1671
+ Math.max(0, 0.5 - (dependencyCount * 0.005)));
1672
+ const maintenancePublish = lastPublishDaysAgo < 30 ? 1.0
1673
+ : lastPublishDaysAgo < 90 ? 0.8
1674
+ : lastPublishDaysAgo < 365 ? 0.5
1675
+ : 0.2;
1676
+ let maintenance = maintenancePublish;
1677
+ if (github.hasGitHubData) {
1678
+ const issuesScore = github.openIssuesCount < 50 ? 0.3 : github.openIssuesCount < 200 ? 0.2 : 0.1;
1679
+ maintenance = (maintenancePublish * 0.7) + issuesScore;
1680
+ }
1681
+ const finalScore = popularity * 0.4 + quality * 0.3 + maintenance * 0.3;
1682
+ const result = {
1683
+ packageName: name,
1684
+ version: latestVersion,
1685
+ description: versionData.description || null,
1686
+ analyzedAt: new Date().toISOString(),
1687
+ downloadsLastMonth,
1688
+ github,
1689
+ score: {
1690
+ final: finalScore,
1691
+ detail: {
1692
+ quality,
1693
+ popularity,
1694
+ maintenance,
1695
+ },
1696
+ },
1697
+ };
1698
+ cacheSet(cacheKey, result, CACHE_TTL_LONG);
1699
+ return result;
1700
+ }
1626
1701
  // Function to get package quality metrics
1627
1702
  export async function handleNpmQuality(args) {
1628
1703
  try {
@@ -1634,7 +1709,7 @@ export async function handleNpmQuality(args) {
1634
1709
  let name = '';
1635
1710
  if (typeof pkgInput === 'string') {
1636
1711
  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
1712
+ name = atIdx > 0 ? pkgInput.slice(0, atIdx) : pkgInput;
1638
1713
  }
1639
1714
  else {
1640
1715
  return {
@@ -1679,57 +1754,20 @@ export async function handleNpmQuality(args) {
1679
1754
  };
1680
1755
  }
1681
1756
  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;
1757
+ const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
1715
1758
  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.
1759
+ analyzedAt: metrics.analyzedAt,
1760
+ versionInScore: metrics.version,
1761
+ qualityScore: metrics.score.detail.quality,
1721
1762
  };
1722
- const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
1723
- ? CACHE_TTL_SHORT
1724
- : CACHE_TTL_LONG;
1725
- cacheSet(cacheKey, qualityData, ttl);
1763
+ cacheSet(cacheKey, qualityData, CACHE_TTL_LONG);
1726
1764
  return {
1727
1765
  packageInput: pkgInput,
1728
1766
  packageName: name,
1729
1767
  status: 'success',
1730
1768
  error: null,
1731
1769
  data: qualityData,
1732
- message: `Successfully fetched quality score for ${name} (version analyzed: ${collected.metadata.version}).`,
1770
+ message: `Successfully fetched quality score for ${name} (version analyzed: ${metrics.version}).`,
1733
1771
  };
1734
1772
  }
1735
1773
  catch (error) {
@@ -1762,6 +1800,7 @@ export async function handleNpmQuality(args) {
1762
1800
  };
1763
1801
  }
1764
1802
  }
1803
+ // Function to get package maintenance metrics
1765
1804
  export async function handleNpmMaintenance(args) {
1766
1805
  try {
1767
1806
  const packagesToProcess = args.packages || [];
@@ -1817,55 +1856,20 @@ export async function handleNpmMaintenance(args) {
1817
1856
  };
1818
1857
  }
1819
1858
  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;
1859
+ const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
1853
1860
  const maintenanceData = {
1854
- analyzedAt: analyzedAt,
1855
- versionInScore: collected.metadata.version,
1856
- maintenanceScore: maintenanceScoreValue,
1861
+ analyzedAt: metrics.analyzedAt,
1862
+ versionInScore: metrics.version,
1863
+ maintenanceScore: metrics.score.detail.maintenance,
1857
1864
  };
1858
- const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
1859
- ? CACHE_TTL_SHORT
1860
- : CACHE_TTL_LONG;
1861
- cacheSet(cacheKey, maintenanceData, ttl);
1865
+ cacheSet(cacheKey, maintenanceData, CACHE_TTL_LONG);
1862
1866
  return {
1863
1867
  packageInput: pkgInput,
1864
1868
  packageName: name,
1865
1869
  status: 'success',
1866
1870
  error: null,
1867
1871
  data: maintenanceData,
1868
- message: `Successfully fetched maintenance score for ${name} (version analyzed: ${collected.metadata.version}).`,
1872
+ message: `Successfully fetched maintenance score for ${name} (version analyzed: ${metrics.version}).`,
1869
1873
  };
1870
1874
  }
1871
1875
  catch (error) {
@@ -2025,6 +2029,7 @@ export async function handleNpmMaintainers(args) {
2025
2029
  };
2026
2030
  }
2027
2031
  }
2032
+ // Function to get package score
2028
2033
  export async function handleNpmScore(args) {
2029
2034
  try {
2030
2035
  const packagesToProcess = args.packages || [];
@@ -2078,86 +2083,40 @@ export async function handleNpmScore(args) {
2078
2083
  };
2079
2084
  }
2080
2085
  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;
2086
+ const metrics = await getLocalPackageMetrics(name, args.ignoreCache);
2118
2087
  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
- },
2088
+ analyzedAt: metrics.analyzedAt,
2089
+ versionInScore: metrics.version,
2090
+ score: metrics.score,
2129
2091
  packageInfoFromScore: {
2130
- name: collected.metadata.name,
2131
- version: collected.metadata.version,
2132
- description: collected.metadata.description || null,
2092
+ name: metrics.packageName,
2093
+ version: metrics.version,
2094
+ description: metrics.description,
2133
2095
  },
2134
2096
  npmStats: {
2135
- downloadsLastMonth: lastMonthDownloads,
2136
- starsCount: collected.npm.starsCount,
2097
+ downloadsLastMonth: metrics.downloadsLastMonth,
2098
+ starsCount: metrics.github.hasGitHubData ? metrics.github.starsCount : 0, // Fallback best effort
2137
2099
  },
2138
- githubStats: collected.github
2100
+ githubStats: metrics.github.hasGitHubData
2139
2101
  ? {
2140
- starsCount: collected.github.starsCount,
2141
- forksCount: collected.github.forksCount,
2142
- subscribersCount: collected.github.subscribersCount,
2102
+ starsCount: metrics.github.starsCount,
2103
+ forksCount: metrics.github.forksCount,
2104
+ subscribersCount: metrics.github.subscribersCount,
2143
2105
  issues: {
2144
- count: collected.github.issues.count,
2145
- openCount: collected.github.issues.openCount,
2106
+ count: metrics.github.openIssuesCount, // Heuristic default count = open issues
2107
+ openCount: metrics.github.openIssuesCount,
2146
2108
  },
2147
2109
  }
2148
2110
  : null,
2149
2111
  };
2150
- const ttl = !collected.metadata.version.match(/^\d+\.\d+\.\d+$/)
2151
- ? CACHE_TTL_SHORT
2152
- : CACHE_TTL_LONG;
2153
- cacheSet(cacheKey, scoreData, ttl);
2112
+ cacheSet(cacheKey, scoreData, CACHE_TTL_LONG);
2154
2113
  return {
2155
2114
  packageInput: pkgInput,
2156
2115
  packageName: name,
2157
2116
  status: 'success',
2158
2117
  error: null,
2159
2118
  data: scoreData,
2160
- message: `Successfully fetched score data for ${name} (version analyzed: ${collected.metadata.version}).`,
2119
+ message: `Successfully fetched score data for ${name} (version analyzed: ${metrics.version}).`,
2161
2120
  };
2162
2121
  }
2163
2122
  catch (error) {
@@ -2190,6 +2149,27 @@ export async function handleNpmScore(args) {
2190
2149
  };
2191
2150
  }
2192
2151
  }
2152
+ export async function fetchReadmeFromCDN(packageName, version) {
2153
+ const cdnUrls = [
2154
+ `https://cdn.jsdelivr.net/npm/${packageName}@${version}/README.md`,
2155
+ `https://unpkg.com/${packageName}@${version}/README.md`,
2156
+ ];
2157
+ for (const cdnUrl of cdnUrls) {
2158
+ try {
2159
+ const response = await fetchWithRetry(cdnUrl, { headers: { Accept: 'text/plain' } }, { maxRetries: 1, skipThrottle: true });
2160
+ if (response.ok) {
2161
+ const text = await response.text();
2162
+ if (text && text.length > 10) {
2163
+ return text;
2164
+ }
2165
+ }
2166
+ }
2167
+ catch {
2168
+ // Sigue intentando con la próxima URL
2169
+ }
2170
+ }
2171
+ return null;
2172
+ }
2193
2173
  export async function handleNpmPackageReadme(args) {
2194
2174
  try {
2195
2175
  const packagesToProcess = args.packages || [];
@@ -2259,7 +2239,7 @@ export async function handleNpmPackageReadme(args) {
2259
2239
  };
2260
2240
  }
2261
2241
  try {
2262
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}`);
2242
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
2263
2243
  if (!response.ok) {
2264
2244
  let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
2265
2245
  if (response.status === 404) {
@@ -2301,11 +2281,19 @@ export async function handleNpmPackageReadme(args) {
2301
2281
  }
2302
2282
  const versionData = packageInfo.versions[versionToUse];
2303
2283
  // README can be in version-specific data or at the root of packageInfo
2304
- const readmeContent = versionData.readme || packageInfo.readme || null;
2284
+ let readmeContent = versionData.readme || packageInfo.readme || null;
2285
+ let readmeSource = readmeContent ? 'registry' : null;
2286
+ if (!readmeContent && versionToUse) {
2287
+ readmeContent = await fetchReadmeFromCDN(name, versionToUse);
2288
+ if (readmeContent) {
2289
+ readmeSource = 'cdn';
2290
+ }
2291
+ }
2305
2292
  const hasReadme = !!readmeContent;
2306
2293
  const readmeResultData = {
2307
2294
  readme: readmeContent,
2308
2295
  hasReadme: hasReadme,
2296
+ readmeSource: readmeSource,
2309
2297
  versionFetched: versionToUse, // Store the actually fetched version
2310
2298
  };
2311
2299
  cacheSet(cacheKey, readmeResultData, CACHE_TTL_LONG);
@@ -2316,7 +2304,7 @@ export async function handleNpmPackageReadme(args) {
2316
2304
  versionFetched: versionToUse,
2317
2305
  status: 'success',
2318
2306
  error: null,
2319
- data: { readme: readmeContent, hasReadme: hasReadme }, // Return only readme and hasReadme in data field for consistency
2307
+ data: { readme: readmeContent, hasReadme: hasReadme, readmeSource: readmeSource }, // Return only readme and hasReadme in data field for consistency
2320
2308
  message: `Successfully fetched README for ${name}@${versionToUse}.`,
2321
2309
  };
2322
2310
  }
@@ -2370,7 +2358,7 @@ export async function handleNpmSearch(args) {
2370
2358
  message: `Search results for query '${query}' with limit ${limit} from cache.`,
2371
2359
  };
2372
2360
  }
2373
- const response = await fetch(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
2361
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`);
2374
2362
  if (!response.ok) {
2375
2363
  throw new Error(`Failed to search packages: ${response.status} ${response.statusText}`);
2376
2364
  }
@@ -2508,7 +2496,7 @@ export async function handleNpmLicenseCompatibility(args) {
2508
2496
  };
2509
2497
  }
2510
2498
  try {
2511
- const response = await fetch(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
2499
+ const response = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/${versionTag}`);
2512
2500
  if (!response.ok) {
2513
2501
  let errorMsg = `Failed to fetch package info: ${response.status} ${response.statusText}`;
2514
2502
  if (response.status === 404) {
@@ -2683,7 +2671,7 @@ export async function handleNpmRepoStats(args) {
2683
2671
  };
2684
2672
  }
2685
2673
  try {
2686
- const npmResponse = await fetch(`${NPM_REGISTRY_URL}/${name}/latest`);
2674
+ const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}/latest`);
2687
2675
  if (!npmResponse.ok) {
2688
2676
  const errorData = {
2689
2677
  packageInput: pkgInput,
@@ -2736,10 +2724,9 @@ export async function handleNpmRepoStats(args) {
2736
2724
  }
2737
2725
  const [, owner, repo] = githubMatch;
2738
2726
  const githubRepoApiUrl = `https://api.github.com/repos/${owner}/${repo.replace(/\.git$/, '')}`;
2739
- const githubResponse = await fetch(githubRepoApiUrl, {
2727
+ const githubResponse = await fetchWithRetry(githubRepoApiUrl, {
2740
2728
  headers: {
2741
2729
  Accept: 'application/vnd.github.v3+json',
2742
- 'User-Agent': 'NPM-Sentinel-MCP',
2743
2730
  },
2744
2731
  });
2745
2732
  if (!githubResponse.ok) {
@@ -2860,7 +2847,7 @@ export async function handleNpmDeprecated(args) {
2860
2847
  }
2861
2848
  // console.debug(`[handleNpmDeprecated] Cache miss for ${cacheKey}`);
2862
2849
  try {
2863
- const mainPkgResponse = await fetch(`${NPM_REGISTRY_URL}/${name}`);
2850
+ const mainPkgResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
2864
2851
  if (!mainPkgResponse.ok) {
2865
2852
  return {
2866
2853
  package: initialPackageNameForOutput,
@@ -2902,7 +2889,7 @@ export async function handleNpmDeprecated(args) {
2902
2889
  let statusMessage = '';
2903
2890
  try {
2904
2891
  // console.debug(`[handleNpmDeprecated] Checking dependency: ${depName}@${depSemVer}`);
2905
- const depInfoResponse = await fetch(`${NPM_REGISTRY_URL}/${encodeURIComponent(depName)}`);
2892
+ const depInfoResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${encodeURIComponent(depName)}`);
2906
2893
  if (!depInfoResponse.ok) {
2907
2894
  statusMessage = `Could not fetch dependency info for '${depName}' (status: ${depInfoResponse.status}). Deprecation status unknown.`;
2908
2895
  // console.warn(`[handleNpmDeprecated] ${statusMessage}`);
@@ -3079,7 +3066,7 @@ export async function handleNpmChangelogAnalysis(args) {
3079
3066
  };
3080
3067
  }
3081
3068
  try {
3082
- const npmResponse = await fetch(`${NPM_REGISTRY_URL}/${name}`);
3069
+ const npmResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/${name}`);
3083
3070
  if (!npmResponse.ok) {
3084
3071
  const errorResult = {
3085
3072
  packageInput: pkgInput,
@@ -3153,7 +3140,7 @@ export async function handleNpmChangelogAnalysis(args) {
3153
3140
  for (const file of changelogFiles) {
3154
3141
  try {
3155
3142
  const rawChangelogUrl = `https://raw.githubusercontent.com/${owner}/${repoNameForUrl}/master/${file}`;
3156
- const response = await fetch(rawChangelogUrl);
3143
+ const response = await fetchWithRetry(rawChangelogUrl);
3157
3144
  if (response.ok) {
3158
3145
  changelogContent = await response.text();
3159
3146
  changelogSourceUrl = rawChangelogUrl;
@@ -3167,10 +3154,9 @@ export async function handleNpmChangelogAnalysis(args) {
3167
3154
  }
3168
3155
  let githubReleases = [];
3169
3156
  try {
3170
- const githubApiResponse = await fetch(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
3157
+ const githubApiResponse = await fetchWithRetry(`https://api.github.com/repos/${owner}/${repoNameForUrl}/releases?per_page=5`, {
3171
3158
  headers: {
3172
3159
  Accept: 'application/vnd.github.v3+json',
3173
- 'User-Agent': 'NPM-Sentinel-MCP',
3174
3160
  },
3175
3161
  });
3176
3162
  if (githubApiResponse.ok) {
@@ -3258,12 +3244,10 @@ export async function handleNpmAlternatives(args) {
3258
3244
  }
3259
3245
  const processedResults = await Promise.all(packagesToProcess.map(async (pkgInput) => {
3260
3246
  let originalPackageName = '';
3261
- let versionQueried;
3262
3247
  if (typeof pkgInput === 'string') {
3263
3248
  const atIdx = pkgInput.lastIndexOf('@');
3264
3249
  if (atIdx > 0) {
3265
3250
  originalPackageName = pkgInput.slice(0, atIdx);
3266
- versionQueried = pkgInput.slice(atIdx + 1);
3267
3251
  }
3268
3252
  else {
3269
3253
  originalPackageName = pkgInput;
@@ -3312,7 +3296,7 @@ export async function handleNpmAlternatives(args) {
3312
3296
  };
3313
3297
  }
3314
3298
  try {
3315
- const searchResponse = await fetch(`${NPM_REGISTRY_URL}/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=10`);
3299
+ const searchResponse = await fetchWithRetry(`${NPM_REGISTRY_URL}/-/v1/search?text=keywords:${encodeURIComponent(originalPackageName)}&size=10`);
3316
3300
  if (!searchResponse.ok) {
3317
3301
  const errorResult = {
3318
3302
  packageInput: pkgInput,
@@ -3328,7 +3312,7 @@ export async function handleNpmAlternatives(args) {
3328
3312
  const alternativePackagesRaw = searchData.objects || [];
3329
3313
  let originalPackageDownloads = 0;
3330
3314
  try {
3331
- const dlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
3315
+ const dlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${originalPackageName}`);
3332
3316
  if (dlResponse.ok) {
3333
3317
  originalPackageDownloads =
3334
3318
  (await dlResponse.json()).downloads || 0;
@@ -3364,7 +3348,7 @@ export async function handleNpmAlternatives(args) {
3364
3348
  .map(async (alt) => {
3365
3349
  let altDownloads = 0;
3366
3350
  try {
3367
- const altDlResponse = await fetch(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
3351
+ const altDlResponse = await fetchWithRetry(`https://api.npmjs.org/downloads/point/last-month/${alt.package.name}`);
3368
3352
  if (altDlResponse.ok) {
3369
3353
  altDownloads = (await altDlResponse.json()).downloads || 0;
3370
3354
  }
@@ -3445,7 +3429,7 @@ export default function createServer({ config }) {
3445
3429
  __filename = fileURLToPath(import.meta.url);
3446
3430
  __dirname = path.dirname(__filename);
3447
3431
  }
3448
- catch (error) {
3432
+ catch {
3449
3433
  // Fallback for CJS environment (Smithery HTTP build)
3450
3434
  __filename = process.argv[1] || '.';
3451
3435
  __dirname = path.dirname(__filename);
@@ -3884,7 +3868,7 @@ function isNpmPackageVersionData(data) {
3884
3868
  }
3885
3869
  return result.success;
3886
3870
  }
3887
- catch (e) {
3871
+ catch {
3888
3872
  // This catch block might not be strictly necessary with safeParse but kept for safety
3889
3873
  // console.error("isNpmPackageVersionData validation failed unexpectedly:", e);
3890
3874
  return false;