UNPKG

zed.mcp

Version:

MCP server for project analysis, AI rules reading, and dependency checking

337 lines (292 loc) 10.1 kB
import { z } from "zod"; import path from "path"; import type { ToolDefinition, ToolHandler, ToolResponse, DependencyInfo, DependencyCheckResult, PackageJson, } from "../types/index"; import { fileExists, readJsonFile, getLatestVersions, isVersionOutdated, cleanVersionString, } from "../utils/index"; /** * Input schema for check dependencies tool */ const CheckDependenciesInput = z.object({ projectPath: z .string() .describe("The absolute path to the project root directory."), }); type CheckDependenciesParams = z.infer<typeof CheckDependenciesInput>; /** * Tool definition for checking dependencies */ export const checkDependenciesDefinition: ToolDefinition = { name: "check-dependencies", title: "Check Dependencies", description: "Analyzes package.json dependencies and checks for available updates from npm registry. Returns current vs latest versions for all dependencies, devDependencies, peerDependencies, and workspaces catalogs.", inputSchema: { projectPath: CheckDependenciesInput.shape.projectPath, }, }; /** * Processes dependencies of a specific type */ async function processDependencies( dependencies: Record<string, string> | undefined, type: "dependencies" | "devDependencies" | "peerDependencies" | "catalog" | "catalogs", latestVersions: Map<string, string | null>, catalogName?: string, ): Promise<DependencyInfo[]> { if (!dependencies) { return []; } const results: DependencyInfo[] = []; for (const [name, currentVersion] of Object.entries(dependencies)) { const latestVersion = latestVersions.get(name); if (latestVersion) { const isOutdated = isVersionOutdated(currentVersion, latestVersion); results.push({ name, currentVersion, latestVersion, isOutdated, type, catalogName, }); } else { // If we couldn't fetch the latest version, still include it results.push({ name, currentVersion, latestVersion: "unknown", isOutdated: false, type, catalogName, }); } } return results; } /** * Formats the dependency check results into a readable string */ function formatResults(result: DependencyCheckResult): string { const { projectName, totalDependencies, outdatedCount, dependencies } = result; let output = `# Dependency Check: ${projectName}\n\n`; output += `Total dependencies: ${totalDependencies}\n`; output += `Outdated: ${outdatedCount}\n\n`; if (outdatedCount === 0) { output += "✓ All dependencies are up to date!\n"; return output; } // Group by type const byType = { dependencies: dependencies.filter((d) => d.type === "dependencies"), devDependencies: dependencies.filter((d) => d.type === "devDependencies"), peerDependencies: dependencies.filter((d) => d.type === "peerDependencies"), catalog: dependencies.filter((d) => d.type === "catalog"), catalogs: dependencies.filter((d) => d.type === "catalogs"), }; // Show outdated dependencies const outdated = dependencies.filter((d) => d.isOutdated); output += "## Outdated Dependencies\n\n"; for (const dep of outdated) { let icon = "📦"; if (dep.type === "devDependencies") icon = "🔧"; else if (dep.type === "peerDependencies") icon = "🔗"; else if (dep.type === "catalog") icon = "📚"; else if (dep.type === "catalogs") icon = "📖"; const typeLabel = dep.type === "catalogs" && dep.catalogName ? `${dep.type} > ${dep.catalogName}` : dep.type; output += `${icon} **${dep.name}** (${typeLabel})\n`; output += ` Current: ${dep.currentVersion}\n`; output += ` Latest: ${dep.latestVersion}\n`; output += ` Update: "${dep.name}": "${dep.latestVersion}"\n\n`; } // Show summary by type output += "## Summary by Type\n\n"; for (const [typeName, deps] of Object.entries(byType)) { if (deps.length === 0) continue; const outdatedInType = deps.filter((d) => d.isOutdated).length; // For catalogs, group by catalog name if (typeName === "catalogs") { const catalogGroups = new Map<string, typeof deps>(); for (const dep of deps) { const catName = dep.catalogName || "unknown"; if (!catalogGroups.has(catName)) { catalogGroups.set(catName, []); } catalogGroups.get(catName)!.push(dep); } output += `### ${typeName}\n`; for (const [catName, catDeps] of catalogGroups) { const outdatedInCat = catDeps.filter((d) => d.isOutdated).length; output += `#### ${catName}\n`; output += `Total: ${catDeps.length} | Outdated: ${outdatedInCat}\n\n`; if (outdatedInCat > 0) { const outdatedDeps = catDeps.filter((d) => d.isOutdated); for (const dep of outdatedDeps) { output += `- ${dep.name}: ${dep.currentVersion}${dep.latestVersion}\n`; } output += "\n"; } } } else { output += `### ${typeName}\n`; output += `Total: ${deps.length} | Outdated: ${outdatedInType}\n\n`; if (outdatedInType > 0) { const outdatedDeps = deps.filter((d) => d.isOutdated); for (const dep of outdatedDeps) { output += `- ${dep.name}: ${dep.currentVersion}${dep.latestVersion}\n`; } output += "\n"; } } } // Provide update instructions output += "## How to Update\n\n"; output += "You can update dependencies in package.json by:\n"; output += "1. Manually updating the version strings in package.json\n"; output += "2. Running: `bun update` (updates all)\n"; output += "3. Running: `bun update <package-name>` (updates specific package)\n"; return output; } /** * Handler for checking dependencies */ export const checkDependenciesHandler: ToolHandler<CheckDependenciesParams> = async ({ projectPath, }): Promise<ToolResponse> => { try { const packageJsonPath = path.join(projectPath, "package.json"); // Check if package.json exists if (!(await fileExists(packageJsonPath))) { return { content: [ { type: "text", text: "Error: No package.json found in the specified project path. This command only works for JavaScript/TypeScript projects.", }, ], isError: true, }; } // Read package.json const packageJson = await readJsonFile<PackageJson>(packageJsonPath); // Collect all package names const allPackageNames = new Set<string>(); if (packageJson.dependencies) { Object.keys(packageJson.dependencies).forEach((name) => allPackageNames.add(name), ); } if (packageJson.devDependencies) { Object.keys(packageJson.devDependencies).forEach((name) => allPackageNames.add(name), ); } if (packageJson.peerDependencies) { Object.keys(packageJson.peerDependencies).forEach((name) => allPackageNames.add(name), ); } // Collect catalog dependencies if (packageJson.workspaces?.catalog) { Object.keys(packageJson.workspaces.catalog).forEach((name) => allPackageNames.add(name), ); } // Collect catalogs dependencies if (packageJson.workspaces?.catalogs) { for (const catalogDeps of Object.values(packageJson.workspaces.catalogs)) { Object.keys(catalogDeps).forEach((name) => allPackageNames.add(name), ); } } if (allPackageNames.size === 0) { return { content: [ { type: "text", text: "No dependencies found in package.json.", }, ], }; } // Fetch latest versions from npm registry const latestVersions = await getLatestVersions(Array.from(allPackageNames)); // Process each dependency type const allDependencies: DependencyInfo[] = []; const deps = await processDependencies( packageJson.dependencies, "dependencies", latestVersions, ); allDependencies.push(...deps); const devDeps = await processDependencies( packageJson.devDependencies, "devDependencies", latestVersions, ); allDependencies.push(...devDeps); const peerDeps = await processDependencies( packageJson.peerDependencies, "peerDependencies", latestVersions, ); allDependencies.push(...peerDeps); // Process catalog dependencies if (packageJson.workspaces?.catalog) { const catalogDeps = await processDependencies( packageJson.workspaces.catalog, "catalog", latestVersions, ); allDependencies.push(...catalogDeps); } // Process catalogs dependencies if (packageJson.workspaces?.catalogs) { for (const [catalogName, catalogDeps] of Object.entries(packageJson.workspaces.catalogs)) { const deps = await processDependencies( catalogDeps, "catalogs", latestVersions, catalogName, ); allDependencies.push(...deps); } } // Create result const result: DependencyCheckResult = { projectName: packageJson.name || "Unknown Project", totalDependencies: allDependencies.length, outdatedCount: allDependencies.filter((d) => d.isOutdated).length, dependencies: allDependencies, }; // Format and return const formattedOutput = formatResults(result); return { content: [{ type: "text", text: formattedOutput }], }; } catch (error: any) { return { content: [ { type: "text", text: `An unexpected error occurred while checking dependencies: ${error.message}`, }, ], isError: true, }; } };