Analyze multiple dependency changes in parallel
analyze_packages_bulkAnalyzes multiple package upgrades simultaneously to generate a risk-ranked report identifying security fixes, breaking changes, and recommendations from caution to safe.
Instructions
Analyzes a list of package upgrades in parallel and returns a unified risk report with packages ranked by recommendation level (security > caution > review > likely-safe > safe). Use when the user provides many dependency changes from a Dependabot PR, npm outdated output, lockfile diff, or batch upgrade. Returns: total count, breakdown by semver class, total security fixes found, packages with breaking changes, and per-package details. Limit 50 packages per call (chunk larger lists).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| changes | Yes | List of package changes to analyze |
Implementation Reference
- src/index.ts:98-146 (handler)The handler function for the analyze_packages_bulk tool. It iterates over an array of package changes, calls analyzePackageChange (from analyzer.ts) for each in parallel (concurrency-limited via p-limit), sorts results by recommendation level, computes summary statistics (total, semver breakdown, security fixes count, breaking changes count), and returns a JSON report.
async ({ changes }) => { try { const results = await Promise.allSettled( changes.map((c) => limit(() => analyzePackageChange(c.ecosystem as Ecosystem, c.name, c.fromVersion, c.toVersion, githubToken) ) ) ); const analyzed = results.map((r, i) => r.status === "fulfilled" ? r.value : { package: changes[i]?.name ?? "unknown", error: (r.reason as Error).message, recommendationLevel: "review" as const, } ); const ranks: Record<string, number> = { security: 0, caution: 1, review: 2, "likely-safe": 3, safe: 4 }; const sorted = [...analyzed].sort( (a: any, b: any) => (ranks[a.recommendationLevel] ?? 5) - (ranks[b.recommendationLevel] ?? 5) ); const summary = { totalPackages: changes.length, bySemverClass: { major: analyzed.filter((a: any) => a.semverClass === "major").length, minor: analyzed.filter((a: any) => a.semverClass === "minor").length, patch: analyzed.filter((a: any) => a.semverClass === "patch").length, }, securityFixesTotal: analyzed.reduce( (n, a: any) => n + (a.securityFixes?.length ?? 0), 0 ), packagesWithBreakingChanges: analyzed.filter( (a: any) => (a.breakingChanges?.length ?? 0) > 0 ).length, packages: sorted, }; return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; } catch (err: any) { return { content: [{ type: "text", text: `Bulk analysis failed: ${err?.message ?? String(err)}` }], isError: true, }; } } - src/index.ts:83-96 (schema)The input schema for analyze_packages_bulk — a Zod schema defining an array of 'changes' objects (ecosystem, name, fromVersion, toVersion), limited to 1-50 items.
inputSchema: { changes: z .array( z.object({ ecosystem: ecosystemSchema, name: z.string().min(1), fromVersion: z.string().min(1), toVersion: z.string().min(1), }) ) .min(1) .max(50) .describe("List of package changes to analyze"), }, - src/index.ts:65-147 (registration)The registration of the tool using server.registerTool('analyze_packages_bulk', ...) in the createMcpServer function.
server.registerTool( "analyze_packages_bulk", { title: "Analyze multiple dependency changes in parallel", description: "Analyzes a list of package upgrades in parallel and returns a unified risk report with " + "packages ranked by recommendation level (security > caution > review > likely-safe > safe). " + "Use when the user provides many dependency changes from a Dependabot PR, npm outdated output, " + "lockfile diff, or batch upgrade. Returns: total count, breakdown by semver class, total " + "security fixes found, packages with breaking changes, and per-package details. " + "Limit 50 packages per call (chunk larger lists).", annotations: { title: "Analyze multiple dependency changes in parallel", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, inputSchema: { changes: z .array( z.object({ ecosystem: ecosystemSchema, name: z.string().min(1), fromVersion: z.string().min(1), toVersion: z.string().min(1), }) ) .min(1) .max(50) .describe("List of package changes to analyze"), }, }, async ({ changes }) => { try { const results = await Promise.allSettled( changes.map((c) => limit(() => analyzePackageChange(c.ecosystem as Ecosystem, c.name, c.fromVersion, c.toVersion, githubToken) ) ) ); const analyzed = results.map((r, i) => r.status === "fulfilled" ? r.value : { package: changes[i]?.name ?? "unknown", error: (r.reason as Error).message, recommendationLevel: "review" as const, } ); const ranks: Record<string, number> = { security: 0, caution: 1, review: 2, "likely-safe": 3, safe: 4 }; const sorted = [...analyzed].sort( (a: any, b: any) => (ranks[a.recommendationLevel] ?? 5) - (ranks[b.recommendationLevel] ?? 5) ); const summary = { totalPackages: changes.length, bySemverClass: { major: analyzed.filter((a: any) => a.semverClass === "major").length, minor: analyzed.filter((a: any) => a.semverClass === "minor").length, patch: analyzed.filter((a: any) => a.semverClass === "patch").length, }, securityFixesTotal: analyzed.reduce( (n, a: any) => n + (a.securityFixes?.length ?? 0), 0 ), packagesWithBreakingChanges: analyzed.filter( (a: any) => (a.breakingChanges?.length ?? 0) > 0 ).length, packages: sorted, }; return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; } catch (err: any) { return { content: [{ type: "text", text: `Bulk analysis failed: ${err?.message ?? String(err)}` }], isError: true, }; } } ); - src/worker.ts:56-89 (registration)The server card registration (declarative metadata) for analyze_packages_bulk, used in the .well-known MCP server card.
{ name: "analyze_packages_bulk", description: "Analyzes a list of package upgrades in parallel and returns a unified risk report with packages ranked by recommendation level (security > caution > review > likely-safe > safe). Use when the user provides many dependency changes from a Dependabot PR, npm outdated output, lockfile diff, or batch upgrade. Returns: total count, breakdown by semver class, total security fixes found, packages with breaking changes, and per-package details. Limit 50 packages per call.", annotations: { title: "Analyze multiple dependency changes in parallel", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, inputSchema: { type: "object", properties: { changes: { type: "array", minItems: 1, maxItems: 50, description: "List of package changes to analyze", items: { type: "object", properties: { ecosystem: { type: "string", enum: ["npm", "pypi"] }, name: { type: "string", minLength: 1 }, fromVersion: { type: "string", minLength: 1 }, toVersion: { type: "string", minLength: 1 }, }, required: ["ecosystem", "name", "fromVersion", "toVersion"], }, }, }, required: ["changes"], }, }, - src/analyzer.ts:317-369 (helper)The analyzePackageChange helper function which is called by the bulk handler for each individual package change. It fetches package metadata, classifies semver bumps, checks CVEs via OSV.dev, extracts breaking changes/release notes, and generates recommendations.
export async function analyzePackageChange( ecosystem: Ecosystem, name: string, fromVersion: string, toVersion: string, githubToken?: string ): Promise<PackageAnalysis> { const semverClass = classifyBump(fromVersion, toVersion); const meta = ecosystem === "npm" ? await fetchNpmMeta(name) : await fetchPyPIMeta(name); const repo = extractGitHubRepo(meta, ecosystem); const [releases, cvesAtFrom, cvesAtTo] = await Promise.all([ repo ? fetchReleasesBetween(repo.owner, repo.repo, fromVersion, toVersion, githubToken).catch(() => []) : Promise.resolve([]), fetchCvesAtVersion(ecosystem, name, fromVersion).catch(() => []), fetchCvesAtVersion(ecosystem, name, toVersion).catch(() => []), ]); const toIds = new Set(cvesAtTo.map((c: any) => c.id)); const fixedCves = cvesAtFrom.filter((c: any) => !toIds.has(c.id)); const breakingChanges = extractBreakingChanges(releases); const migrationLinks = extractMigrationLinks(releases); const rec = generateRecommendation(semverClass, breakingChanges, fixedCves); const needsFallback = breakingChanges.length === 0 && (semverClass === "major" || semverClass === "minor"); const releaseExcerpts = needsFallback ? extractReleaseExcerpts(releases) : undefined; const result: PackageAnalysis = { package: name, ecosystem, fromVersion, toVersion, semverClass, repoUrl: repo ? `https://github.com/${repo.owner}/${repo.repo}` : null, releaseCount: releases.length, breakingChanges, securityFixes: fixedCves.map((c: any) => ({ id: c.id, summary: c.summary ?? c.details?.slice(0, 200) ?? "", severity: c.database_specific?.severity ?? "unknown", })), migrationLinks, recommendation: rec.text, recommendationLevel: rec.level, }; if (releaseExcerpts && releaseExcerpts.length > 0) { result.releaseExcerpts = releaseExcerpts; } return result; }