Skip to content

fix(mcp): bundle @uppy into self-contained MCP app bundles - #451

Merged
pcfreak30 merged 2 commits into
developfrom
fix/mcp-apps-uppy-bundle
Aug 22, 2026
Merged

fix(mcp): bundle @uppy into self-contained MCP app bundles#451
pcfreak30 merged 2 commits into
developfrom
fix/mcp-apps-uppy-bundle

Conversation

@pcfreak30

@pcfreak30 pcfreak30 commented Aug 22, 2026

Copy link
Copy Markdown
Member

Summary

Fixes Claude hitting Failed to resolve module specifier "@uppy/core" when rendering the MCP Apps (ui:// views) for upload_file and vault_put_file.

Root cause

The upload apps depend on @uppy/core + @uppy/xhr-upload for the out-of-band XHR uploader, but these weren't in the alwaysBundle list of packages/apps/scripts/build-apps.mjs. tsdown therefore externalized them into bare module specifiers (import ... from "@uppy/core") that survive into the served bundle. Host apps are sandboxed so those bare imports cannot resolve, and the iframe fails to render.

Fix

Add @uppy/core and @uppy/xhr-upload to alwaysBundle, forcing them into the self-contained inline-module bundle. Rebuilt all 14 bundles and resynced to internal/mcpapp/appsassets/dist/ (gitignored; CI regenerates via the js-bundle step).

Regression coverage

  • Go guard (internal/mcpapp/mcpapp_test.go): TestAppModuleJSEmbedded now scans all embedded app bundles (previously only a 7-app subset that missed the broken upload apps) for bare module specifiers, with a TestBareModuleSpecifiers self-check for the detector.
  • Browser coverage (tests/sunpeak/tests/inspector-apps.test.ts): new upload_file and vault_put_file tests render the apps in real Chromium across ChatGPT and Claude hosts. They assert the module actually executed via window.__PINNER_CLI_VERSION__ (set as the first statement of the same module script — only present after all static imports resolve), so an unmet import fails the test.

Verification

  • go test ./internal/mcpapp/... — pass
  • All 14 bundles: zero bare module specifiers
  • Browser inspector suite: 6 passed
  • Confirmed negative case: injecting a bare @uppy/core import makes the upload_file test fail, proving coverage catches the exact regression

Files changed

  • packages/apps/scripts/build-apps.mjs
  • internal/mcpapp/mcpapp_test.go
  • tests/sunpeak/tests/inspector-apps.test.ts

Summary

This pull request fixes a critical issue where the MCP apps' self-contained JavaScript bundles contained bare module imports (specifically from @uppy/core and @uppy/xhr-upload) that the browser cannot resolve when served as inline <script type="module"> tags in a sandboxed iframe. This caused the upload apps (Upload to IPFS / Upload to Vault) to fail at load time with "Failed to resolve module specifier" errors.

Changes

Build Configuration (packages/apps/scripts/build-apps.mjs)

  • Added @uppy/core and @uppy/xhr-upload to the alwaysBundle list in the tsdown build configuration
  • This forces these dependencies to be bundled into the output rather than left as external imports
  • Added documentation explaining the requirement that ALL runtime dependencies must be bundled for the self-contained inline module architecture

Test Enhancements (internal/mcpapp/mcpapp_test.go)

  • Added a new bareModuleSpecifiers function that detects bare module specifiers (imports not starting with ., /, or a URL scheme) in the bundle source code
  • Added TestBareModuleSpecifiers to validate the detection logic against good and bad import patterns
  • Enhanced TestAppModuleJSEmbedded to check ALL apps (not just a subset) for bare imports that would fail in a browser environment
  • The test now specifically catches the @uppy/* regression that previously slipped through because the old test only checked a subset of apps

Integration Tests (tests/sunpeak/tests/inspector-apps.test.ts)

  • Added browser-level tests that render the upload apps in a real browser environment
  • These tests verify the modules actually execute by checking for the __PINNER_CLI_VERSION__ global variable that's injected as the first statement of the module
  • If an import fails to resolve, the module never executes and the global remains unset, catching the regression at runtime
  • Added tests for both upload_file (Upload to IPFS) and vault_put_file (Upload to Vault) apps

Root Cause

The tsdown build configuration did not include Uppy packages in the alwaysBundle list, so they were left as external dependencies that resolved to bare imports (e.g., import from "@uppy/core"). When served as an inline module in a sandboxed iframe with no node_modules available, these imports could not be resolved, crashing the apps.

The upload MCP apps (upload_file, vault_put_file) depend on @uppy/core and
@uppy/xhr-upload, but these were not in the tsdown alwaysBundle list, so
they were externalized as bare module specifiers (e.g. import ... from
"@uppy/core") that the host's sandboxed app iframe cannot resolve. This
caused a 'Failed to resolve module specifier "@uppy/core"' crash when
rendering the apps in Claude.

Fix by forcing @uppy/* into the self-contained bundle, and add regression
coverage at both layers:
- Go: mcpapp_test TestAppModuleJSEmbedded now scans all embedded app
  bundles for bare module specifiers (previously only a 7-app subset that
  missed the upload apps), plus a self-check for the detector.
- Browser: sunpeak inspector tests render upload_file and vault_put_file in
  real Chromium across ChatGPT and Claude hosts, asserting the bundle module
  actually executed via the window.__PINNER_CLI_VERSION__ global (which is
  only set after all static imports resolve).
@kody-ai

This comment has been minimized.

Comment thread internal/mcpapp/mcpapp_test.go Outdated
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Code Coverage Report

Total Coverage: 51.7%

Generated from commit: 67f20fe
Repository: LumeWeb/pinner-cli

The MCP app self-containment guard only matched static import-from and
side-effect import forms. A bare specifier introduced via a dynamic
import call would slip past CI and throw the same Failed to resolve module
specifier error when the sandboxed inline module evaluates it. Extend the
regex to match the dynamic form (tolerating whitespace around the parens)
and add bad-vector coverage for it.
@kody-ai

kody-ai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@pcfreak30
pcfreak30 merged commit 65ad9b6 into develop Aug 22, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant