UNPKG

@nekzus/mcp-server

Version:

MCP server for comprehensive NPM package analysis. Provides real-time insights into package quality, security, dependencies, and metrics. Built on the MCP SDK for seamless integration with Claude and Anthropic AI tools.

1,359 lines β€’ 71.2 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import fetch from 'node-fetch'; import { z } from 'zod'; // Zod schemas for npm package data export const NpmMaintainerSchema = z .object({ name: z.string(), email: z.string().optional(), url: z.string().optional(), }) .passthrough(); export const NpmPackageVersionSchema = z .object({ name: z.string(), version: z.string(), description: z.string().optional(), author: z.union([z.string(), z.object({}).passthrough()]).optional(), license: z.string().optional(), repository: z .object({ type: z.string().optional(), url: z.string().optional(), }) .passthrough() .optional(), bugs: z .object({ url: z.string().optional(), }) .passthrough() .optional(), homepage: z.string().optional(), }) .passthrough(); export const NpmPackageInfoSchema = z .object({ name: z.string(), 'dist-tags': z.record(z.string()), versions: z.record(NpmPackageVersionSchema), time: z.record(z.string()).optional(), repository: z .object({ type: z.string().optional(), url: z.string().optional(), }) .passthrough() .optional(), bugs: z .object({ url: z.string().optional(), }) .passthrough() .optional(), homepage: z.string().optional(), maintainers: z.array(NpmMaintainerSchema).optional(), }) .passthrough(); export const NpmPackageDataSchema = z.object({ name: z.string(), version: z.string(), description: z.string().optional(), license: z.string().optional(), dependencies: z.record(z.string()).optional(), devDependencies: z.record(z.string()).optional(), peerDependencies: z.record(z.string()).optional(), types: z.string().optional(), typings: z.string().optional(), }); export const BundlephobiaDataSchema = z.object({ size: z.number(), gzip: z.number(), dependencyCount: z.number(), }); export const NpmDownloadsDataSchema = z.object({ downloads: z.number(), start: z.string(), end: z.string(), package: z.string(), }); // Schemas for NPM quality, maintenance and popularity metrics export const NpmQualitySchema = z.object({ score: z.number(), tests: z.number(), coverage: z.number(), linting: z.number(), types: z.number(), }); export const NpmMaintenanceSchema = z.object({ score: z.number(), issuesResolutionTime: z.number(), commitsFrequency: z.number(), releaseFrequency: z.number(), lastUpdate: z.string(), }); export const NpmPopularitySchema = z.object({ score: z.number(), stars: z.number(), downloads: z.number(), dependents: z.number(), communityInterest: z.number(), }); function isValidNpmsResponse(data) { if (typeof data !== 'object' || data === null) { console.debug('Response is not an object or is null'); return false; } const response = data; // Check score structure if (!response.score || typeof response.score !== 'object' || !('final' in response.score) || typeof response.score.final !== 'number' || !('detail' in response.score) || typeof response.score.detail !== 'object') { console.debug('Invalid score structure'); return false; } // Check score detail metrics const detail = response.score.detail; if (typeof detail.quality !== 'number' || typeof detail.popularity !== 'number' || typeof detail.maintenance !== 'number') { console.debug('Invalid score detail metrics'); return false; } // Check collected data structure if (!response.collected || typeof response.collected !== 'object' || !response.collected.metadata || typeof response.collected.metadata !== 'object' || typeof response.collected.metadata.name !== 'string' || typeof response.collected.metadata.version !== 'string') { console.debug('Invalid collected data structure'); return false; } // Check npm data if (!response.collected.npm || typeof response.collected.npm !== 'object' || !Array.isArray(response.collected.npm.downloads) || typeof response.collected.npm.starsCount !== 'number') { console.debug('Invalid npm data structure'); return false; } // Optional github data check if (response.collected.github) { if (typeof response.collected.github !== 'object' || typeof response.collected.github.starsCount !== 'number' || typeof response.collected.github.forksCount !== 'number' || typeof response.collected.github.subscribersCount !== 'number' || !response.collected.github.issues || typeof response.collected.github.issues !== 'object' || typeof response.collected.github.issues.count !== 'number' || typeof response.collected.github.issues.openCount !== 'number') { console.debug('Invalid github data structure'); return false; } } return true; } export const NpmSearchResultSchema = z .object({ objects: z.array(z.object({ package: z.object({ name: z.string(), version: z.string(), description: z.string().optional(), keywords: z.array(z.string()).optional(), publisher: z .object({ username: z.string(), }) .optional(), links: z .object({ npm: z.string().optional(), homepage: z.string().optional(), repository: z.string().optional(), }) .optional(), }), score: z.object({ final: z.number(), detail: z.object({ quality: z.number(), popularity: z.number(), maintenance: z.number(), }), }), searchScore: z.number(), })), total: z.number(), }) .passthrough(); // Logger function that uses stderr - only for critical errors const log = (...args) => { // Filter out server status messages const message = args[0]; if (typeof message === 'string' && (!message.startsWith('[Server]') || message.includes('error') || message.includes('Error'))) { console.error(...args); } }; // Define tools const TOOLS = [ // NPM Package Analysis Tools { name: 'npmVersions', description: 'Get all available versions of an NPM package', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z.array(z.string()).describe('List of package names to get versions for'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmLatest', description: 'Get the latest version and changelog of an NPM package', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z.array(z.string()).describe('List of package names to get latest versions for'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmDeps', description: 'Analyze dependencies and devDependencies of an NPM package', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z.array(z.string()).describe('List of package names to analyze dependencies for'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmTypes', description: 'Check TypeScript types availability and version for a package', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z.array(z.string()).describe('List of package names to check types for'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmSize', description: 'Get package size information including dependencies and bundle size', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z.array(z.string()).describe('List of package names to get size information for'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmVulnerabilities', description: 'Check for known vulnerabilities in packages', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z .array(z.string()) .describe('List of package names to check for vulnerabilities'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmTrends', description: 'Get download trends and popularity metrics for packages. Available periods: "last-week" (7 days), "last-month" (30 days), or "last-year" (365 days)', parameters: z.object({ packages: z.array(z.string()).describe('List of package names to get trends for'), period: z .enum(['last-week', 'last-month', 'last-year']) .describe('Time period for trends. Options: "last-week", "last-month", "last-year"') .optional() .default('last-month'), }), inputSchema: { type: 'object', properties: { packages: { type: 'array', items: { type: 'string' }, description: 'List of package names to get trends for', }, period: { type: 'string', enum: ['last-week', 'last-month', 'last-year'], description: 'Time period for trends. Options: "last-week" (7 days), "last-month" (30 days), or "last-year" (365 days)', default: 'last-month', }, }, required: ['packages'], }, }, { name: 'npmCompare', description: 'Compare multiple NPM packages based on various metrics', parameters: z.object({ packages: z.array(z.string()).describe('List of package names to compare'), }), inputSchema: { type: 'object', properties: { packages: { type: 'array', items: { type: 'string' }, }, }, required: ['packages'], }, }, { name: 'npmMaintainers', description: 'Get maintainers for an NPM package', parameters: z.object({ packageName: z.string().describe('The name of the package'), }), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, }, required: ['packageName'], }, }, { name: 'npmScore', description: 'Get consolidated package score based on quality, maintenance, and popularity metrics', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z.array(z.string()).describe('List of package names to get scores for'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmPackageReadme', description: 'Get the README for an NPM package', parameters: z.union([ z.object({ packageName: z.string().describe('The name of the package'), }), z.object({ packages: z.array(z.string()).describe('List of package names to get READMEs for'), }), ]), inputSchema: { type: 'object', properties: { packageName: { type: 'string' }, packages: { type: 'array', items: { type: 'string' } }, }, oneOf: [{ required: ['packageName'] }, { required: ['packages'] }], }, }, { name: 'npmSearch', description: 'Search for NPM packages', parameters: z.object({ query: z.string().describe('Search query for packages'), limit: z .number() .min(1) .max(50) .optional() .describe('Maximum number of results to return (default: 10)'), }), inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number', minimum: 1, maximum: 50 }, }, required: ['query'], }, }, ]; // Type guards for API responses function isNpmPackageInfo(data) { return (typeof data === 'object' && data !== null && (!('maintainers' in data) || (Array.isArray(data.maintainers) && (data.maintainers?.every((m) => typeof m === 'object' && m !== null && 'name' in m && 'email' in m && typeof m.name === 'string' && typeof m.email === 'string') ?? true)))); } function isNpmPackageData(data) { try { return NpmPackageDataSchema.parse(data) !== null; } catch { return false; } } function isBundlephobiaData(data) { try { return BundlephobiaDataSchema.parse(data) !== null; } catch { return false; } } function isNpmDownloadsData(data) { try { return NpmDownloadsDataSchema.parse(data) !== null; } catch { return false; } } async function handleNpmVersions(args) { try { const packagesToProcess = args.packages || []; if (packagesToProcess.length === 0) { throw new Error('No package names provided'); } const results = await Promise.all(packagesToProcess.map(async (pkg) => { const response = await fetch(`https://registry.npmjs.org/${pkg}`); if (!response.ok) { return { name: pkg, error: `Failed to fetch package info: ${response.statusText}` }; } const rawData = await response.json(); if (!isNpmPackageInfo(rawData)) { return { name: pkg, error: 'Invalid package info data received' }; } const versions = Object.keys(rawData.versions ?? {}).sort((a, b) => { const [aMajor = 0, aMinor = 0, aPatch = 0] = a.split('.').map(Number); const [bMajor = 0, bMinor = 0, bPatch = 0] = b.split('.').map(Number); if (aMajor !== bMajor) return aMajor - bMajor; if (aMinor !== bMinor) return aMinor - bMinor; return aPatch - bPatch; }); return { name: pkg, versions }; })); let text = ''; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; } else { text += `πŸ“¦ Available versions for ${result.name}:\n${result.versions.join('\n')}\n\n`; } } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching package versions: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmLatest(args) { try { const packages = args.packages || []; let text = ''; for (const pkg of packages) { const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`); if (!response.ok) { throw new Error(`Failed to fetch latest version for ${pkg}: ${response.statusText}`); } const data = (await response.json()); text += `πŸ“¦ Latest version of ${pkg}\n`; text += `Version: ${data.version}\n`; text += `Description: ${data.description || 'No description available'}\n`; text += `Author: ${data.author?.name || 'Unknown'}\n`; text += `License: ${data.license || 'Unknown'}\n`; text += `Homepage: ${data.homepage || 'Not specified'}\n\n`; text += '---\n\n'; } return { content: [ { type: 'text', text, }, ], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching latest version: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmDeps(args) { try { const packagesToProcess = args.packages || []; if (packagesToProcess.length === 0) { throw new Error('No package names provided'); } const results = await Promise.all(packagesToProcess.map(async (pkg) => { try { const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`); if (!response.ok) { return { name: pkg, error: `Failed to fetch package info: ${response.statusText}` }; } const rawData = await response.json(); if (!isNpmPackageData(rawData)) { return { name: pkg, error: 'Invalid package data received' }; } return { name: pkg, version: rawData.version, dependencies: rawData.dependencies ?? {}, devDependencies: rawData.devDependencies ?? {}, peerDependencies: rawData.peerDependencies ?? {}, }; } catch (error) { return { name: pkg, error: error instanceof Error ? error.message : 'Unknown error' }; } })); let text = ''; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; continue; } text += `πŸ“¦ Dependencies for ${result.name}@${result.version}\n\n`; if (Object.keys(result.dependencies).length > 0) { text += 'Dependencies:\n'; for (const [dep, version] of Object.entries(result.dependencies)) { text += `β€’ ${dep}: ${version}\n`; } text += '\n'; } if (Object.keys(result.devDependencies).length > 0) { text += 'Dev Dependencies:\n'; for (const [dep, version] of Object.entries(result.devDependencies)) { text += `β€’ ${dep}: ${version}\n`; } text += '\n'; } if (Object.keys(result.peerDependencies).length > 0) { text += 'Peer Dependencies:\n'; for (const [dep, version] of Object.entries(result.peerDependencies)) { text += `β€’ ${dep}: ${version}\n`; } text += '\n'; } text += '---\n\n'; } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching dependencies: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmTypes(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`); if (!response.ok) { throw new Error(`Failed to fetch package info: ${response.statusText}`); } const data = (await response.json()); let text = `πŸ“¦ TypeScript support for ${pkg}@${data.version}\n`; const hasTypes = Boolean(data.types || data.typings); if (hasTypes) { text += 'βœ… Package includes built-in TypeScript types\n'; text += `Types path: ${data.types || data.typings}\n`; } const typesPackage = `@types/${pkg.replace('@', '').replace('/', '__')}`; const typesResponse = await fetch(`https://registry.npmjs.org/${typesPackage}/latest`).catch(() => null); if (typesResponse?.ok) { const typesData = (await typesResponse.json()); text += `πŸ“¦ DefinitelyTyped package available: ${typesPackage}@${typesData.version}\n`; text += `Install with: npm install -D ${typesPackage}`; } else if (!hasTypes) { text += '❌ No TypeScript type definitions found'; } return { name: pkg, text }; })); let text = ''; for (const result of results) { text += `${result.text}\n\n`; if (results.indexOf(result) < results.length - 1) { text += '---\n\n'; } } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error checking TypeScript types: ${error.message}` }, ], isError: true, }; } } async function handleNpmSize(args) { try { const packagesToProcess = args.packages || []; if (packagesToProcess.length === 0) { throw new Error('No package names provided'); } const results = await Promise.all(packagesToProcess.map(async (pkg) => { const response = await fetch(`https://bundlephobia.com/api/size?package=${pkg}`); if (!response.ok) { return { name: pkg, error: `Failed to fetch package size: ${response.statusText}` }; } const rawData = await response.json(); if (!isBundlephobiaData(rawData)) { return { name: pkg, error: 'Invalid response from bundlephobia' }; } return { name: pkg, sizeInKb: Number((rawData.size / 1024).toFixed(2)), gzipInKb: Number((rawData.gzip / 1024).toFixed(2)), dependencyCount: rawData.dependencyCount, }; })); let text = ''; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; } else { text += `πŸ“¦ ${result.name}\n`; text += `Size: ${result.sizeInKb}KB (gzipped: ${result.gzipInKb}KB)\n`; text += `Dependencies: ${result.dependencyCount}\n\n`; } } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching package sizes: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmVulnerabilities(args) { try { const packagesToProcess = args.packages || []; if (packagesToProcess.length === 0) { throw new Error('No package names provided'); } const results = await Promise.all(packagesToProcess.map(async (pkg) => { const response = await fetch('https://api.osv.dev/v1/query', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ package: { name: pkg, ecosystem: 'npm', }, }), }); if (!response.ok) { return { name: pkg, error: `Failed to fetch vulnerability info: ${response.statusText}` }; } const data = (await response.json()); return { name: pkg, vulns: data.vulns || [] }; })); let text = 'πŸ”’ Security Analysis\n\n'; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; continue; } text += `πŸ“¦ ${result.name}\n`; if (result.vulns.length === 0) { text += 'βœ… No known vulnerabilities\n\n'; } else { text += `⚠️ Found ${result.vulns.length} vulnerabilities:\n\n`; for (const vuln of result.vulns) { text += `- ${vuln.summary}\n`; const severity = typeof vuln.severity === 'object' ? vuln.severity.type || 'Unknown' : vuln.severity || 'Unknown'; text += ` Severity: ${severity}\n`; if (vuln.references && vuln.references.length > 0) { text += ` More info: ${vuln.references[0].url}\n`; } text += '\n'; } } text += '---\n\n'; } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error checking vulnerabilities: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmTrends(args) { try { const period = args.period || 'last-month'; const periodDays = { 'last-week': 7, 'last-month': 30, 'last-year': 365, }; const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://api.npmjs.org/downloads/point/${period}/${pkg}`); if (!response.ok) { return { name: pkg, error: `Failed to fetch download trends: ${response.statusText}`, success: false, }; } const data = await response.json(); if (!isNpmDownloadsData(data)) { return { name: pkg, error: 'Invalid response format from npm downloads API', success: false, }; } return { name: pkg, downloads: data.downloads, success: true, }; })); let text = 'πŸ“ˆ Download Trends\n\n'; text += `Period: ${period} (${periodDays[period]} days)\n\n`; // Individual package stats for (const result of results) { if (!result.success) { text += `❌ ${result.name}: ${result.error}\n`; continue; } text += `πŸ“¦ ${result.name}\n`; text += `Total downloads: ${result.downloads.toLocaleString()}\n`; text += `Average daily downloads: ${Math.round(result.downloads / periodDays[period]).toLocaleString()}\n\n`; } // Total stats const totalDownloads = results.reduce((total, result) => { if (result.success) { return total + result.downloads; } return total; }, 0); text += `Total downloads across all packages: ${totalDownloads.toLocaleString()}\n`; text += `Average daily downloads across all packages: ${Math.round(totalDownloads / periodDays[period]).toLocaleString()}\n`; return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching download trends: ${error.message}` }, ], isError: true, }; } } async function handleNpmCompare(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const [infoRes, downloadsRes] = await Promise.all([ fetch(`https://registry.npmjs.org/${pkg}/latest`), fetch(`https://api.npmjs.org/downloads/point/last-month/${pkg}`), ]); if (!infoRes.ok || !downloadsRes.ok) { throw new Error(`Failed to fetch data for ${pkg}`); } const info = await infoRes.json(); const downloads = await downloadsRes.json(); if (!isNpmPackageData(info) || !isNpmDownloadsData(downloads)) { throw new Error(`Invalid response format for ${pkg}`); } return { name: pkg, version: info.version, description: info.description, downloads: downloads.downloads, license: info.license, dependencies: Object.keys(info.dependencies || {}).length, }; })); let text = 'πŸ“Š Package Comparison\n\n'; // Table header text += 'Package | Version | Monthly Downloads | Dependencies | License\n'; text += '--------|---------|------------------|--------------|--------\n'; // Table rows for (const pkg of results) { text += `${pkg.name} | ${pkg.version} | ${pkg.downloads.toLocaleString()} | ${pkg.dependencies} | ${pkg.license || 'N/A'}\n`; } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [{ type: 'text', text: `Error comparing packages: ${error.message}` }], isError: true, }; } } // Function to get package quality metrics async function handleNpmQuality(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(pkg)}`); if (!response.ok) { return { name: pkg, error: `Failed to fetch quality data: ${response.statusText}` }; } const rawData = await response.json(); if (!isValidNpmsResponse(rawData)) { return { name: pkg, error: 'Invalid response format from npms.io API' }; } const quality = rawData.score.detail.quality; return { name: pkg, ...NpmQualitySchema.parse({ score: Math.round(quality * 100) / 100, tests: 0, // These values are no longer available in the API coverage: 0, linting: 0, types: 0, }), }; })); let text = 'πŸ“Š Quality Metrics\n\n'; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; continue; } text += `πŸ“¦ ${result.name}\n`; text += `- Overall Score: ${result.score}\n`; text += '- Note: Detailed metrics (tests, coverage, linting, types) are no longer provided by the API\n\n'; } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching quality metrics: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmMaintenance(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(pkg)}`); if (!response.ok) { return { name: pkg, error: `Failed to fetch maintenance data: ${response.statusText}` }; } const rawData = await response.json(); if (!isValidNpmsResponse(rawData)) { return { name: pkg, error: 'Invalid response format from npms.io API' }; } const maintenance = rawData.score.detail.maintenance; return { name: pkg, score: Math.round(maintenance * 100) / 100, }; })); let text = 'πŸ› οΈ Maintenance Metrics\n\n'; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; continue; } text += `πŸ“¦ ${result.name}\n`; text += `- Maintenance Score: ${result.score}\n\n`; } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching maintenance metrics: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmPopularity(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(pkg)}`); if (!response.ok) { return { name: pkg, error: `Failed to fetch popularity data: ${response.statusText}` }; } const data = await response.json(); if (!isValidNpmsResponse(data)) { return { name: pkg, error: 'Invalid API response format' }; } const popularityScore = data.score.detail.popularity; return { name: pkg, ...NpmPopularitySchema.parse({ score: Math.round(popularityScore * 100) / 100, stars: 0, downloads: 0, dependents: 0, communityInterest: 0, }), }; })); let text = 'πŸ“ˆ Popularity Metrics\n\n'; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; continue; } text += `πŸ“¦ ${result.name}\n`; text += `- Overall Score: ${result.score}\n`; text += '- Note: Detailed metrics are no longer provided by the API\n\n'; } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching popularity metrics: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmMaintainers(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`); if (response.status === 404) { return { name: pkg, error: 'Package not found in the npm registry', }; } if (!response.ok) { throw new Error(`API request failed with status ${response.status} (${response.statusText})`); } const data = await response.json(); if (!isNpmPackageInfo(data)) { throw new Error('Invalid package info data received'); } return { name: pkg, maintainers: data.maintainers || [], }; })); let text = 'πŸ‘₯ Package Maintainers\n\n'; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; continue; } text += `πŸ“¦ ${result.name}\n`; text += `${'-'.repeat(40)}\n`; const maintainers = result.maintainers || []; if (maintainers.length === 0) { text += '⚠️ No maintainers found.\n'; } else { text += `πŸ‘₯ Maintainers (${maintainers.length}):\n\n`; for (const maintainer of maintainers) { text += `β€’ ${maintainer.name}\n`; text += ` πŸ“§ ${maintainer.email}\n\n`; } } if (results.indexOf(result) < results.length - 1) { text += '\n'; } } return { content: [ { type: 'text', text, }, ], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching package maintainers: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmScore(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://api.npms.io/v2/package/${encodeURIComponent(pkg)}`); if (response.status === 404) { return { name: pkg, error: 'Package not found in the npm registry', }; } if (!response.ok) { throw new Error(`API request failed with status ${response.status} (${response.statusText})`); } const rawData = await response.json(); if (!isValidNpmsResponse(rawData)) { return { name: pkg, error: 'Invalid or incomplete response from npms.io API', }; } const { score, collected } = rawData; const { detail } = score; return { name: pkg, score, detail, collected, }; })); let text = 'πŸ“Š Package Scores\n\n'; for (const result of results) { if ('error' in result) { text += `❌ ${result.name}: ${result.error}\n\n`; continue; } text += `πŸ“¦ ${result.name}\n`; text += `${'-'.repeat(40)}\n`; text += `Overall Score: ${(result.score.final * 100).toFixed(1)}%\n\n`; text += '🎯 Quality Breakdown:\n'; text += `β€’ Quality: ${(result.detail.quality * 100).toFixed(1)}%\n`; text += `β€’ Maintenance: ${(result.detail.maintenance * 100).toFixed(1)}%\n`; text += `β€’ Popularity: ${(result.detail.popularity * 100).toFixed(1)}%\n\n`; if (result.collected.github) { text += 'πŸ“ˆ GitHub Stats:\n'; text += `β€’ Stars: ${result.collected.github.starsCount.toLocaleString()}\n`; text += `β€’ Forks: ${result.collected.github.forksCount.toLocaleString()}\n`; text += `β€’ Watchers: ${result.collected.github.subscribersCount.toLocaleString()}\n`; text += `β€’ Total Issues: ${result.collected.github.issues.count.toLocaleString()}\n`; text += `β€’ Open Issues: ${result.collected.github.issues.openCount.toLocaleString()}\n\n`; } if (result.collected.npm?.downloads?.length > 0) { const lastDownloads = result.collected.npm.downloads[0]; text += 'πŸ“₯ NPM Downloads:\n'; text += `β€’ Last day: ${lastDownloads.count.toLocaleString()} (${new Date(lastDownloads.from).toLocaleDateString()} - ${new Date(lastDownloads.to).toLocaleDateString()})\n\n`; } if (results.indexOf(result) < results.length - 1) { text += '\n'; } } // Retornar en el formato MCP estΓ‘ndar return { content: [ { type: 'text', text, }, ], isError: false, }; } catch (error) { // Manejo de errores en formato MCP estΓ‘ndar return { content: [ { type: 'text', text: `Error fetching package scores: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmPackageReadme(args) { try { const results = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://registry.npmjs.org/${pkg}`); if (!response.ok) { throw new Error(`Failed to fetch package info: ${response.statusText}`); } const rawData = await response.json(); if (!isNpmPackageInfo(rawData)) { throw new Error('Invalid package info data received'); } const latestVersion = rawData['dist-tags']?.latest; if (!latestVersion || !rawData.versions?.[latestVersion]) { throw new Error('No latest version found'); } const readme = rawData.versions[latestVersion].readme || rawData.readme; if (!readme) { return { name: pkg, version: latestVersion, text: 'No README found' }; } return { name: pkg, version: latestVersion, text: readme }; })); let text = ''; for (const result of results) { text += `${'='.repeat(80)}\n`; text += `πŸ“– ${result.name}@${result.version}\n`; text += `${'='.repeat(80)}\n\n`; text += result.text; if (results.indexOf(result) < results.length - 1) { text += '\n\n'; text += `${'='.repeat(80)}\n\n`; } } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching READMEs: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async function handleNpmSearch(args) { try { const limit = args.limit || 10; const response = await fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(args.query)}&size=${limit}`); if (!response.ok) { throw new Error(`Failed to search packages: ${response.statusText}`); } const rawData = await response.json(); const parseResult = NpmSearchResultSchema.safeParse(rawData); if (!parseResult.success) { throw new Error('Invalid search results data received'); } const { objects, total } = parseResult.data; let text = `πŸ” Search results for "${args.query}"\n`; text += `Found ${total.toLocaleString()} packages (showing top ${limit})\n\n`; for (const result of objects) { const pkg = result.package; const score = result.score; text += `πŸ“¦ ${pkg.name}@${pkg.version}\n`; if (pkg.description) text += `${pkg.description}\n`; // Normalize and format score to ensure it's between 0 and 1 const normalizedScore = Math.min(1, score.final / 100); const finalScore = normalizedScore.toFixed(2); text += `Score: ${finalScore} (${(normalizedScore * 100).toFixed(0)}%)\n`; if (pkg.keywords && pkg.keywords.length > 0) { text += `Keywords: ${pkg.keywords.join(', ')}\n`; } if (pkg.links) { text += 'Links:\n'; if (pkg.links.npm) text += `β€’ NPM: ${pkg.links.npm}\n`; if (pkg.links.homepage) text += `β€’ Homepage: ${pkg.links.homepage}\n`; if (pkg.links.repository) text += `β€’ Repository: ${pkg.links.repository}\n`; } text += '\n'; } return { content: [{ type: 'text', text }], isError: false, }; } catch (error) { return { content: [ { type: 'text', text: `Error searching packages: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } // License compatibility checker async function handleNpmLicenseCompatibility(args) { try { const licenses = await Promise.all(args.packages.map(async (pkg) => { const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`); if (!response.ok) { throw new Error(`Failed to fetch license info for ${pkg}: ${response.statusText}`); } const data = (await response.json()); return { package: pkg, license: data.license || 'UNKNOWN', }; })); let text = 'πŸ“œ License Compatibility Analysis\n\n'; text += 'Packages analyzed:\n'; for (const { package: pkg, license } of licenses) { text += `β€’ ${pkg}: ${license}\n`; } text += '\n'; // Basic license compatibility check const hasGPL = licenses.some(({ license }) => license?.includes('GPL')); const hasMIT = licenses.some(({ license }) => license === 'MIT'); const hasApache = licenses.some(({ license }) => license?.includes('Apache')); const hasUnknown = licenses.some(({ license }) => license === 'UNKNOWN'); text += 'Compatibility Analysis:\n'; if (hasUnknown) { text += '⚠️ Warning: Some packages have unknown licenses. Manual review