Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure Vite with vite-plugin-singlefile for mandatory single-file HTML bundling of MCP Apps. All assets (JS, CSS, images, fonts) must be inlined into a single HTML file for sandboxed iframe compatibility.
.claude/skills/a5c-ai-single-file-bundling/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 202% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 120% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 96% | 0% |
Configure Vite with vite-plugin-singlefile to produce a single self-contained HTML file for MCP Apps running in sandboxed iframes.
MCP Apps run in sandboxed iframes with no same-origin server. This means:
<script src="./main.js"> will not resolve<link href="./styles.css"> will not loadvite-plugin-singlefile handles this by inlining all assets into one HTML file during the Vite build. This is mandatory for MCP Apps -- without it, the app will show a blank iframe.
vite-plugin-singlefilebase, build.outDir, and entry point<script> tags<style> tagsdist/mcp-app.htmldist/server.jsmcp-app.html vs index.html)typescript// vite.config.ts import { defineConfig } from 'vite'; import { viteSingleFile } from 'vite-plugin-singlefile'; export default defineConfig({ plugins: [viteSingleFile()], build: { outDir: 'dist', // Entry point for the MCP App UI rollupOptions: { input: 'mcp-app.html', }, }, });
typescript// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { viteSingleFile } from 'vite-plugin-singlefile'; export default defineConfig({ plugins: [react(), viteSingleFile()], build: { outDir: 'dist', rollupOptions: { input: 'mcp-app.html', }, }, });
typescript// vite.config.ts import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { viteSingleFile } from 'vite-plugin-singlefile'; export default defineConfig({ plugins: [vue(), viteSingleFile()], build: { outDir: 'dist', rollupOptions: { input: 'mcp-app.html', }, }, });
typescript// vite.config.ts import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; import { viteSingleFile } from 'vite-plugin-singlefile'; export default defineConfig({ plugins: [svelte(), viteSingleFile()], build: { outDir: 'dist', rollupOptions: { input: 'mcp-app.html', }, }, });
html<!-- mcp-app.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>My MCP App</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.ts"></script> </body> </html>
For React (.tsx entry):
html<!-- mcp-app.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>My MCP App</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html>
json{ "scripts": { "build:ui": "vite build", "build:server": "tsc --project tsconfig.server.json", "build": "npm run build:ui && npm run build:server", "dev": "concurrently \"vite\" \"tsx watch src/server.ts\"", "serve": "tsx src/server.ts" } }
typescriptimport fs from 'fs'; import path from 'path'; import { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps'; // Read the single-file bundle produced by Vite const bundledHtml = fs.readFileSync( path.join(__dirname, '../dist/mcp-app.html'), 'utf-8' ); registerAppResource(server, { uri: 'app:///my-app', name: 'My App', mimeType: RESOURCE_MIME_TYPE, async read() { return { contents: [{ uri: 'app:///my-app', mimeType: RESOURCE_MIME_TYPE, text: bundledHtml, }], }; }, });
When converting a web app that already has its own build:
typescript// vite.config.mcp.ts -- MCP-specific build config import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { viteSingleFile } from 'vite-plugin-singlefile'; export default defineConfig({ plugins: [react(), viteSingleFile()], build: { outDir: 'dist/mcp', rollupOptions: { input: 'mcp-app.html', // Separate entry from index.html }, }, });
json{ "scripts": { "build:standalone": "vite build", "build:mcp:ui": "vite build --config vite.config.mcp.ts", "build:mcp:server": "tsc --project tsconfig.server.json", "build:mcp": "npm run build:mcp:ui && npm run build:mcp:server", "build:all": "npm run build:standalone && npm run build:mcp" } }
bash# Required dev dependencies npm install -D vite vite-plugin-singlefile # Framework-specific (pick one) npm install -D @vitejs/plugin-react # React npm install -D @vitejs/plugin-vue # Vue npm install -D @sveltejs/vite-plugin-svelte # Svelte
rollupOptions.input must point to the MCP App HTML file, not the standalone index.html.resourceDomains for large external resources.type="module": The <script> tag in the HTML entry must have type="module" for Vite to process it.vite-plugin-singlefile in devDependenciesvite.config.ts imports and uses viteSingleFile()rollupOptions.input points to mcp-app.html (not index.html)mcp-app.html entry point exists with <script type="module">npm run build:ui produces dist/mcp-app.htmldist/mcp-app.html is self-contained (no external src= or href= to files)build:ui, build:server, builddist/mcp-app.html at runtimejavascriptconst singleFileBundlingTask = defineTask({ name: 'single-file-bundling', description: 'Configure Vite with vite-plugin-singlefile for MCP App', inputs: { framework: { type: 'string', required: true }, entryPoint: { type: 'string', default: 'mcp-app.html' }, outDir: { type: 'string', default: 'dist' }, hybrid: { type: 'boolean', default: false } }, outputs: { viteConfigCreated: { type: 'boolean' }, entryPointCreated: { type: 'boolean' }, buildScriptsAdded: { type: 'boolean' }, artifacts: { type: 'array' } }, async run(inputs, taskCtx) { return { kind: 'skill', title: `Configure single-file bundling (${inputs.framework})`, skill: { name: 'single-file-bundling', context: { framework: inputs.framework, entryPoint: inputs.entryPoint, outDir: inputs.outDir, hybrid: inputs.hybrid, instructions: [ 'Install vite and vite-plugin-singlefile', 'Create vite.config.ts with framework plugin and singlefile', 'Create mcp-app.html entry point', 'Add build scripts to package.json', inputs.hybrid ? 'Create separate MCP Vite config alongside existing build' : null, 'Build and verify single-file output' ].filter(Boolean) } }, io: { inputJsonPath: `tasks/${taskCtx.effectId}/input.json`, outputJsonPath: `tasks/${taskCtx.effectId}/result.json` } }; } });
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 21,178 | 5,953 | -72% | 1 | 1 | 0% | 1,379 | 4,167 | +202% | 0 | 0 | — |
case-02 | fail→pass | 15,128 | 9,520 | -37% | 1 | 1 | 0% | 3,226 | 5,019 | +56% | 0 | 0 | — |
case-03 | pass→pass | 12,718 | 5,772 | -55% | 1 | 1 | 0% | 2,340 | 4,022 | +72% | 0 | 0 | — |
case-04 | pass→pass | 11,482 | 7,722 | -33% | 1 | 1 | 0% | 2,465 | 4,477 | +82% | 0 | 0 | — |
case-05 | fail→pass | 8,415 | 5,651 | -33% | 1 | 1 | 0% | 1,781 | 3,919 | +120% | 0 | 0 | — |
case-06 | pass→pass | 12,683 | 8,712 | -31% | 1 | 1 | 0% | 2,386 | 4,695 | +97% | 0 | 0 | — |
case-07 | fail→fail | 4,586 | 3,229 | -30% | 1 | 1 | 0% | 809 | 3,356 | +315% | 0 | 0 | — |
case-08 | fail→pass | 10,635 | 5,213 | -51% | 1 | 1 | 0% | 1,969 | 3,767 | +91% | 0 | 0 | — |
case-09 | pass→pass | 15,469 | 8,510 | -45% | 1 | 1 | 0% | 2,513 | 4,467 | +78% | 0 | 0 | — |
case-10 | pass→pass | 7,667 | 3,041 | -60% | 1 | 1 | 0% | 1,472 | 3,208 | +118% | 0 | 0 | — |
case-11 | fail→pass | 10,083 | 5,633 | -44% | 1 | 1 | 0% | 2,052 | 4,020 | +96% | 0 | 0 | — |
case-12 | pass→pass | 8,602 | 4,463 | -48% | 1 | 1 | 0% | 1,838 | 3,698 | +101% | 0 | 0 | — |
case-13 | pass→pass | 9,344 | 3,752 | -60% | 1 | 1 | 0% | 1,838 | 3,470 | +89% | 0 | 0 | — |
case-14 | pass→pass | 7,894 | 3,050 | -61% | 1 | 1 | 0% | 1,598 | 3,259 | +104% | 0 | 0 | — |
case-15 | pass→pass | 11,110 | 8,694 | -22% | 1 | 1 | 0% | 2,233 | 4,506 | +102% | 0 | 0 | — |
case-16 | pass→pass | 15,856 | 8,863 | -44% | 1 | 1 | 0% | 2,723 | 4,679 | +72% | 0 | 0 | — |
case-17 | pass→pass | 4,919 | 6,096 | +24% | 1 | 1 | 0% | 935 | 3,867 | +314% | 0 | 0 | — |
case-18 | pass→pass | 10,117 | 4,471 | -56% | 1 | 1 | 0% | 2,169 | 3,663 | +69% | 0 | 0 | — |
case-19 | fail→pass | 11,505 | 4,600 | -60% | 1 | 1 | 0% | 2,261 | 3,647 | +61% | 0 | 0 | — |
case-20 | fail→pass | 12,589 | 3,107 | -75% | 1 | 1 | 0% | 2,420 | 3,418 | +41% | 0 | 0 | — |
case-21 | pass→pass | 13,465 | 10,074 | -25% | 1 | 1 | 0% | 2,589 | 4,645 | +79% | 0 | 0 | — |
case-22 | fail→pass | 11,452 | 4,507 | -61% | 1 | 1 | 0% | 2,363 | 3,635 | +54% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.