zed.mcp
Version:
MCP server for project analysis, AI rules reading, and dependency checking
337 lines (292 loc) • 10.1 kB
text/typescript
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,
};
}
};