Releases: 2nd1st/open-mcp-apps
Release list
v0.6.0 — the engine becomes a runtime
The engine becomes a runtime, not just a container. One release, built over two weeks
(plan: engine repo docs/runtime-plan-2026-08-16.md, internal): apps written outside a chat
and installed from a file, functions that run on their own thread and reach the network, widgets
that declare what they connect to, and the author-side kit those three make possible.
oma.contract is 3 — see Fixed for the one change an existing app can notice.
Changed
- An app document has no size cap. The 200,000-unit write-side ceiling (
MAX_APP_HTMLand
theui_too_largerefusal behind it) is gone — from the store, frominstall-app.mjs, and
from the package barrel. The only size that ever did load-bearing work is on the READ side,
whereget_appreturns the source in windows, so a large app now costs windows rather than a
refusal.empty_uistays: it is a validity rule (an app is something a person opens), never a
size floor. The/rpcand/mcprequest-body limit moves 2 MB → 64 MB and is named
MAX_BODY_BYTES, because it is transport self-defence and was never a statement about how big
an app may be. - App functions run on a worker thread, not a synchronous
vmclosure. A body may
await; the store stays synchronous to it (api.list(...)returns rows) over a blocking
cross-thread call. The time budget is enforced byworker.terminate(), which ends running code,
pending timers and in-flight sockets — a harder cancel thanvm's timeout, which could only
interrupt code it was running. Every function written for the old executor runs unchanged; the
async_not_supportederror is gone (a returned promise is awaited). Default per-call wall clock
2 s → 10 s, sized for one HTTPS round trip; the other budgets (100 writes, 200 reads, 32 KB
result) are unchanged. Up to 8 calls run concurrently; further calls queue without burning their
own deadline.src/functions.mjs's header now says why the old "synchronous is the contract" argument
was right then and what replaces it — including the one property that was given up (no work
after the window) and why the receipts already covered it. - A per-app
ui://resource's_meta.ui.csp(and theopenai/widgetCSPtwin) is computed
per read from the app's declaration ∪ the user's additions, instead of a constant empty
allowlist. The engine adjudicates nothing: it merges and relays; hosts enforce. An app that
declares nothing produces byte-identical metadata to 0.5.9. The engine's own two policies — the
runner child's CSP meta and the/viewresponse header — are built from that same merged
declaration, so an app that works in a host works in the browser viewer; both floors are
unchanged byte for byte.app_htmlreturns the mergedcsp, which is how the loader gives a
sandboxed child its policy. - An app whose
uicarries asset references is read-only to the model:get_appreturns
the template, whileedit_app, the model'ssave_app,restore_appandpromote_apprefuse
withbuilt_outside("source lives outside this store; rebuild and re-install with
install-app.mjs"). Rebuilding and re-pushing is the edit. Detection is structural — what the
stored document is, never anauthorstring. Human-pushed apps keep no version stock: after a
human push onto a human-authored app, revisions older than 7 days (HUMAN_HISTORY_KEEP_DAYS)
are dropped; the current revision is never swept, and AI-authored apps keep everything.
save_apprefusesbad_asset_reffor a reference the file plane could never store; existence
is deliberately not checked at save (a push is two writes and either order must work) — a
missing asset is reported loudly, in the widget, at serve time. - The authoring guide's "keep it under ~100KB" is advice again, and now says what it was
actually about: do not write a complex app in one shot — save a skeleton, then grow it with
edit_app; data belongs in the collection; source is read in windows.
Added
fetchin function bodies, withAbortController/AbortSignal,URL,
URLSearchParams,setTimeout/clearTimeout,TextEncoder/TextDecoder,atob/btoa.
Egress is not filtered: the engine runs on the user's own machine over the user's own network.
The worker has an emptyenvand its own 256 MB heap, and its stdout never reaches the parent's
(which, on the stdio transport, is the protocol channel).manifest.functions[name].timeout_ms— a per-function deadline, declared where the signature
is. The engine sets no policy ceiling on it: the real limit is the host's own tool-call
timeout, sincecall_functionis an MCP tool call the host waits on — past it the body only spins
against a result nobody reads. The default, when nothing is declared, is 10 s. The save door keeps
only a sanity floor (a positive integer the timer can hold), not a cap on how long a function may
run. The deadline covers the whole call — thread start, body, and every await inside it — and is
enforced byworker.terminate(), the cancel that keeps a runaway loop or a hung fetch from pinning
a worker slot. (A SaaS sandbox that wants a cap sets its own; the OSS engine does not.)- Secrets are reserved, not delivered:
api.secretexists and refuses, and settings keys under
secret:are refused by the genericdata_*writers and bysecurity_setalike — the namespace
is held empty for the release that fills it (entry will be the viewer's settings UI, never a
model-facing tool). - Apps can declare where they reach. A manifest may carry
cspwith the four keys from the
MCP Apps spec —connectDomains,resourceDomains,frameDomains,baseUriDomains—
validated for shape at the save door (RUNTIME.md §5.1). Users can add origins of their own, per
app or globally, through the reserved settings keyspolicy:csp:<app>andpolicy:csp:*
(written withsecurity_set, which now refuses a value that is not a well-shaped JSON object of
origins).open_apprenders through the universal loader — one resource serving every app — so
that resource's_meta.ui.cspnow carries the union of everything declared in the store
(every app'smanifest.csp∪ the user's additions), computed per read: hosts are asked to allow
the union, and the runner child inside the loader is narrowed back to its own app. A store with
no declarations serves the 0.5.9 bytes exactly. The per-app resource (OMA_DYNAMIC_TOOLS=1)
still carries only its own app's declaration, and the engine's own viewer and runner always
build from the same merge. The loader's public cache hint is gone with this: its answer is
store-derived, so it is not the same for everybody. Whether a host reads the list-time or the
read-time_metais a host-matrix question, not settled here. - Apps built outside the chat. An app produced by a build pipeline now installs as a readable
template plus its bundle: the template references its own build output
(<script src="oma-asset:app.js">,<link rel="stylesheet" href="oma-asset:app.css">) and the
files live in that app's file plane. The engine inlines every reference at serve time — the
widget CSP allows no external subresource, and a host iframe could not reach this machine
anyway. Documents with no references are served byte-identically to before.
install-app.mjs --manifest <manifest.json>(the declaration as its own file, the shape a build
emits) and repeatable--asset <path>(build output into the app's file plane, keyed by the
file's basename);--updateis unchanged.types/window-oma.d.tstypes the API an app sees
(index.d.tstypes the engine's Node API, which an app never touches), pinned against the same
name listRUNTIME.mdandtest/runtime-contract.mjsshare. RUNTIME.md §6.1 is the target a
build step has to hit — including the one thing bundlers get wrong:<script type="text/oma-function">blocks must be emitted into the template, never bundled. - A host-CSP probe app under
test/probes/host-csp-probe/: install it, open it in a host, and
it writes what that host does with an app'scspdeclaration into its own collection — nine
cells (declared connect, loopback connect, resource, frame, function fetch, a two-step
list-vs-read_metatest, an undeclared-origin control, blob Worker, WebAssembly), each with
the policy the browser actually applied, captured fromsecuritypolicyviolation. Two of the
cells measure something no app can declare —McpUiResourceCspis four lists of domains, so
there is no way to ask for a Worker or for WebAssembly, and this engine's own floor grants
neither (open-decisions D-20). The README is the run-book, one row per host and per door
(open_appvsopen_<name>). types/oma-function.d.ts— theargs/apia function body sees; and both type files now
actually ship: they are in the published snapshot and declared in the packageexports, so
/// <reference types="@2nd1st/open-mcp-apps/types/window-oma" />resolves underbundler
andnode16(it wasTS2688under both — the file existed in the repo and never reached
npm).oma.callFunction<T>()is generic over what the body returns.install-app.mjs --prune-assets— opt-in removal of files this push neither carried nor
referenced, for builds with content-hashed output names (without it every rebuild leaves the
previous bundle behind in the app's file plane).list_appsrows carry afunctionscount when an app's manifest declares any:
· N function(s)in the text line,functions: nin the structured row, and absent — not
0— when there are none. Names and signatures stay oneget_app {slot:"manifest"}away.
NooutputSchemawas added, sotools/listis byte-identical.
Fixed
call_functionnow says what the function returned on the text channel too. The reply
carried the...
v0.5.9 — what initialize declares, the engine now does
What initialize declares, the engine now does.
The handshake every MCP host reads first is a set of promises about verbs, and two of them had never been kept — for as long as they existed, and without a single test going red, because nothing in the repository ever called the verbs the promises were about.
resources: { subscribe: true }was declared from the first release. A host that took it at its word and sentresources/subscribegot-32601 Method not found— on every legacy protocol version the SDK negotiates, which is the wire every shipping host speaks today. The engine already pushesnotifications/resources/updatedto everyone on the connection for every app-plane write (a larger promise than "subscribe"); what was missing was a handler that accepts the request. Bothresources/subscribeandresources/unsubscribenow answer. The bit stays declared, because on the 2026-07-28 wire it is what makes asubscriptions/listenfilter naming our URIs honourable — and that verb the SDK serves itself.tools.listChangedwas written as a conditional meant to sayfalseunless the per-app openers are on. It saidtruein every mode: an absent key is notfalseto the SDK, which fills the bit in with?? truethe moment a tool is registered. Measured in all three settings. It is now written out unconditionally —truewithOMA_DYNAMIC_TOOLS=1,falsewithout.
A new suite, test/capabilities.mjs, starts the server over real stdio, reads what was declared, and then calls each declared verb — on the legacy wire and, separately, on 2026-07-28. Run against the code as it was, three assertions went red by name.
Added
- A bug-report form on the public repository whose first four fields are the tuple nearly every defect in this project's history has turned out to be a property of: host × host version × channel × surface. One template, blank issues still enabled, three contact links (
KNOWN-ISSUES.mdfirst, Discussions for questions and app wishes, the private advisory channelSECURITY.mdnames). No "app request" template on purpose — this project's answer to I want an app is to have your AI build it. - One sentence when a person runs the server by hand. Pasting
npx -y @2nd1st/open-mcp-appsinto a terminal used to end in a cursor that stopped: zero bytes on stdout, because a stdio MCP server prints nothing until a host speaks. Now, when — and only when —stdinis a terminal, one line on stderr says what this process is and where the command belongs (your host's MCP config). Hosts spawning over pipes never see it. Both READMEs' fact table renamed the row that invited the paste from Run it to Command. - A net under every tool call in the smoke suite, reading each result the way the model reads it, so
undefined,NaNand[object Object]cannot reach the model as prose again. 0.5.7 fixed one such row; this pins the species. A row deliberately broken elsewhere was caught by name; the tree as shipped scans clean.
Fixed
- A comment at the top of
src/server.mjssaid the repository must not advertisenpx, and the README's first install path isnpx. The line predates the scoped package. The rule is scoped only — barenpx open-mcp-appsstill runs a stranger's package — and the comment now says exactly that.
The tool surface did not move — 44,911 B, byte-identical to 0.5.8 — because none of this rides tools/list.
Verified before this note was written: the public CI ran on the snapshot commit itself (6f6d2fd) — test (22) and test (24) both green, no MCP host present: https://github.com/2nd1st/open-mcp-apps/actions/runs/31930937332
Full detail: CHANGELOG.md
v0.5.8 — four green lights that could not have gone red
Four green lights that could not have gone red.
The largest group below shares a shape rather than a subject: a reading was trusted for an answer it was structurally incapable of giving.
install.mjs --checkcalled a Codex entry clean by parsing a line on which every value is printed*****.- The MCP Registry badge read
servers[0]off an endpoint that returns every version ever published under a name, oldest first — so it printed a version that got staler with each release. - The link-closure gate this project added one release ago printed CLEAN over the densest batch of relative links in the repository, because one regex read left to right can only ever see the inner half of a nested badge.
openai/codex#28912closing as completed was quoted in the place a reader looks to find out whether a failure still bites — the closure of an umbrella, which cannot speak for any single failure filed beneath it.
None of the four was a wrong number waiting to be corrected. Each was an instrument reporting on itself while wearing the clothes of a report about the world.
Added
- The engine's first MCP prompt,
get_started. The README used to open by asking new users to type a sentence; that sentence is now an entry in the host's own menu. Its body is an instruction to the AI — look at what you already have, check the App Store, readget_app_guidebefore writing anything — not a copy of the authoring guide, and a test fails if any line of it starts appearing verbatim in the guide.
Removed
- The
OMA_DYNAMIC_TOOLS=1the installer wrote into host entries since 2026-07-28. It existed because a chat-surface regression swallowed the loader widget's boot call andopen_apphung at "Loading app…"; it cost one approval prompt per app. Re-measured 2026-08-16 on Claude Desktop 1.30096.5: a registration without the flag renders through the universalopen_app, clicks write, and the data survives a restart. Re-running the installer removes the key from your host entries — and only that key.
Fixed
--checkreported a Codex entry as clean while it carried the retired key, because it parsed output where every env value is masked. It now reads the value where the value exists, and reportsstalewhen it cannot read it at all rather than assuming the best. The same commit closes a defect of the same family: re-registering a stale Codex entry used to send anaddwith no env, which silently dropped a user's other variables.- Every visitor to the npm package page was served the Chinese README. npm force-packs every
README*in the package root and picked the second one. The fix is not to guess npm's rule but to leave exactly oneREADME*at the root; the Chinese one now lives ini18n/. KNOWN-ISSUES.mdpointed at a closed umbrella issue in the place a reader looks to learn whether a failure still bites. It now names the open bug, and says what the closed one was.- The Host support table gave one row to a product with two surfaces. Claude Code in a terminal has no widget surface; Claude Code inside the Claude app does, and renders through the universal
open_app(measured 2026-08-16). The section on the screen beside a terminal now leads with its cheapest form — a browser pane in the same tiled workspace, same machine, no tunnel. - Six passages a stranger trips over that a reader inside this repository completes from memory, plus three comments in shipped files that had stopped being true — including
src/http.mjs's claim that binding to loopback "costs nothing", which stopped being true the day@liveshipped:/mcppays nothing for it,/viewpays a bridge you stand up yourself.
Verified before publishing: the snapshot's own bytes ran through the public CI steps in Linux containers on node:22 and node:24 — 1616 assertions, zero failures, no MCP host present. The published v0.5.7 was run through the same container for comparison (1567), so the delta is a reading, not an inference.
Full detail: CHANGELOG.md
v0.5.7 — what a stranger receives
Three defects that were only ever visible from outside, plus the documentation and the gates that let them live here unnoticed.
All three were found by looking at what a stranger receives rather than at what this repository contains: the text an MCP client is actually handed by list_apps, a link a reader on GitHub actually clicks, and the security alerts the public repository page actually shows. None of them had ever failed a test. The runtime's only behavioural change is one line of listing text — the tool surface is otherwise byte-identical to 0.5.6.
Fixed
list_appstold the model every app wasundefinedcharacters long. The rendered row read a field namedhtml_size; the query behind it has producedlength(ui) AS ui_sizesince the manifest split, and a missing property in JavaScript isundefined, not an error. Every row of every listing, in every host, printed(undefined chars, by …)— while the structured half stayed correct, because it spreads the store row rather than naming fields. Found by driving the published npm package the way a client would (npx -y @2nd1st/open-mcp-apps, initialize →save_app→list_apps), which is why the test now asserts on the rendered text.- A link in the changelog 404'd for every reader (
CLA.md, deleted in 0.5.4 with the MIT relicense). It now points at the last release that carried the file. TRADEMARKS.mdstill described the licence in the present tense as AGPL for the engine and MIT forcomponents/. The whole repository has been MIT since 0.5.4. That file ships in the npm tarball as well as the repository.- Nine Dependabot alerts, not one of them on a dependency we declare —
honoand@hono/node-serverby two paths at once,ip-addressunder the sdk'sexpress-rate-limit,fast-uriunder itsajv. Every repair was a patch release already inside a^range we had written, sonpm audit fixwithout--forcewas the entire fix:package.jsonunchanged,package-lock.jsontwelve lines,npm audit --omit=devreports 0 afterwards. - Documentation that had stopped being true: an assertion count, a size comparison whose argument no longer held once measured (the gap had closed from 2.7× to 1.2×), a contributor guide describing three test suites where there are twenty-three, and two places where the Chinese and English READMEs disagreed about whether an endpoint was included.
Added
- Terminal hosts get a section of their own. A host that renders no widgets was described here as "text fallback" — which reads as no UI. Since
@live, an app opened from a terminal host appears on a screen beside it, and that screen keeps following whatever the AI opens next. The host table now carries that path, with its cost stated: the conversation still shows no widget, and the viewer binds127.0.0.1, so a second device needs the tunnel described above it.
Changed
scripts/publish.mjschecks link closure over the staged snapshot (step 7b): every relative markdown link in a published.mdmust resolve inside the published file set. It covers the two cases nothing else could — a target deleted from the repository, and a target that exists here but is not in theALLOWLIST, which no check running against the internal tree can detect by construction.- The release version now has a machine counting its homes. It lives in six places; two of them —
server.json(the MCP Registry contract) andlhm.plugin.json(the LobeHub one) — were checked by nothing, and one of them silently stayed at 0.5.4 through a release, which merged a directory update into an old entry.test/invariants.mjsnow pins both, along with the package identifiers they carry. - A byte count in
RUNTIME.mdis now derived from source rather than copied by hand; the previous value had been wrong by more than a factor of two.
Verified before publishing: the snapshot's own bytes were run through the public CI steps in Linux containers on node:22 and node:24, on both architectures — 1567 assertions, zero failures, no MCP host present. That check exists because 0.5.5 shipped with a red CI after being "verified" on a machine that had Claude Desktop installed.
Full detail: CHANGELOG.md
v0.5.6 — the public CI goes green
0.5.5 fixed two of the three things that broke the test chain on the public repository; the third was standing behind them. npm test stops at the first failure, so each repair only revealed the next — and this one was invisible to me for a second reason worth recording: I verified 0.5.5 by running the snapshot on a machine where Claude Desktop is installed. The CI runner has no MCP host at all.
test/install-paths.mjscould never pass on Linux. Its fixtures build a fakeHOMEand write Claude Desktop's config intoLibrary/Application Support/Claude— the macOS shape. On Linux the installer looks under~/.config, found no host, exited 1, and took every suite behind it down. The fixture now resolves that path per platform, the wayinstall.mjsdoes.- The same fixtures could write into the person running them. A fake
HOMEis a complete boundary only on macOS; elsewhere the installer honoursXDG_CONFIG_HOMEandAPPDATA, which on a developer machine point at the real user. The sandbox now names those too.
Measured both ways: 30/30 on macOS, 23 suites green in a Linux container.
v0.5.5 — the copy other people get
Nothing in the runtime changed (the tool surface is byte-identical to 0.5.4). Everything here is about the copy of this repository other people get.
Fixed — three things that only ever failed somewhere else
pnpm installhad never worked on any version: the licence collector inprepareassemblednode_modules/<name>by hand, which is npm's layout — under pnpm the first segment is.pnpm, the content-addressed store, and reading its package.json is an ENOENT that fails the whole install. Package roots now come from the bundler's own input paths. (This is also why the Glama build of this repo had never gone green.)- The public repository's CI had never been green:
npm testis an&&chain with a docs-reading suite second, anddocs/never enters a public snapshot — so the other 21 suites had never run here at all. Verified against the real snapshot:npm ci,node build.mjs,npm testall exit 0. require("@2nd1st/open-mcp-apps/package.json")threwERR_PACKAGE_PATH_NOT_EXPORTED.
Added — a Dockerfile that actually runs the server (with the glibc/prebuilt trap that makes a green build die on first query written into the file), .mcp.json, and the registry contracts that had been living in a scratch directory.
Changed — both READMEs rebuilt for the reader they actually have: a fact table and three complete host-config blocks first, deep material below. Two quoted assertion counts turned out to be wrong.
Full detail: CHANGELOG.md
v0.5.4 — MIT
The engine is now MIT licensed — one license for the whole repository (components/ already were). The trademark reservations in TRADEMARKS.md are unchanged. The CLA is retired: MIT in, MIT out, no signature ceremony.
Why: the MCP ecosystem is MIT end to end (the official SDKs, the hosts we bridge into), embedding the engine inside other hosts is exactly how it grows, and several registries reject copyleft outright. Nothing tightens for anyone; everything loosens.
Also in this release: version numbers 0.5.2/0.5.3 were burned by an npm publish incident (npm-side only — GitHub v0.5.2 exists and is fine); the npm package resumes at 0.5.4 as @2nd1st/open-mcp-apps.
v0.5.2 — npm packaging
Packaging-only release: the engine is now on npm as @2nd1st/open-mcp-apps. One-line install for any MCP host: npx -y @2nd1st/open-mcp-apps. No behavior changes — tool surface byte-identical to 0.5.1.
v0.5.1 — an app stops being a guest and starts owning a screen
An app stops being a guest in someone's conversation and starts owning a screen. 0.5.0 made an app's declaration a first-class object; 0.5.1 asks the next question — where is this app standing? — and gives it three answers: a card in a chat, a page it owns, or a region inside a host's panel. One contract covers all three.
Highlights
- The stage contract. Every app used to hand-roll its own outer card, hero header, root width and overflow policy — 7 different frame styles and 10 different width values across the store, double-rounded corners on phones. Now the kit owns the wrapper: in a chat the app gets one standard conditional card (and on narrow frames, none — the host's widget frame is the card); on a page or panel it renders bare, edge to edge. Apps declare their width posture in one manifest word (
stage.width: column | wide | fluid), and all 22 store apps have been rebuilt to the contract — decorative identities (gradients, rings, accent bars) moved into content, where stripping the frame can't take them. @live— a brick, not a route. A display that follows whatever the AI opened last is now a primitive: any app can placeoma.embed("@live"), and the engine keeps a single overwritten pointer (schema v7, deliberately not a ledger event — opening an app is a glance, not a data change) and pushes it over the existing SSE channel. The store ships a ready-made Live Display app: install it, open/view/liveon a spare tablet, and the screen switches by itself as the AI works. Apps that contain@livedeclarestage.displayand are excluded from the pointer — a wall never points at itself.- Panel hosts are first-class. Two opt-in URL words for anyone embedding
/viewin their own chrome:?chrome=0(no viewer bar or stage — the panel draws its own) and?nav=intent(app→app links become anopenmcp:open-apppostMessage instead of navigating the frame — "open X" is a request to the host, not this page's navigation). Embedded child documents now inherit their parent's context, andstage.displayapps render chrome-less on/viewby default.
Fixed, found on real devices
- No sandboxed child document had ever reported its height — a minified-bundle name mismatch landed in the broadcast's own
catch, silently, since the runner shipped. Every embedded frame sat at 140px forever. One line; frames now size to content. - Forms inside embedded apps couldn't submit at all: the sandbox lacked
allow-forms, and Chrome refuses thesubmitevent before dispatch — so everyonsubmithandler (17 of 23 store apps hang their add/edit on one) never ran. Granted, with the actual submission still triple-walled off. - Apps taller than their frame couldn't scroll in fixed-height contexts: seven store apps pinned
overflow: hiddenon the root, which is correct in a host that grants height and a dead scroll wheel in one that doesn't. The viewer now unlocks the root; the apps handed in their declarations.
Upgrading
- A 0.5.0 store upgrades in place on first open (v6 → v7: one added table, one transaction). A store opened by 0.5.1 cannot be opened by 0.5.0 — upgrade every host registration together.
- Tool surface,
oma.contract, and the runtime API are byte-for-byte unchanged from 0.5.0.manifest.stageis optional and additive.
Full detail, including the measurement behind every line: CHANGELOG.md.
v0.5.0 — the declaration becomes an object, the library becomes the App Store
Breaking, and the largest release since the engine existed. Three things moved at once: an app's declaration became a first-class object the engine stores; row deletion became something the engine confirms rather than something every app had to remember to ask about; and an app can now expose a function — a data→data closure the AI can call without rendering anything.
Highlights
- The declaration is an object.
save_apptakesuiandmanifestas separate slots, the stored manifest column is authoritative, and history snapshots both slots —restore_appand undo bring back the pair. A document that still embeds an#oma-manifestblock is refused with a pointer at the parameter. - Deletion is confirmed by the engine, inside the store transaction every tool, batch and widget bridge passes through. Twelve apps used to implement their own arm-then-delete; the ones that forgot simply deleted. No app author writes confirmation UI anymore.
- Functions. An app can declare data→data closures; the AI calls them through
call_functionwithout a render. Bodies are synchronous by design, the seat is opt-in atcreateEngineand absent by default. - The library is now the App Store —
app_store_list,app_store_preview,install_from_app_store, system appapp-store. Oldlibrary_*names are gone, not aliased. The store grew from 17 to 21 ready-made apps, now shipped as directories (components/<name>/{ui.html, manifest.json, fixtures.json}). - Settings and the App Store were rebuilt — the two system apps a user actually sits in, each with a narrow and a wide form, themed entirely through host tokens.
promote_appturns a visual into an app in one call, and edits by range let the AI patch a large app without re-sending the document.- SDK v2. The engine moved to
@modelcontextprotocol/{server,client,node}v2, and a line-by-line audit took the tool surface from 36 to 33. The widget runtime contract bumps tooma.contract = 2.
In a real chat, measured on real hosts
A long tail of fixes came out of live sessions on Claude, ChatGPT and Codex rather than from a test rig:
- A widget's height is now governed by the host: the engine only unpins documents that froze their own measurement (
overflow:hidden,min-height:100vh), so an app that grows is seen to grow — and a card in a conversation no longer nests a private scrollbar. - The App Store no longer pours its whole catalogue into a conversation: Discover is compact in chat, and the only live preview is the detail view you asked for. In the browser viewer, every preview now renders with its sample data (two independent bugs had been emptying 21 of 21).
- A refreshed widget on ChatGPT could come back permanently empty — the host replays a different call's envelope; the widget now writes its identity down through the host's own state channel and recovers.
- Nine store apps crashed on first open in a chat; a filtered app could not shrink back down; the store forced a horizontal scrollbar on narrow widgets — all fixed, each with the measurement that found it recorded in the CHANGELOG.
Upgrading
- A 0.4.2 store upgrades in a single open with nothing lost (v4 → v5 → v6, one transaction each, manifests backfilled by replaying the ledger). Rehearsed against real stores before shipping.
- A store opened by 0.5.0 cannot be opened by 0.4.x — upgrade every host registration and the browser viewer together.
- Hosts holding a cached tool list must re-list: five tools are gone, two are new, four are renamed.
Full detail, including the reasoning and the measurements behind every line: CHANGELOG.md.