mountly
v1.0.1
Published
**On-Demand Interactive UI Platform**
Readme
mountly
On-Demand Interactive UI Platform
Load rich UI only when the user needs it. Use your existing React, Vue, or Svelte components with no new component model. Modernize legacy pages incrementally without rewriting the host app.
Documentation: https://jagreehal.github.io/mountly
Two ways in
Building a web app? Load widgets on user intent — hover, click, viewport, idle — so the page ships a shell instead of everything. → Quick start
Building an MCP server? Turn a React, Vue or Svelte component into an MCP Apps (SEP-1865) view that renders inside Claude and ChatGPT.
npx mountly-mcp create my-app --framework react
cd my-app && pnpm install && pnpm dev→ Agent Skills ·
MCP Apps quick start ·
Mountly vs ext-apps ·
mountly-mcp
Both sit on the same widget model, so a component written for one works in the
other. For MCP Apps, prefer the mountly-mcp docs. You do not need the islands
API to ship a View.
The Problem
Modern web apps ship too much JavaScript upfront. Component libraries load everything at once. Microfrontends are operationally heavy. Framework lazy-loading lacks standardized interaction patterns.
No unified system covers: "Load rich UI only when the user needs it."
What mountly Does
mountly is a frontend platform for building Features, Widgets that load on user intent (hover, click, focus, viewport entry, or idle time).
Before mountly: After mountly:
┌──────────────────────┐ ┌──────────────────────┐
│ Full app JS bundle │ │ Page shell (light) │
│ - Payment widget │ │ │
│ - Video player │────► │ [User hovers] │
│ - Image lightbox │ │ → Load widget code │
│ - Analytics panel │ │ → Fetch data │
│ - Chat widget │ │ → Mount UI │
└──────────────────────┘ └──────────────────────┘
Slow TTI, heavy bundle Fast TTI, lean bundleKey Features
- Intent-driven loading: code splits at the feature level, loads on hover/click/focus/viewport/idle/url-change
- Dual caching: module cache (JS code) plus data cache (API responses) with in-flight deduplication
- Framework-agnostic core: the runtime is framework-agnostic. React, Vue, and Svelte adapters today; Solid in the same shape later.
- Standardized lifecycle:
idle → preload → activate → mount → unmount - Multiple instances: mount the same feature multiple times on one page
- Small core: 2.3 KB gzipped, one file, no dependencies; widgets load on demand, not on page load
- Custom element:
<mountly-feature>web component for declarative usage - Isolation when you need it: light DOM, shadow DOM, or a cross-origin iframe — the host chooses, the widget source does not change
- Analytics: built-in interaction timing and performance tracking
- Predictive prefetch: idle-time loading scored by interaction history
- Extensible triggers: eight built in; add swipe, long-press or a keyboard chord with one assignment to the
triggerstable - Devtools panel: floating debug UI showing live feature states and events
Packages
| Package | Purpose |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| mountly | Core runtime, on-demand loader, lifecycle, custom element, CLI |
| mountly-react | React adapter, createWidget(Component, { styles }) |
| mountly-vue | Vue adapter, createWidget(Component, { styles }) |
| mountly-svelte | Svelte adapter, createWidget(Component, { styles }) |
| mountly-tailwind | Tailwind v4 design preset (opt-in) |
| mountly-vite-plugin | Vite lib build plugin, dual index.js / peer.js widget output |
| mountly-manifest | Vertical registry schema, import map + host helpers |
| mountly-mcp | MCP Apps (SEP-1865) — build views from React, Vue or Svelte components. Subpaths: ./react, ./vue, ./svelte, ./vite, ./server, ./json-render |
Build with Agent Skills
The fastest way to build an MCP App View is to let your coding agent do it. Install the Mountly skills once, then ask:
| Skill | What it does | Try it |
| ---------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------- |
| create-mcp-app | Scaffolds via mountly-mcp create | "Create an MCP App" |
| add-app-to-server | Adds Views to an existing server | "Add UI to my MCP server" |
| convert-web-app | Wraps an existing component | "Turn my component into an MCP App" |
| migrate-ext-apps | Migrates official ext-apps Views | "Migrate from ext-apps" |
/plugin marketplace add jagreehal/mountly
/plugin install mountly-mcp@mountlyOr: npx skills add jagreehal/mountly · full install notes:
Agent Skills.
Build an MCP App from a component you already have
MCP Apps (SEP-1865) lets an MCP server render interactive UI inside Claude, ChatGPT and other hosts. Mountly MCP is the View kit. New dashboards and existing website components use the same path:
import { createMcpView } from "mountly-mcp/vue";
import Dashboard from "./Dashboard.vue";
createMcpView(Dashboard);Greenfield without an agent:
npx mountly-mcp create my-app --framework reactAdd mountlyMcpViews() to your Vite config and npx mountly-mcp build emits
the ui:// resource plus its sidecar. Then develop it against a real host —
sandbox proxy, CSP, the full handshake — without installing one:
npx mountly-mcp dev --server ./server.jsregisterMcpApps(server, { views, tools }) installs the result into an MCP
server you own, so transport, auth and deployment stay yours. The wire protocol
is delegated to the official
@modelcontextprotocol/ext-apps
SDK, so it tracks the spec rather than reimplementing it.
→ Agent Skills · MCP Apps quick start · protocol harness demo
Quick Start (60 seconds)
An island is a URL, a trigger, and some props. Write it in HTML; there is no JavaScript for you to write.
<button data-mountly="/widgets/cart.js" data-preload="hover" data-target="#panel">
Basket (3)
</button>
<div id="panel"></div>
<script type="module" src="https://unpkg.com/mountly/dist/auto.js"></script>Hovering the button downloads cart.js (and its sibling cart.css); clicking
mounts it into #panel. Until then the page ships the button and nothing else.
/widgets/cart.js is any module with a mount:
export default {
mount(el, props) {
/* React, Vue, Svelte, or plain DOM */
},
unmount(el) {},
};The three adapters produce exactly this shape from a component you already have:
import { createWidget } from "mountly-react";
import Cart from "./Cart";
export default createWidget(Cart);See it running: clone the repo, run pnpm install && pnpm -r build && cd
docs/examples/plain-html && pnpm dev, then open
http://localhost:5175/docs/examples/quickstart/host.html
(source). Or try the hosted
quickstart —
no clone required.
The attributes
| Attribute | Default | What it does |
| -------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| data-mountly | — | Module URL, or a key in the host script's data-mountly-urls map |
| data-on | click | When to mount. click focus fires on whichever comes first |
| data-preload | — | When to fetch without mounting — usually hover or viewport |
| data-props | {} | JSON props. A <script type="application/json"> child works too, when quotes get awkward |
| data-target | the element | Where to mount, if not the trigger itself |
| data-toggle | off | A second activation unmounts instead of doing nothing |
| data-css | sibling .css | none, or an explicit stylesheet URL |
| data-mountly-state | — | idle / loading / mounted / error, so CSS can style each. Server-render it as mounted and mountly leaves the island alone |
Triggers read as kind or kind:arg — hover:300, viewport:200px,
media:(min-width: 60rem), idle:2000. click, focus, url and never
take no argument.
When HTML is not enough
import { mountly, mount, unmount, update } from "mountly";
const stop = mountly({
urls: { cart: "/widgets/cart.js" },
// the one escape hatch: retries, auth headers, a bundler's own import(),
// a test double — instead of an attribute for each
load: (url) => fetch(url).then(/* ... */),
});mountly() also watches the DOM, so islands rendered later — by another
widget, by htmx, by a Turbo frame swap — wire themselves up with no ordering
knob to configure.
Going further
- Imperative feature API (
mountly/feature):createOnDemandFeature(...)for data loading, custom cache keys and multi-container lifecycles that the attributes do not cover. See docs/examples/marketing-site. - Plain-HTML host (no bundler):
installRuntime({...})injects a shared-React import map. For direct browser import maps, also map usedmountly/*subpaths (for examplemountly/attach,mountly/elements,mountly/shadow,mountly/assets,mountly/adapter). See docs/examples/plain-html. - Pick a distribution (self-contained vs shared React): when to ship one widget vs many, when to share React. See docs/examples/README.md#choosing-a-distribution.
- Choosing an architecture: monorepo first, widget drop-in, manifest verticals — when you do and do not need micro frontends. See Choosing an architecture.
- Strong isolation (iframe widgets):
mountly/iframeruns a vertical in its own document — its ownwindow, its own styles — with the same triggers and lifecycle. The samecreateWidgetoutput works in light DOM, shadow DOM or a frame, so the host picks the boundary. Overlay breakout and host-owned history: see frame protocols. Manifest flip:isolation: "iframe". - Import-map verticals (advanced): independent widget repos via manifest + CDN, not Webpack federation. See docs/micro-frontends.md and docs/examples/multi-vertical-host. Read the architecture guide first.
- When not to use mountly: single SPA, full SSR-hydration ownership, MFE orchestration control plane. See When not to use mountly.
- All runnable examples: docs/examples/README.md.
- Host runtime API: packages/mountly/README.md.
- MCP Apps integration: docs/protocol-layering.md and docs/how-to-test.md.
- MCP Apps runnable demo:
docs/examples/mcp-app-demofor an end-to-endui://resource + MCP server verification. - Generative UI (agent emits the UI):
mountly-mcp/json-renderrenders@json-renderspecs as MCP widgets with an agent-action bridge;createGenerativeView+streamSpec. Self-driving streaming demo:docs/examples/mcp-generative-demo. - MCP adapter package docs:
mountly-mcp, with subpathsmountly-mcp/reactandmountly-mcp/server. All thin wrappers around the official@modelcontextprotocol/ext-appsSDK (SEP-1865, 2026-01-26).
API Stability
mountly is 1.0. The surface below follows semver: additions ship in minor
releases, breaking changes wait for 2.0.
- the
data-*island attributes anddata-mountly-state mountly()/mount/unmount/update/wire/triggers- adapter contract types (
WidgetModule,AdapterOptions) installRuntimeshape (includingreact/jsx-runtimemapping support)
Migrating from an earlier release? See the changelog.
Releases follow docs/release-checklist.md.
SSR
Render the island's markup on the server and set data-mountly-state="mounted".
mountly skips it — no double paint, no client mount, and the markup stays
styled if JavaScript never arrives. That one attribute is the whole handshake.
<div data-mountly="/widgets/cart.js" data-mountly-state="mounted">
<!-- server-rendered markup -->
</div>Anything inside an island before it mounts is its fallback: a link, a static
summary, a skeleton. Reserve space with style="min-height: 40px" — no
mountly attribute needed, CSS already does this.
Examples
See docs/examples/README.md for start order, ports, and when to use each pattern.
Summary:
docs/examples/payment-breakdown: a popover with async data loading and shadow-DOM stylingdocs/examples/image-lightbox: a media viewer with focus restorationdocs/examples/signup-card: a marketing carddocs/examples/demo: a Vite host that exercises all of the abovedocs/examples/plain-html: bundler-free integration via import mapsdocs/examples/marketing-site: embedding widgets in static HTMLdocs/examples/quickstart/host.html: minimal import map +attach()hostdocs/examples/pokemon-kitchen-sink: stress-test of all features
Development
pnpm install
pnpm -r build
pnpm testLicense
MIT
