Bundle Budget Check
bundle_budget_checkAnalyzes build output file sizes and flags files that exceed a global or per-entry KB budget for CI integration.
Instructions
Walk a build directory, measure raw + gzip (+ optional brotli) sizes per file, and flag files that exceed a global or per-entry KB budget. CI-friendly JSON output.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| buildDir | Yes | Absolute path to the build output directory (e.g. dist, .next/static). | |
| budgetKb | No | Global size budget in KB. Flags any file above this (gzipped). Default: 250. | |
| perEntryBudgetKb | No | Per-file budgets in KB keyed by glob-ish substring match. Matching file is checked against the most specific (longest) matching key. | |
| ext | No | File extensions to include (default: ['.js','.mjs','.cjs','.css']). | |
| includeBrotli | No | Also compute brotli sizes (slower). |
Implementation Reference
- src/tools/bundle-budget.ts:62-126 (handler)The `registerBundleBudget` handler function that implements the bundle_budget_check tool logic: walks a build directory, computes raw/gzip/brotli sizes, checks against global and per-entry budgets, and returns a JSON report indicating pass/fail status.
export function registerBundleBudget(server: McpServer) { server.registerTool( "bundle_budget_check", { title: "Bundle Budget Check", description: "Walk a build directory, measure raw + gzip (+ optional brotli) sizes per file, and flag files that exceed a global or per-entry KB budget. CI-friendly JSON output.", inputSchema: InputShape, }, async (args) => { try { const exts = args.ext && args.ext.length ? args.ext : DEFAULT_EXT; const budgetKb = args.budgetKb ?? 250; const stat = await fs.stat(args.buildDir).catch(() => null); if (!stat || !stat.isDirectory()) { return errorResult(`buildDir not a directory: ${args.buildDir}`); } const files = await walk(args.buildDir, exts); const report = []; let overCount = 0; let totalRaw = 0; let totalGzip = 0; for (const f of files) { const buf = await fs.readFile(f); const gz = await gzip(buf); const br = args.includeBrotli ? await brotli(buf) : undefined; const rel = path.relative(args.buildDir, f); const match = matchBudget(rel, args.perEntryBudgetKb); const effectiveBudget = match?.budgetKb ?? budgetKb; const gzKb = gz.length / 1024; const over = gzKb > effectiveBudget; if (over) overCount++; totalRaw += buf.length; totalGzip += gz.length; report.push({ file: rel, rawBytes: buf.length, rawKb: +(buf.length / 1024).toFixed(2), gzipBytes: gz.length, gzipKb: +gzKb.toFixed(2), ...(br ? { brotliBytes: br.length, brotliKb: +(br.length / 1024).toFixed(2) } : {}), budgetKb: effectiveBudget, budgetKey: match?.key ?? "(global)", over, overBy: over ? +(gzKb - effectiveBudget).toFixed(2) : 0, }); } report.sort((a, b) => b.gzipBytes - a.gzipBytes); return jsonResult({ buildDir: args.buildDir, fileCount: files.length, overBudgetCount: overCount, totals: { rawKb: +(totalRaw / 1024).toFixed(2), gzipKb: +(totalGzip / 1024).toFixed(2), }, files: report, status: overCount === 0 ? "pass" : "fail", }); } catch (err) { return errorResult(err instanceof Error ? err.message : String(err)); } } ); } - src/tools/bundle-budget.ts:12-30 (schema)Input schema for the bundle_budget_check tool using Zod, defining buildDir (required), budgetKb, perEntryBudgetKb, ext, and includeBrotli parameters.
const InputShape = { buildDir: z.string().describe("Absolute path to the build output directory (e.g. dist, .next/static)."), budgetKb: z .number() .positive() .optional() .describe("Global size budget in KB. Flags any file above this (gzipped). Default: 250."), perEntryBudgetKb: z .record(z.number().positive()) .optional() .describe( "Per-file budgets in KB keyed by glob-ish substring match. Matching file is checked against the most specific (longest) matching key." ), ext: z .array(z.string()) .optional() .describe("File extensions to include (default: ['.js','.mjs','.cjs','.css'])."), includeBrotli: z.boolean().optional().describe("Also compute brotli sizes (slower)."), }; - src/index.ts:26-26 (registration)Registration call: `registerBundleBudget(server)` which is the entry point that wires the bundle_budget_check tool into the MCP server.
registerBundleBudget(server); - src/tools/bundle-budget.ts:34-46 (helper)Helper function `walk()` that recursively walks a directory and collects files matching given extensions.
async function walk(dir: string, exts: string[]): Promise<string[]> { const out: string[] = []; async function visit(d: string) { const entries = await fs.readdir(d, { withFileTypes: true }); for (const e of entries) { const full = path.join(d, e.name); if (e.isDirectory()) await visit(full); else if (exts.includes(path.extname(e.name))) out.push(full); } } await visit(dir); return out; } - src/tools/bundle-budget.ts:48-60 (helper)Helper function `matchBudget()` that finds the most specific per-entry budget key matching a file path via substring matching.
function matchBudget( relPath: string, perEntry: Record<string, number> | undefined ): { key: string; budgetKb: number } | null { if (!perEntry) return null; let best: { key: string; budgetKb: number } | null = null; for (const [key, budget] of Object.entries(perEntry)) { if (relPath.includes(key)) { if (!best || key.length > best.key.length) best = { key, budgetKb: budget }; } } return best; }