Summary
Production widget resources still assume the host will execute a static <script type="module" src="…"> (and a stylesheet <link>) from BASE_URL. That works in MCPJam and a normal browser tab. It does not work in ChatGPT or Claude, which render MCP App HTML in a srcdoc iframe and often ignore those static tags.
Dev already has an escape hatch for this (WIDGET_BOOTSTRAP_CLIENTS / inlined HTML). Production currently does not, and the README still says production is unaffected.
What happens
npm run build writes hashed JS/CSS URLs into assets/<widget>.html.
- In production the server fetches that HTML from
BASE_URL and returns it as the MCP resource.
- Sandboxed hosts never run the external module script, so the widget root stays empty.
A two-route Docker deploy (auth'd MCP on 8080, public assets on 4444, serve providing CORS) can still look fine in MCPJam. The same image fails in ChatGPT/Claude.
Proposed fix
When NODE_ENV=production, rewrite the fetched widget HTML before returning it:
- Embed the built CSS in a
<style> tag (stylesheet links are blocked the same way).
- Replace the static module
<script src> with an inline import("https://assets…/widget.js"). Relative chunks then keep resolving from BASE_URL.
- Keep the standalone
assets/*.html unchanged for direct browser checks.
Cover this with unit tests around the HTML rewriter (prefixing the module URL is optional; useful later for cache-busting).
Out of scope for this issue
- Kubernetes sidecar / two-route docs (follow-up).
- Docker
ARG BASE_URL packaging (already required at build and runtime; separate from the host bug).
- App-specific catalog files or CSS chunk names.
Test plan
- Unit: production rewriter embeds CSS, emits
import("…/widget.js"), strips stylesheet/modulepreload tags.
- Manual: production build + public
BASE_URL in MCPJam (still works) and in ChatGPT or Claude (widget actually mounts).
Summary
Production widget resources still assume the host will execute a static
<script type="module" src="…">(and a stylesheet<link>) fromBASE_URL. That works in MCPJam and a normal browser tab. It does not work in ChatGPT or Claude, which render MCP App HTML in a srcdoc iframe and often ignore those static tags.Dev already has an escape hatch for this (
WIDGET_BOOTSTRAP_CLIENTS/ inlined HTML). Production currently does not, and the README still says production is unaffected.What happens
npm run buildwrites hashed JS/CSS URLs intoassets/<widget>.html.BASE_URLand returns it as the MCP resource.A two-route Docker deploy (auth'd MCP on 8080, public assets on 4444,
serveproviding CORS) can still look fine in MCPJam. The same image fails in ChatGPT/Claude.Proposed fix
When
NODE_ENV=production, rewrite the fetched widget HTML before returning it:<style>tag (stylesheet links are blocked the same way).<script src>with an inlineimport("https://assets…/widget.js"). Relative chunks then keep resolving fromBASE_URL.assets/*.htmlunchanged for direct browser checks.Cover this with unit tests around the HTML rewriter (prefixing the module URL is optional; useful later for cache-busting).
Out of scope for this issue
ARG BASE_URLpackaging (already required at build and runtime; separate from the host bug).Test plan
import("…/widget.js"), strips stylesheet/modulepreload tags.BASE_URLin MCPJam (still works) and in ChatGPT or Claude (widget actually mounts).