@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
JavaScript
#!/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