What is bundling
Bundling turns your UI entrypoint into a single, self-contained index.html
. Scripts, styles, and assets are inlined so you can ship one file instead of a small web app. That file can be embedded as raw HTML in a UIResource
or served directly from disk by your server.
The result is a portable artifact that behaves like a miniature web app but deploys like a document.
Why bundle
You have three ways to deliver UI into an agent chat:
-
Raw inline HTML. It’s the quickest way to get something on screen, but it caps complexity at “what you’re willing to hand‑write in one file.” As your UI grows, you end up re‑implementing build steps by hand or hitting maintainability walls.
-
A remote URL. You get total freedom—routing, assets, origin storage, full frameworks—but you’ve also signed up for hosting, deployment pipelines, versioning, and an extra network round trip before anything renders. Great for full apps, heavy for embedded UI.
Bundling is the middle path. You build a real component (React/Vue/Svelte or even plain HTML), then collapse it into one file. Startup is fast (fewer round trips), and operations are light (no separate UI to deploy). You keep most of the flexibility of a real app without the operational overhead of one.
CLI usage
Bundle a component entrypoint (React/Vue/Svelte automatically detected):
npx @fractal-mcp/cli bundle --entrypoint=./ui/src/MyComponent.tsx --out=./dist/my-component
Bundle a standalone HTML entrypoint:
npx @fractal-mcp/cli bundle --entrypoint=./ui/index.html --out=./dist/my-static-ui
Output: a single index.html
in the destination directory you provide.
CLI flags:
--entrypoint <path>
component (.tsx/.jsx/.vue/.svelte) or.html
--out <path>
output directory for the bundle
Register a tool that serves the bundle
Serve the bundled index.html
as raw HTML in a UIResource
:
import fs from 'node:fs';
import path from 'node:path';
import { createUIResource } from '@mcp-ui/server';
server.registerTool('hello_ui', {
title: 'HelloUI',
description: 'Renders the bundled Hello UI',
inputSchema: {},
}, async () => {
const html = fs.readFileSync(path.resolve('dist/my-component/index.html'), 'utf8');
const uiResource = createUIResource({
uri: 'ui://hello',
content: { type: 'rawHtml', htmlString: html },
encoding: 'text',
});
return { content: [uiResource] };
});
Point the file read at the --out
directory you chose during bundling.
Limitations, tradeoffs, and when to use it
The right choice depends on the surface you’re building and the operational cost you want to carry. Here’s the landscape at a glance:
Tradeoffs at a glance
Approach | Flexibility | Security | Access to localStorage | Complexity/Overhead | Loading Speed |
---|---|---|---|---|---|
Raw inline HTML | Low | High (no external code) | Limited (depends on iframe/origin) | Minimal | Fastest (no fetches) |
Remote URL | Highest | Strong isolation via separate origin | Full (normal web origin) | Highest (deploy, host, auth, versions) | Slower (extra network, DNS, TLS) |
Bundled HTML | High | Good (single file; can be sandboxed) | Limited under srcdoc /no-origin | Low (one artifact, no hosting) | Very fast (single fetch/inline) |
Notes:
- If you embed with
iframe srcdoc
, you either share the parent origin (less isolation) or run without an origin (tighter sandbox, but limited APIs like localStorage). - Bundling uses Vite under the hood and inherits its limitations (e.g., weaker Angular support without additional config).
Recommendations
For tiny, static widgets, inline HTML is hard to beat. For most rich components in chat, bundling gives you the best performance‑to‑ops ratio. Reach for a remote URL when you’re truly building a standalone application and you need origin capabilities (e.g., persistent storage, complex routing) and the full web runtime.
Under the hood
- Vite +
vite-plugin-singlefile
produce a single self-contained HTML file. - Framework detection walks up from your entrypoint to find
package.json
and inspects dependencies (React, Vue, Svelte). If not found, falls back to file extensions. - For component entrypoints, a lightweight bootstrap is generated and rendered, then inlined.
- For HTML entrypoints, assets referenced by the page are bundled and inlined.