Skip to content

fix: Migrate MCPB build to esbuild with ESM compatibility fixes#360

Closed
wonderwhy-er wants to merge 1 commit intomainfrom
fix/mcpb-esbuild-bundle
Closed

fix: Migrate MCPB build to esbuild with ESM compatibility fixes#360
wonderwhy-er wants to merge 1 commit intomainfrom
fix/mcpb-esbuild-bundle

Conversation

@wonderwhy-er
Copy link
Copy Markdown
Owner

@wonderwhy-er wonderwhy-er commented Feb 25, 2026

User description

Summary

Replaces the tsc + copy dist/ MCPB build approach with esbuild single-file bundling for faster builds and smaller bundles. Includes post-processing fixes for three esbuild ESM compatibility issues discovered during testing.

Changes

scripts/build-mcpb.cjs

  • esbuild bundling: Replaces npm run build (tsc) with esbuild to produce a single dist/index.js file
  • External deps: Native modules (sharp, ripgrep) and CJS-only packages (pdf2md, pdf-lib) are kept external and installed separately
  • Post-processing step with three fixes:
    1. ESM shims: Injects createRequire, __dirname, __filename at bundle top so CJS require() calls work for Node.js built-ins
    2. Async propagation fix: Adds async to __esm wrapper callbacks that contain await but weren't marked async (esbuild bug — async not propagated to modules that transitively depend on async modules)
    3. isMainModule removal: Strips the auto-start guard from device.ts that incorrectly fires in the bundle (since import.meta.url === process.argv[1] in a single-file bundle), which was outputting debug messages to stdout and crashing Claude Desktop's MCP transport

src/utils/trackTools.ts

  • Replace top-level await fs.promises.mkdir() with synchronous fs.mkdirSync() to avoid cascading async propagation issues in esbuild's ESM bundling

Testing

  • Bundle builds successfully with npm run build:mcpb
  • MCP initialize handshake works correctly (clean JSON-RPC on stdout, no debug pollution)
  • Tested in Claude Desktop — extension connects without crashing

Related to #336


CodeAnt-AI Description

Bundle MCPB with esbuild and fix ESM compatibility to prevent startup crashes

What Changed

  • Build now produces a single ESM dist/index.js via esbuild instead of a tsc + copy flow; native and CJS-only dependencies are kept external and installed in the bundle.
  • Post-processing of the esbuild output injects Node ESM shims (createRequire, __dirname, __filename), fixes non-async module wrappers that contained await, and removes an auto-start guard in device.ts so the bundled extension does not emit debug output or start unintended processes.
  • Directory creation for tool logs is now synchronous to avoid top-level await propagation issues in the bundled ESM output; ripgrep binaries and a runtime wrapper are copied into the bundle for cross-platform support.
  • Manifest and bundle package.json are generated for the single-file bundle and production-only native/CJS deps are installed before packing .mcpb.

Impact

✅ Clearer MCPB startup (no stray debug output on stdout)
✅ Fewer MCPB crashes when installed in Claude Desktop
✅ Smaller, single-file bundle that builds faster and installs platform ripgrep correctly

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • Chores
    • Improved the build process with a new bundling workflow that enhances application packaging and delivery efficiency.
    • Enhanced error handling and logging with updated directory management for better reliability during the build process.

Replace tsc+copy build approach with esbuild single-file bundling for faster
builds and smaller bundles. Includes three post-processing fixes for esbuild
ESM compatibility issues:

1. ESM shims: Inject createRequire, __dirname, __filename so CJS modules
   bundled into ESM format can use require() and path globals

2. Async propagation: Fix esbuild bug where __esm wrapper callbacks that
   transitively depend on async modules don't get marked as async, causing
   'SyntaxError: Unexpected reserved word' for await in non-async functions

3. isMainModule guard: Strip device.ts auto-start block that incorrectly
   triggers in the single-file bundle (import.meta.url always matches
   process.argv[1]), which outputs debug messages to stdout and breaks
   Claude Desktop's MCP JSON-RPC transport

Also:
- Replace top-level await in trackTools.ts with sync mkdirSync
- Keep native modules (sharp, ripgrep) and CJS-only packages (pdf2md,
  pdf-lib) as external dependencies installed separately
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Feb 25, 2026

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

The pull request replaces the original TypeScript build process in the build script with a new esbuild-based bundling workflow, including post-processing, manifest validation, and dependency management. Additionally, a logging utility is updated to use synchronous directory creation instead of asynchronous operations.

Changes

Cohort / File(s) Summary
Build Bundling Workflow
scripts/build-mcpb.cjs
Replaced TS build flow with esbuild-based bundling, adds post-processing for ESM compatibility and async fixes, removes isMainModule auto-start block. New steps include manifest validation, dependency installation in bundle, cross-platform ripgrep binary setup, and MCPB CLI packing. Updated error handling and console logging for bundling operations.
Logging Directory Creation
src/utils/trackTools.ts
Replaced asynchronous mkdir with synchronous mkdirSync call with explanatory comment about esbuild ESM bundling constraints. Log rotation and append logic remain unchanged.

Possibly related PRs

  • #262: Modifies scripts/build-mcpb.cjs to add bundling steps and cross-platform ripgrep binary inclusion.
  • #270: Alters build-mcpb.cjs pack/validate workflow and MCPB CLI integration.
  • #314: Modifies bundle package.json generation in the build script.

Suggested labels

size:XL

Poem

🐰 A bundle so tight, esbuild shines bright,
Post-processing fixes ESM in flight,
Sync calls now dance where async once played,
The build pipeline blooms in this masquerade! ✨


Estimated code review effort: 🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: migrating the MCPB build system from tsc to esbuild with ESM compatibility fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/mcpb-esbuild-bundle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Feb 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
scripts/build-mcpb.cjs (5)

184-189: No guard if an expected dependency is missing from root package.json.

If any of these four packages are removed or renamed in the root package.json, the version will silently be undefined, producing invalid JSON that causes a confusing npm install failure later. Consider adding an early check:

Example guard
const requiredExternal = ['sharp', '@vscode/ripgrep', '@opendocsg/pdf2md', 'pdf-lib'];
for (const dep of requiredExternal) {
    if (!packageJson.dependencies?.[dep]) {
        console.error(`❌ Missing required dependency "${dep}" in root package.json`);
        process.exit(1);
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build-mcpb.cjs` around lines 184 - 189, Add an early validation that
packageJson.dependencies contains the four required keys ("sharp",
"@vscode/ripgrep", "@opendocsg/pdf2md", "pdf-lib") before assembling the
dependencies object so you never inject undefined versions into the generated
JSON; check packageJson.dependencies for each required dependency, log a clear
error including the missing package name (e.g. using console.error), and call
process.exit(1) if any are absent—place this guard near the top of the script
before the code that builds the dependencies object that references
packageJson.dependencies.

156-171: package.json is copied then immediately overwritten.

Line 157 includes package.json in filesToCopy, but Step 6 (lines 191-194) overwrites BUNDLE_DIR/package.json with a new object. The initial copy is wasted work and could mislead future maintainers into thinking the root package.json is used as-is.

Proposed fix
-const filesToCopy = ['package.json', 'README.md', 'LICENSE', 'PRIVACY.md', 'icon.png'];
+const filesToCopy = ['README.md', 'LICENSE', 'PRIVACY.md', 'icon.png'];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build-mcpb.cjs` around lines 156 - 171, The script currently copies
package.json via the filesToCopy array then later replaces
BUNDLE_DIR/package.json with a generated object, so remove that wasted step by
excluding "package.json" from filesToCopy (or add a conditional that skips
copying when a later write will overwrite it); update the filesToCopy
declaration or the copy loop around the filesToCopy symbol and ensure the later
package.json generation code that writes to BUNDLE_DIR (the write that creates
the final bundle package.json) remains unchanged.

113-122: Regex for isMainModule removal is fragile with nested braces.

The trailing [\s\S]*?\n\} will match the first \n} after the if (isMainModule) { opening. If esbuild ever emits nested braces inside that block (e.g., a try/catch or object literal), this will truncate the removal and leave broken JS in the bundle.

Consider adding a post-write syntax check (e.g., node --check mcpb-bundle/dist/index.js) after line 124 to catch this class of breakage at build time rather than at runtime.

Proposed addition after line 124
     fs.writeFileSync(bundlePath, bundleContent);
+    // Syntax-check the post-processed bundle to catch regex replacement errors early
+    try {
+        execSync(`node --check "${bundlePath}"`, { cwd: PROJECT_ROOT, stdio: 'pipe' });
+    } catch (syntaxError) {
+        console.error('❌ Post-processed bundle has syntax errors:', syntaxError.stderr?.toString() || syntaxError.message);
+        process.exit(1);
+    }
     console.log('✅ Post-processing completed');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build-mcpb.cjs` around lines 113 - 122, The current
isMainModulePattern replacement in build-mcpb.cjs (isMainModulePattern /
bundleContent) is brittle because the regex can stop at the first closing brace
and leave broken JS when nested braces exist in the removed block; update the
fix to robustly remove the auto-start block by either parsing the bundleContent
with a JS parser (e.g., acorn/estree) to locate the if (isMainModule) { ... }
AST node and remove its full range, or implement a brace-matching routine that
consumes nested braces reliably for the if (isMainModule) block in device.ts,
and after performing the replacement add a post-write syntax check (run node
--check on the emitted bundle) to fail the build if the resulting bundle is
invalid.

63-63: Typo: shebanLineshebangLine.

Minor naming nit — "shebang" is the standard spelling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build-mcpb.cjs` at line 63, Rename the misspelled variable shebanLine
to the correct shebangLine across the script (where it's declared as const
shebanLine = '#!/usr/bin/env node'; and any usages) to use the standard
"shebang" spelling; update all references (declaration and any reads/writes) to
shebangLine so the code compiles and names are consistent.

196-204: --production flag is redundant with --omit=dev.

npm install --omit=dev already excludes devDependencies. The --production flag is its legacy equivalent and is deprecated in npm v9+. Using both works but the redundancy may cause confusion.

Proposed fix
-    execSync('npm install --omit=dev --production', { cwd: BUNDLE_DIR, stdio: 'inherit' });
+    execSync('npm install --omit=dev', { cwd: BUNDLE_DIR, stdio: 'inherit' });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build-mcpb.cjs` around lines 196 - 204, In the try/catch block that
runs execSync to install dependencies, remove the redundant `--production` flag
from the command string so it uses only `npm install --omit=dev` (the execSync
call that references BUNDLE_DIR); update the execSync invocation accordingly to
avoid mixing legacy and modern flags while keeping stdio and cwd behavior the
same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/build-mcpb.cjs`:
- Around line 89-103: The code uses a magic fallback blockEnd = blockStart +
10000 when no nextInit is found, which can truncate the final __esm block and
miss await checks; update the logic in the loop that uses esmBlockRegex and
bundleContent so that when nextInit === -1 you use bundleContent.length (e.g.,
blockEnd = bundleContent.length) instead of blockStart + 10000, ensuring
blockBody contains the full remaining content before checking for 'await' and
pushing fixes into the fixes array.

---

Nitpick comments:
In `@scripts/build-mcpb.cjs`:
- Around line 184-189: Add an early validation that packageJson.dependencies
contains the four required keys ("sharp", "@vscode/ripgrep",
"@opendocsg/pdf2md", "pdf-lib") before assembling the dependencies object so you
never inject undefined versions into the generated JSON; check
packageJson.dependencies for each required dependency, log a clear error
including the missing package name (e.g. using console.error), and call
process.exit(1) if any are absent—place this guard near the top of the script
before the code that builds the dependencies object that references
packageJson.dependencies.
- Around line 156-171: The script currently copies package.json via the
filesToCopy array then later replaces BUNDLE_DIR/package.json with a generated
object, so remove that wasted step by excluding "package.json" from filesToCopy
(or add a conditional that skips copying when a later write will overwrite it);
update the filesToCopy declaration or the copy loop around the filesToCopy
symbol and ensure the later package.json generation code that writes to
BUNDLE_DIR (the write that creates the final bundle package.json) remains
unchanged.
- Around line 113-122: The current isMainModulePattern replacement in
build-mcpb.cjs (isMainModulePattern / bundleContent) is brittle because the
regex can stop at the first closing brace and leave broken JS when nested braces
exist in the removed block; update the fix to robustly remove the auto-start
block by either parsing the bundleContent with a JS parser (e.g., acorn/estree)
to locate the if (isMainModule) { ... } AST node and remove its full range, or
implement a brace-matching routine that consumes nested braces reliably for the
if (isMainModule) block in device.ts, and after performing the replacement add a
post-write syntax check (run node --check on the emitted bundle) to fail the
build if the resulting bundle is invalid.
- Line 63: Rename the misspelled variable shebanLine to the correct shebangLine
across the script (where it's declared as const shebanLine = '#!/usr/bin/env
node'; and any usages) to use the standard "shebang" spelling; update all
references (declaration and any reads/writes) to shebangLine so the code
compiles and names are consistent.
- Around line 196-204: In the try/catch block that runs execSync to install
dependencies, remove the redundant `--production` flag from the command string
so it uses only `npm install --omit=dev` (the execSync call that references
BUNDLE_DIR); update the execSync invocation accordingly to avoid mixing legacy
and modern flags while keeping stdio and cwd behavior the same.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4374f6 and c763aef.

📒 Files selected for processing (2)
  • scripts/build-mcpb.cjs
  • src/utils/trackTools.ts

Comment thread scripts/build-mcpb.cjs
Comment on lines +89 to +103
while ((match = esmBlockRegex.exec(bundleContent)) !== null) {
if (match[3]) continue; // Already async
const modName = match[4];
const blockStart = match.index;
const nextInit = bundleContent.indexOf('\nvar init_', blockStart + match[0].length);
const blockEnd = nextInit !== -1 ? nextInit : blockStart + 10000;
const blockBody = bundleContent.slice(blockStart, blockEnd);
if (blockBody.includes('await ')) {
fixes.push({
position: match.index + match[0].indexOf(`"${modName}"()`),
oldText: `"${modName}"()`,
newText: `async "${modName}"()`
});
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Magic fallback blockStart + 10000 can silently miss await in the last/large __esm block.

If the final __esm block in the bundle has no subsequent \nvar init_ marker and its body exceeds 10,000 characters, the blockBody slice will be truncated and any await past that point won't be detected — leaving a non-async callback that will throw at runtime.

Consider using bundleContent.length as the fallback instead:

Proposed fix
-        const blockEnd = nextInit !== -1 ? nextInit : blockStart + 10000;
+        const blockEnd = nextInit !== -1 ? nextInit : bundleContent.length;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build-mcpb.cjs` around lines 89 - 103, The code uses a magic fallback
blockEnd = blockStart + 10000 when no nextInit is found, which can truncate the
final __esm block and miss await checks; update the logic in the loop that uses
esmBlockRegex and bundleContent so that when nextInit === -1 you use
bundleContent.length (e.g., blockEnd = bundleContent.length) instead of
blockStart + 10000, ensuring blockBody contains the full remaining content
before checking for 'await' and pushing fixes into the fixes array.

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Feb 25, 2026

CodeAnt AI finished reviewing your PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant