UNPKG

npmplus-mcp-server

Version:

Production-ready MCP server for intelligent JavaScript package management. Works with Claude, Windsurf, Cursor, VS Code, and any MCP-compatible AI editor.

419 lines (366 loc) 12.5 kB
import { z } from "zod"; import { execa } from "execa"; import { readFile, readdir } from "fs/promises"; import path, { join } from "path"; import { detectPackageManager } from "../pm-detect.js"; import { httpClient } from "../http-client.js"; import { resolveProjectCwd, resolveCwd } from "../utils/path-resolver.js"; import { CacheService } from '../services/CacheService.js'; import { PackageService } from '../services/PackageService.js'; import { PackageManagerService } from '../services/PackageManagerService.js'; import { URLS, VERSION } from '../constants.js'; // Add VERSION to imports const ListLicensesSchema = z.object({ cwd: z.string().default(process.cwd()).describe("Working directory"), production: z.boolean().default(false).describe("Only check production dependencies"), summary: z.boolean().default(true).describe("Show summary of license types") }); const CheckLicenseSchema = z.object({ packageName: z.string().describe("Package name to check"), version: z.string().optional().describe("Specific version to check") }); const CleanCacheSchema = z.object({ cwd: z.string().default(process.cwd()).describe("Working directory"), global: z.boolean().default(false).describe("Clean global cache") }); const PackageInfoSchema = z.object({ packageName: z.string().describe("Package name"), version: z.string().optional().describe("Specific version") }); // Export tools and handlers export const tools = [ { name: "list_licenses", description: "List licenses of all dependencies in a project", inputSchema: ListLicensesSchema }, { name: "check_license", description: "Check the license of a specific package", inputSchema: CheckLicenseSchema }, { name: "clean_cache", description: "Clean the package manager cache", inputSchema: CleanCacheSchema }, { name: "package_info", description: "Get detailed information about a package", inputSchema: PackageInfoSchema }, // ADD THIS NEW TOOL { name: "debug_version", description: "Get debug information about the running MCP server version", inputSchema: z.object({}) // Empty schema since no inputs needed } ]; export const handlers = new Map([ ["list_licenses", handleListLicenses], ["check_license", handleCheckLicense], ["clean_cache", handleCleanCache], ["package_info", handlePackageInfo], ["debug_version", handleDebugVersion] // ADD THIS HANDLER MAPPING ]); async function handleListLicenses(args: unknown) { const input = ListLicensesSchema.parse(args); try { const resolvedCwd = resolveProjectCwd(input.cwd); // Read package.json and lock file to get all dependencies const packageJson = JSON.parse( await readFile(join(resolvedCwd, "package.json"), "utf-8") ); const dependencies = { ...(input.production ? {} : packageJson.devDependencies || {}), ...(packageJson.dependencies || {}) }; const licenses: Record<string, string[]> = {}; const packageLicenses: Array<{ name: string; version: string; license: string }> = []; // Get license info for each dependency for (const [name, version] of Object.entries(dependencies)) { try { // Try to read from node_modules first const packagePath = join(resolvedCwd, "node_modules", name, "package.json"); const pkgData = JSON.parse(await readFile(packagePath, "utf-8")); const license = pkgData.license || pkgData.licenses || "Unknown"; const licenseStr = typeof license === "object" ? (Array.isArray(license) ? license.map(l => l.type || l).join(", ") : license.type || "Unknown") : license; packageLicenses.push({ name, version: pkgData.version, license: licenseStr }); // Group by license type if (!licenses[licenseStr]) { licenses[licenseStr] = []; } licenses[licenseStr].push(`${name}@${pkgData.version}`); } catch { // Package not installed locally, skip } } const output: string[] = ["📜 License Report:\n"]; if (input.summary) { output.push("License Summary:"); for (const [license, packages] of Object.entries(licenses)) { output.push(`\n${license}: ${packages.length} packages`); if (packages.length <= 5) { packages.forEach(pkg => output.push(` - ${pkg}`)); } else { packages.slice(0, 3).forEach(pkg => output.push(` - ${pkg}`)); output.push(` ... and ${packages.length - 3} more`); } } } else { output.push("All Packages:"); packageLicenses.sort((a, b) => a.name.localeCompare(b.name)); packageLicenses.forEach(({ name, version, license }) => { output.push(`${name}@${version}: ${license}`); }); } return { content: [ { type: "text", text: output.join("\n") } ] }; } catch (error: any) { return { content: [ { type: "text", text: `❌ Failed to list licenses: ${error.message}` } ], isError: true }; } } async function handleCheckLicense(args: unknown) { const input = CheckLicenseSchema.parse(args); try { const packageInfo = await httpClient.npmRegistry<any>( `/${input.packageName}${input.version ? `/${input.version}` : ""}` ); const version = input.version || packageInfo["dist-tags"]?.latest; const versionData = packageInfo.versions?.[version] || packageInfo; const output: string[] = [ `📜 License Information for ${input.packageName}@${version}:\n` ]; // License info const license = versionData.license || versionData.licenses; if (license) { const licenseStr = typeof license === "object" ? (Array.isArray(license) ? license.map(l => l.type || l).join(", ") : license.type || "Unknown") : license; output.push(`License: ${licenseStr}`); } else { output.push("License: Not specified"); } // Check for license file in repository if (versionData.repository?.url) { output.push(`\nRepository: ${versionData.repository.url}`); } // Author info if (versionData.author) { const author = typeof versionData.author === "object" ? `${versionData.author.name}${versionData.author.email ? ` <${versionData.author.email}>` : ""}` : versionData.author; output.push(`Author: ${author}`); } // Maintainers if (versionData.maintainers?.length) { output.push("\nMaintainers:"); versionData.maintainers.forEach((m: any) => { output.push(` - ${m.name}${m.email ? ` <${m.email}>` : ""}`); }); } return { content: [ { type: "text", text: output.join("\n") } ] }; } catch (error: any) { return { content: [ { type: "text", text: `❌ Failed to check license: ${error.message}` } ], isError: true }; } } async function handleCleanCache(args: unknown) { const input = CleanCacheSchema.parse(args); try { const resolvedCwd = resolveCwd(input.cwd); const { packageManager } = await detectPackageManager(resolvedCwd); let command: string[]; switch (packageManager) { case "npm": command = ["npm", "cache", "clean", "--force"]; break; case "yarn": command = ["yarn", "cache", "clean"]; break; case "pnpm": command = ["pnpm", "store", "prune"]; break; } if (input.global && packageManager === "npm") { command.push("-g"); } const { stdout, stderr } = await execa(command[0], command.slice(1), { cwd: resolvedCwd }); return { content: [ { type: "text", text: `✅ Cache cleaned successfully using ${packageManager}:\n\n${stdout || stderr || "Cache cleaned"}` } ] }; } catch (error: any) { return { content: [ { type: "text", text: `❌ Failed to clean cache: ${error.message}` } ], isError: true }; } } async function handlePackageInfo(args: unknown) { const input = PackageInfoSchema.parse(args); try { const packageInfo = await httpClient.npmRegistry<any>( `/${input.packageName}${input.version ? `/${input.version}` : ""}` ); const version = input.version || packageInfo["dist-tags"]?.latest; const versionData = packageInfo.versions?.[version] || packageInfo; const output: string[] = [ `📦 Package Information for ${input.packageName}@${version}:\n` ]; // Basic info output.push(`Name: ${versionData.name}`); output.push(`Version: ${versionData.version}`); output.push(`Description: ${versionData.description || "No description"}`); // License const license = versionData.license || "Not specified"; output.push(`License: ${typeof license === "object" ? license.type || "Unknown" : license}`); // Author if (versionData.author) { const author = typeof versionData.author === "object" ? versionData.author.name : versionData.author; output.push(`Author: ${author}`); } // Keywords if (versionData.keywords?.length) { output.push(`Keywords: ${versionData.keywords.join(", ")}`); } // Dependencies count const deps = Object.keys(versionData.dependencies || {}).length; const devDeps = Object.keys(versionData.devDependencies || {}).length; output.push(`\nDependencies: ${deps}`); output.push(`Dev Dependencies: ${devDeps}`); // Dist tags if (packageInfo["dist-tags"]) { output.push("\nDist Tags:"); for (const [tag, ver] of Object.entries(packageInfo["dist-tags"])) { output.push(` ${tag}: ${ver}`); } } // Links output.push("\nLinks:"); if (versionData.homepage) { output.push(` Homepage: ${versionData.homepage}`); } if (versionData.repository?.url) { output.push(` Repository: ${versionData.repository.url}`); } if (versionData.bugs?.url) { output.push(` Issues: ${versionData.bugs.url}`); } output.push(` NPM: ${URLS.NPM_WEBSITE}/package/${input.packageName}`); // Time info if (packageInfo.time?.[version]) { output.push(`\nPublished: ${new Date(packageInfo.time[version]).toLocaleDateString()}`); } return { content: [ { type: "text", text: output.join("\n") } ] }; } catch (error: any) { return { content: [ { type: "text", text: `❌ Failed to get package info: ${error.message}` } ], isError: true }; } } // ADD THIS NEW HANDLER FUNCTION async function handleDebugVersion(args: unknown) { const debugInfo = { version: VERSION, serverTime: new Date().toISOString(), processUptime: process.uptime(), currentWorkingDirectory: process.cwd(), nodeVersion: process.version, platform: process.platform, pid: process.pid, env: { NODE_ENV: process.env.NODE_ENV || 'production', npm_package_version: process.env.npm_package_version, USER: process.env.USER, HOME: process.env.HOME } }; // Log to console for debugging console.error('[npmplus-mcp] Debug version called:', JSON.stringify(debugInfo, null, 2)); return { content: [ { type: "text", text: `🔍 NPMPlus MCP Debug Information: Version: ${debugInfo.version} Server Time: ${debugInfo.serverTime} Process Uptime: ${Math.floor(debugInfo.processUptime)} seconds Current Directory: ${debugInfo.currentWorkingDirectory} Node Version: ${debugInfo.nodeVersion} Platform: ${debugInfo.platform} Process ID: ${debugInfo.pid} Environment: NODE_ENV: ${debugInfo.env.NODE_ENV} npm_package_version: ${debugInfo.env.npm_package_version || 'not set'} User: ${debugInfo.env.USER} Home: ${debugInfo.env.HOME} Note: If this shows a different version than expected, the MCP may be cached. Try restarting Claude Desktop to pick up the latest version. Debug Log Instructions: - Check console output where Claude was started - Or check system logs for npmplus-mcp entries - Look for messages starting with [npmplus-mcp]` } ] }; }