NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev
AppPlayer Core — shared Flutter library for MCP server connection, bundle handling, and UI runtime orchestration.
Last release 2 days ago
06 Oct 2026
Ships fairly regularly
a new release about every 2 weeks
Nearly every release is documented
notes for 34 of 36 stable releases
Nothing withdrawn
no release was ever pulled
5 months old
36 releases · first in 2026
One column per month.
OpenSourceLicenses and the tool/licenses/ generators are removed. They are the open_source_licenses recipe (os/core/brain_kernel/recipes/open_source_l
OpenSourceLicenses and the tool/licenses/ generators are removed. They are the open_source_licenses recipe (os/core/brain_kernel/recipes/open_source_licenses) now, with the same API, asset format and generators, so apps and Studio share one copy. An app depends on the recipe and imports package:open_source_licenses/open_source_licenses.dart in place of the core.bundleInstallStore — a browser). Boot passed bundleInstallRoot, a placeholder on such a host, as the directory for the kernel's knowledge-bundle list; reading it threw Unsupported operation: _Namespace and the core came up without its kernel — no kernel tools, no bundle session bridge, no bundle activation. Such a host now gives the kernel no directory, and the list is kept in memory (it is rebuilt as bundles open). Needs brain_kernel with KnowledgeBundleRegistry.inMemory().Stopping health monitoring while a check's keepalive sweep was still out no longer lets that check count a reconnect attempt and schedule a reconnect
isError: true) reaches the runtime as a failure, so the document's onError runs. Its text was read as a JSON payload, came back null, and the call was taken for a success — a refused request showed nothing.AppSession releases this host's hold on its connection, as closeApp does. Only the runtime was removed, so an entry left behind kept its link open and a single-peer device (one TCP peer · an exclusive serial port) could not be reached by the next host until the app quit. A connection another holder still uses stays open.EntryFrame shows a close control on the issuer bar whenever the entry was pushed over something; without it a macOS window had no way back once an entry opened.tcp:// · ble:// · serial://) opens again on a host that answers served credentials. With servingAuthorization set, every streamable-HTTP config went to the authorized HTTP connector, so a tcp:// device was dialled as HTTP and timed out. Credentials now apply to http/https addresses only.ServingHeaders (and AppPlayerCoreService.initialize(servingHeaders:)): other headers a host sends on every request to a served address, apart from credentials — an anonymous per-install visitor id that lets the server tell two people on one network apart.AppNotification.expiresAt: a notification delivered after it stopped being true (an inexact alarm woken late) is dropped instead of shown. ExactNotificationTiming.requestExactTiming(): a port where on-time delivery is a separate grant (Android 12+) implements it — the platform port does; it opens the system "Alarms & reminders" screen for the app. Ask for it with if (core.notifications case final ExactNotificationTiming t) await t.requestExactTiming();; a host's own AppNotificationPort needs no change.AppNotification.at: a notification for a later time is handed to the operating system (iOS time trigger · Android AlarmManager with a receiver, exact where allowed), so it shows with the app suspended. Re-posting an id replaces it; cancel withdraws it waiting or shown.flutter_mcp_ui_runtime 0.8.4 and flutter_mcp_ui_core 0.6.10 (0.6.10 compiles against every json_schema its range allows): apps open side by side no longer move, open dialogs on, or recolour each other (each runtime has its own navigator and theme manager).issuer.support — url · phone · email, platform spec 19 §4.1.3) on EntryIssuer.support, on refusals too; EntrySupport is exported. Values outside their form are dropped.Accept-Language, so a server that fills its words in for the requester answers in that language. The device's languages are sent in order of preference; a host with its own language setting answers it through initialize(requestLanguage:). A header named in the server's config wins. acceptLanguageOf builds the value from a list of locales.ServingAuthorization (authorizationFor before each request · afterUnauthorized after a 401), passed to initialize(servingAuthorization:). A streamable-HTTP connection then asks the host for an Authorization header on every request, and a tool call refused with -32001 is retried once when the host signs someone in. The 401's WWW-Authenticate challenge, which the protocol client drops, is kept to tell whose sign-in was asked for.EntryFrame, EntryMessage, EntryLeaveScreen, EntryChromeLabels — the issuer on top of every state of an entry, and a confirmation before an external target leaves the app. Tiers supply only their words.entryLeaveDestination — the one rule for which external targets may be handed to the operating system (https, tel, mailto, sms).EntryTargetNotInstalled — EntryOpener reports a bundle target this device does not hold as not installed. It is a BundleLoadException (notFound), so code that catches the loader's error keeps catching it.A bundle's own tools (manifest.tools.tools[], every kind) are registered as . , as the platform spec's name isolation requires. They were registered u
manifest.tools.tools[], every kind) are registered as <bundleId>.<name>, as the platform spec's name isolation requires. They were registered under the bare declared name, so two bundles declaring the same name overwrote each other, and a bundle that called its tool by the full name — as bundles authored in Studio do — reached nothing.initialize route and host.mcp.callTool resolve a name in the calling bundle's namespace first (ToolDispatcher.resolveInProcess). The full name works from anywhere; outside a bundle the bare declared name no longer reaches another bundle's tool.tool/licenses/apple_licenses.dart no longer writes the CocoaPods acknowledgements footer (Generated by CocoaPods, which has an empty title) as a notic
tool/licenses/apple_licenses.dart no longer writes the CocoaPods acknowledgements footer (Generated by CocoaPods, which has an empty title) as a notice without a name. Such a list was refused by OpenSourceLicenses.registerBundled, so the licenses page failed on iOS and macOS.tool/licenses/ generators check what they are about to write against the same rule OpenSourceLicenses.parseBundled applies, and stop instead of writing a list registration would refuse (tool/licenses/notices.dart).initialize(domainStorage:) is removed. Hosts pass initialize(kvStorage:) — the kernel's KvStoragePort, which the kernel boots on as well — and native
initialize(domainStorage:) is removed. Hosts pass
initialize(kvStorage:) — the kernel's KvStoragePort, which the kernel
boots on as well — and native hosts use KvStoragePortAdapter. Without
one, state is kept in memory for the life of the process. DomainStorage
and JsonFileDomainStorage are no longer re-exported; KvStoragePort and
KvStoragePortAdapter are.KbAtom takes a BundleKbStore instead of a storage and namespace.host.kb is offered to every bundle. It used to be absent where a host
passed no storage.HostAtomDispatcher takes a fourth argument, the NonJsonArguments the
call carried. A non-empty list means the call is refused.openAppFromServer(..., connectWithin:) — the connect is bounded by the core, and a connect that runs out of time is closed rather than left finishing in the background (where it went on to spoil the next open).retainConnection(serverId, holder) / releaseConnection(serverId, holder) — who holds a connection is counted. An open app and every session lent from it hold the same connection; it closes when the last holder lets go. Closing an app, a metadata read or an open that gave up no longer closes a connection somebody else still uses.AwaitsReachability — a connect error that already knows the endpoint is absent and that something will announce its return. The health monitor then stops redialling an open app every second and waits for hintReachable (FR-HEALTH-011). ConnectionInfo.awaitsReachability reports it.OpenSourceLicenses — the way into an app's open-source notices. registerBundled(assetKey) adds the notices an app keeps as an asset (native dependencies Flutter does not collect: Rust libraries, Swift packages, CocoaPods, Maven modules, bundled binaries); the format is [{"packages": [..], "license": "full text"}] and a malformed list throws. open(context, appName:, version:) shows everything registered next to the Dart packages Flutter collects. The list belongs to each app; this package only opens it.tool/licenses/ — generators an app runs on its own project: apple_licenses.dart (resolved Swift packages at their pinned revision, CocoaPods acknowledgements, unpinned binary artifacts) and android_licenses.dart (release runtime classpath modules, notices inside their archives, and every native library in the release APK traced to a module). Anything that cannot be traced stops the run unless the app's tool/licenses/{apple,android}_extra.json covers it.host.kb records live in the kernel key-value store undertool/capability_probe/README.md is in English, and the Python bytecode that was committed under tool/capability_probe/__pycache__ is no longer part of the package.app/<appId>/kb/, one record per key with a version. appId is what the
host's initialize(appIdOf:) answers for the bundle — a marketplace
listing, for example — and bundle:<bundle id> where it answers null.
Uninstalling a bundle clears this device's copy.host.kb.put and host.kb.delete report a conflict — {ok: false, conflict: {value}} — when the record changed after this bundle last read
it, and write anyway with {force: true}. host.kb.conflicts() lists the
conflicts not yet resolved.initialize(kbAccountRecords:) — while the host answers an account's
records (signed in, syncing), a bundle session opened from then on keeps
its host.kb in the account's app/<appId> scope, with this device's copy
underneath. Writes made while the account is unreachable wait on the device
and go up when it answers; one whose base moved is reported by
host.kb.conflicts() and not forced. Uninstalling clears this device's
copy and queue, never the account's records.host.kb.list(prefix) answers in ascending key order.host.kb refuses a key that is empty, starts with /, contains \ or NUL,
or has an empty, . or .. segment (KB_INVALID_KEY), and a value that
is not JSON (KB_INVALID_VALUE). query without a knowledge engine fails
with KB_QUERY_UNAVAILABLE.kind: mcp tools connect through the kernel's MCP client host: one
connection per bundle and server URL, reused across calls and closed when
the bundle's session closes. target.tool names the remote tool when it
differs from the bundle's tool name. A stdio server (target.command,
target.args) starts through the same client host on macOS, Windows and
Linux; on a phone or in a browser the call is refused with that reason. A
host without an outbound MCP client says so.brain_kernel at ^0.2.2 and flutter_mcp_ui_runtime at
^0.8.0.host.<atom>.<verb> argument JSON cannot carry — a function, symbol,NaN, Infinity, or a structure containing itself — reached the
atom as null, so host.kb.put(key, fn) stored null. The call is now
refused without running the verb, naming where the argument sat: kb
answers KB_INVALID_KEY or KB_INVALID_VALUE, and an atom can choose its
own error through NonJsonArgumentPolicy. undefined still crosses as
null, and an object property holding it is still omitted.evaluateAsync on the native worker failed with settle produced no outcome in two cases and hid the reason. Code ending in ; — or in a //
comment — broke the wrapper that settles the value, so it never ran; it now
settles like the same expression without them. Code that is not an
expression now fails with the engine's own message.Floors flutter_mcp_ui_runtime at ^0.7.12.
flutter_mcp_ui_runtime at ^0.7.12.host.kb — get, put, list(prefix?), delete, query —
with the reference host's return shapes ({ok: true}, [{key, value}],
{removed: bool}), isolated per bundle. Hosts pass a DomainStorage to
initialize(domainStorage:); JsonFileDomainStorage is re-exported for
native hosts. A host that passes none does not offer host.kb, and
query is refused by name where no knowledge engine runs. Uninstalling a
bundle clears its state.kind: cloud and kind: mcp tools run (bundle spec §4.5,
§4.6), so a js tool that calls them through host.mcp.callTool works here
as it does on the cloud runner. cloud POSTs the input as JSON to an https
URL and answers the JSON body; a non-2xx status or a non-JSON body fails
with the reason. mcp calls the remote tool over streamable HTTP, one
connection per call; stdio is refused with the reason.evaluate or evaluateAsync on a js runtime with no host bridge
attached never completed: the worker received its reply channel only when
a bridge was attached, so the reply went nowhere. Bundles always attach a
bridge first and did not hit it; any other caller waited forever. The
channel is now handed to the worker when it is spawned.toString() of the value on QuickJS ({count: 2}), and into
JSON.stringify on JavaScriptCore. The worker now serializes the value in
JS and reads the text back synchronously, so every engine answers the same
JSON; a rejection is an error on every engine, and a value JSON cannot
represent is an error. A result that is not JSON is now reported as a
defect instead of being handed on as a string.Floors flutter_mcp_ui_runtime at ^0.7.9.
flutter_mcp_ui_runtime at ^0.7.9.binding was never
populated by its first read — only by the next notification. The
subscription's initial read spread the payload's top-level keys over the
root state instead of storing the payload at the binding, while the
notification path stores it at the binding as-is (spec §4.5). A page
opened before any notification stayed empty; one re-entered later showed
the previous notification's content, which read as a stale cache. The
initial read and the reconnect re-read now land at the binding the way a
notification does. Measured by konpi on waiting-line.resource read borrowed the subscribe handler. The session
registered no read handler, so the runtime's fallback used subscribe:
every read added a wire subscription and, as above, never wrote the
binding. The session now registers a read handler — a one-shot fetch
stored at the binding, holding no subscription and propagating failure
so the action reports it.Floors flutter_mcp_ui_runtime at ^0.7.8 and mcp_bundle at ^0.4.10.
flutter_mcp_ui_runtime at ^0.7.8 and mcp_bundle at ^0.4.10.ui.text / ui.tree answered nothing once a tab app had a page behind
the front one. The snapshot tools walk the whole render tree, and a tab
page kept alive off-stage stays attached with its last size and, after a
state update, no defined geometry — its transform came back NaN, one NaN
failed the JSON encode, and every text on the screen was lost with it.
Before the update the same page leaked its text at off-screen coordinates.
A node is in the answer only when its rect is finite and touches the
capture surface, in textSnapshot, layoutSnapshot and
resolveElementRect alike. What is not on screen is not the tool's
subject. Measured by konpi on two tab apps after Order → Kitchen and
Driver → Trip.
A scanned server endpoint could be registered but never dialled. EntryOpener wrote the endpoint under url, and the streamable-HTTP transport reads bas
A scanned server endpoint could be registered but never dialled.
EntryOpener wrote the endpoint under url, and the streamable-HTTP
transport reads baseUrl — so every server entry, the most common target
kind there is, registered cleanly and then refused to connect with
streamableHttp transport requires "baseUrl" string field.
It survived because the test that covered the registration asserted the same
wrong key and never connected: "connecting fails offline" made a test that
could not tell a working registration from a broken one. The assertion now
names baseUrl and dials a closed loopback port, so the registration is
still what is pinned but a wrong key fails there rather than in the field.
AppSession.registerIdentityPromotion — a host can now wire how a guest
becomes an identified viewer and back (platform spec 19 §5.3). The runtime
has carried the machinery all along and nothing registered handlers, so
identity.promote was unsupported on every tier.
Narrow on purpose: the runtime stays unexposed. Promotion is the host's act — core has no idea who this viewer would be — and a build that registers nothing still reports the action unsupported, which is the honest answer where there is no sign-in.
Entry resolution follows the link's host. EntryResolverPort.resolve now
takes the claimed host the code arrived on, and HttpEntryResolver derives
https://<host>/api/e/<code> per call instead of holding one configured
endpoint. Platform spec 19 §2 puts the resolver on the entry host domain, so
the host is which registry a code belongs to.
A single endpoint beside a set of claimed hosts was the shape that let a
build claiming two issuers ask the first issuer's resolver about the second
issuer's code — answered wrongly rather than refused, and that answer decides
what a viewer is shown. EntryLink now carries host for the same reason:
dropping it at parse time is what made the mistake invisible.
HttpEntryResolver(endpoint:) → HttpEntryResolver(path:), defaulting to
/api/e. One path for every issuer: a host reads all its claimed domains
with one controller, so a per-issuer path makes the second unreadable.
Dependency floors raised to the versions released alongside this cut —
flutter_mcp_ui_core ^0.6.5 and flutter_mcp_ui_runtime ^0.7.7. A release
declares the workspace's current versions rather than the ones its floors
happened to name; the previous floors (^0.6.4 / ^0.7.6) would have let a
consumer resolve this core against the cut it was built beside.
Debug MCP typeText writes through EditableTextState.updateEditingValue, so the field's onChanged runs. It previously assigned controller.value, which
Debug MCP typeText writes through EditableTextState.updateEditingValue,
so the field's onChanged runs. It previously assigned controller.value,
which does not fire it — bindings written from onChanged stayed empty. The
response carries asKeystroke, false when the field had no input connection
and the value had to be assigned. A readOnly field is now a no-op.
An unregistered tool with no connected server raises ToolExecutionException
instead of returning null. The runtime reads a null return as a successful
call with no payload, so a misspelled tool name reached onSuccess.
An in-process tool that reports failure in its payload ({ok: false, code, error} — the kernel mcp.* and bk.* shape) is handed to the runtime as an
error result, matching what the external endpoint already did. The two routes
disagreed, so a failed mcp.connect resolved in-process fired onSuccess
with the error as its payload.
ui.tap takes longPress (600 ms), holdMs and button
(primary | secondary). The response echoes both; an unknown button is
refused.flutter_mcp_ui_core ^0.6.4 · flutter_mcp_ui_runtime ^0.7.6 ·
brain_kernel ^0.2.1 · mcp_client ^2.2.1. A caret bound on a 0.x minor
cannot reach the next one, so without the runtime floor a host keeps
resolving 0.7.5 and a callback declared as a list still does not run.No source change. flutter_mcp_ui_core ^0.6.3 and flutter_mcp_ui_runtime ^0.7.4: a caret bound on a 0.x minor cannot reach the next one, so without thi
No source change. flutter_mcp_ui_core ^0.6.3 and flutter_mcp_ui_runtime ^0.7.4: a caret bound on a 0.x minor cannot reach the next one, so without
this a host keeps resolving the previous runtime and never sees the fix for a
bound networkGraph.nodes that arrives after the first frame.
The runtime's ThemeManager is a process-wide singleton, and a session rebaselined it on every build whenever ownership had changed hands. With two ses
The runtime's ThemeManager is a process-wide singleton, and a session
rebaselined it on every build whenever ownership had changed hands. With two
sessions on screen at once — a harness opening a second app over the first, a
shell stacking renderer routes, Cloud's launcher model doing exactly that —
each build took the singleton from the other, every apply notified listeners
from inside a build (setState() called during build), and the frame never
settled: measured live as a page transition frozen mid-slide.
Ownership answers "did the app change"; it does not answer "would applying again change anything", which is the question that matters when two sessions are alive. The rebaseline now skips when the content it would apply is already the content in place, whoever applied it.
`app.bundles` / `app.open` on the debug host. Reaching an installed bundle meant going through the launcher, which meant registering it in the user's
app.bundles / app.open on the debug host. Reaching an installed bundle
meant going through the launcher, which meant registering it in the user's app
registry first — a probe that edits what it measures. app.open routes through
the shell instead: the core knows what is installed, the tier supplies
AppPlayerCoreService.debugOpenBundle and only it knows how to put a screen up.
A tier that wires nothing gets a tool that reports it cannot open, never one
that claims success — the same rule the UI DSL applies to capabilities.
tool/capability_probe/. analyze 0, a full suite and a clean dry-run are
statements about source; none of them opens the built app. Two defects shipped
on 2026-08-07 that no source gate could see — a bundled PDF and Lottie drew an
empty box, and a web build carried a stale plugin registrant so connectivity,
audio and video were never registered. The probe builds a bundle exercising
every declared capability and reads the running app: did the section report
(none), and did it put pixels on its own background. Platform views are
judged by their report alone — a Flutter screenshot does not capture a native
view, so pixels would read blank however well they work. Verified by
re-introducing the byte-path defect: pdf: reported <empty>.
`ui.drag` on the debug host. Tap, type, scroll and screenshot were there; drag was not, so a widget whose behaviour *is* a drag — a tree that reorders
ui.drag on the debug host. Tap, type, scroll and screenshot were there;
drag was not, so a widget whose behaviour is a drag — a tree that reorders, a
kanban card that moves — could not be verified on a running app at all. The
gesture is dispatched as a press, a held interval, stepped moves and a release:
one jump is not a drag to a recogniser, it is one enormous delta with no
gesture in between.
bundleRootPath reaches the media path. A bundled sound arrives as a
file: reference after the core rewrites bundle://, and nothing downstream
could read those bytes — playback worked (the player opens the path itself)
while anything needing the bytes, such as a waveform, got nothing.
resubscribed after reconnect logged count: <number of URIs attempted>. A
reattach where every single subscribe was refused printed the same line as one
where all of them landed, with the failures on their own earlier lines — so the
summary read as success. It was read as success: someone debugging a board that
had gone silent took it for a healthy reattach and looked elsewhere.
resubscribed and failed separately.reattach returns a ReattachResult instead of nothing, so the two numbers
are assertable rather than only printable. isTotalFailure is the case that
matters — the connection is up and every stream on it is dead.appplayer_core warns once per reattach that re-subscribed nothing, at a
level a log reader notices, rather than leaving it to be inferred.Reported by a consumer board (ESP32 air-quality node) whose owner misread the line exactly as described.
ConnectionHealthMonitor gave a server up for good once its attempt count ran out. The count only clears when a health check observes connected, and af
ConnectionHealthMonitor gave a server up for good once its attempt count ran
out. The count only clears when a health check observes connected, and after
the monitor stops retrying that can never happen — so a foreground app with the
screen on sat there not dialling, and only leaving and re-entering it (or a
background round trip, or a wake) brought the connection back. Found on real
hardware: switch a hotspot off and back on a minute later and the board never
returns.
The tier that hurt first was the one tuned to recover fastest. checkInterval
was doing two jobs — the keepalive/detection sweep AND the retry pace — so Pro
dropping it to 2s to catch BLE hard-drops also burned its 5 attempts in about
fifteen seconds, against ~105s for the default tier.
checkInterval paces detection only;
retries pace themselves with an exponential backoff (reconnectDelay
doubling up to the new maxReconnectDelay, default 30s).maxReconnectAttempts defaults to 0 = unlimited: while monitoring runs the
monitor does not give up. What keeps a dead server from being dialled every
couple of seconds is the backoff ceiling, not an attempt count. Hosts that
want the old behaviour can still pass a positive cap.startMonitoring() clears grown backoff (a foreground return is new
information), stopMonitoring() stands down a reconnect already waiting, and
a wake-driven sweepStale() resets the backoff for what it sweeps.isEngaged seam; appplayer_core wires it
to "a runtime is open for AppHandle.server(id)". A dashboard tile is
deliberately NOT engaged — treating every tiled server that way would dial the
whole home screen every second.reconnectDelay + up to one checkInterval, so Pro's 1s was landing as 1-3s. The loop exits on exactly
three conditions — the app closed, the connection entry is gone, or monitoring
stopped — so nothing dials a server nobody is watching, or a serverId that no
longer exists.Behaviour change, intended: every tier now retries indefinitely while
foreground. Nothing to change at call sites — maxReconnectDelay is additive
and the default flip is the fix.
Floors: mcp_client ^2.1.0 → ^2.2.0 (internal dependency at its latest
published version). Everything else was already there.
SRS NFR-REL-002/003 + new NFR-REL-005/006, FR-HEALTH-003/004/006 + new
FR-HEALTH-007 · DDD/TEST connection-health-monitor · regressions
TC-HEALTH-010~019 and IT-001b (11 mutations killed, including the wiring).
New: hintReachable([serverId]) on the core service — the door for "this may
be reachable now" signals (network returned, device sighted on the discovery
axis, user pressed retry). A connection waiting out its interval dials at once
instead of at the end of a number picked without knowing anything; omitting the
id serves every failed connection, which is the only path remote/cloud servers
have (nothing ever "sights" them). Signals accelerate; they do not replace the
timer, because no single signal source covers every transport.
bindOnlineChanges(Stream<bool>) turns a host's network-availability source
into that signal, firing on the offline→online edge only — platforms repeat
"connected" for every interface change, and the first observation says where we
are rather than that anything changed (treating it as a regain dials on every
launch). It lives here rather than in a host recipe because it needs no radio
and no plugin, every tier already depends on this package, and it is the only
reachability signal a remote / cloud server has.
Paired with it, stalledServers — the servers an open app is waiting on
(failed connection ∩ app open). A host that wants to hear a device come back
rather than dial for it should observe exactly these and stop when the set
empties; observing everything ever registered is the always-on scan the
discovery axis was deliberately scoped away from.
The retry pace applies between dials, never on top of one: the chain awaits its
attempt and holds the server's slot across it, so a 1s pace on a dial that takes
five seconds is one dial every six, not five overlapping ones. Bounding the dial
itself stays with the host connector (Pro bounds board dials at 20s); core
connect() has no deadline of its own.
No source change in this package. The floors move because a caret bound on a 0.x minor cannot reach the next one, so without this bump a consumer of a
No source change in this package. The floors move because a caret bound on a
0.x minor cannot reach the next one, so without this bump a consumer of
appplayer_core keeps resolving the previous runtime and never sees the cut:
flutter_mcp_ui_core — the registry narrows in four places (retired legacy
enum spellings on linear.distribution and qrCode.errorCorrection, the
otpInput.autoSubmit property, and a required value on option objects),
which is what makes that release a minor rather than a patch.flutter_mcp_ui_runtime — vector assets (SVG) draw in every AssetRef
slot including icon, union-typed slots read every branch they declare
(Dimension objects, action lists, bindings in enum and EdgeInsets
slots), ink overlays paint above what the document paints, and ten declared
properties gained implementations. Brings flutter_svg transitively.Consumers that pin appplayer_core need only this bump; the surface they
compile against is unchanged.
Floors flutter_mcp_ui_core ^0.4.3 → ^0.5.0 and flutter_mcp_ui_runtime ^0.5.3 → ^0.6.0.
Floors flutter_mcp_ui_core ^0.4.3 → ^0.5.0 and
flutter_mcp_ui_runtime ^0.5.3 → ^0.6.0.
Both are minor because the schema narrows: AssetRef slots reject a bare
string carrying no scheme, the icon slots take the new IconRef, and thirteen
string properties that documented their values in prose now declare enum. A
caret bound on ^0.5.x cannot reach 0.6.0, so this floor is what lets any
AppPlayer tier see the new runtime at all.
What arrives with it: one asset resolution path for every AssetRef slot
(image / avatar / icon / box.decoration converge, and bundle:// and
client:// are resolvable for the first time), navigation.openUrl, and 23
new widgets. No API in this package changed — the surface is identical and the
bump is the dependency cut.
AppPlayerCoreService.installBundleFromBytes — install from .mcpb bytes already in hand. A host that fetched the archive itself has bytes and never a p
AppPlayerCoreService.installBundleFromBytes — install from .mcpb
bytes already in hand. A host that fetched the archive itself has bytes
and never a path; installBundleFromFile is the same call with a read
in front of it.AppPlayerCoreService bundleInstallStore: — install into and read
installed bundles from host-provided storage instead of a directory.
When given, bundleInstallRoot is not used as one.BundleInstallerAdapter.onStore and BundleLoaderAdapter(installStore:)
— the same seam one layer down.BundleApplicationAdapter requires the bundle to carry readable files,
not a filesystem directory. A bundle installed into host storage now
adapts; previously it was refused outright with
BundleAdaptException(unsupportedEntryPoint) before anything was read.File(<directory>/<entry>), so bundles that carry JS tools
work on hosts with no filesystem.Desktop and mobile behaviour is unchanged — omit bundleInstallStore and
the filesystem path is taken exactly as before.
ConnectionManager.onClientAttached — hook fired whenever a server's
client is replaced (first connect included).ResourceSubscriber.reattach — re-issues the wire resources/subscribe
for every URI recorded under an ownerKey, plus the initial read so the
first value after a resume is current rather than the frozen one.
Bindings are NOT re-registered; they never went away.AppPlayerCoreService wires the two, covering the full-screen app AND
the dashboard's per-device summary runtime (a composed tile watching the
same device is otherwise the one surface still frozen).initialize threw Unsupported operation: Platform._operatingSystem on the web and took the whole host down before the first frame — a blank page. One l
initialize threw Unsupported operation: Platform._operatingSystem on the
web and took the whole host down before the first frame — a blank page. One
line built the LifecycleCoordinator with
platformSuspends: Platform.isAndroid || Platform.isIOS, and Platform is
dart:io. It is now !kIsWeb && (...).
false is the truthful value on the web rather than a way around the throw:
a tab has no process to suspend, and no native background port is injected
for continuity to pause against. The value was also not injectable — the
coordinator takes the flag but initialize hardcoded it, so a host could not
work around this from outside. The neighbouring ports (backgroundPort,
permissionPort, notificationPort) all have seams and web hosts had
already passed them by injecting no-ops; this was the next line.
Reported against a release web build with source maps, so the frame was the line and not a guess.
Against the real app, both directions. appplayer_cloud in Chrome resolving
this package from a local path booted with no exception; the same app with the
published 0.1.16 rendered its error screen naming
app_player_core_service.dart 621:34. Same app, same browser, one line
different.
A @TestOn('browser') boot regression is written
(test/integration/web_boot_test.dart) and is skipped, because it hangs:
headless Chrome loads and initialize never completes there, while the same
initialize completes in the real app. The difference is what a host injects,
so the harness — not the fix — is what is unfinished. It ships skipped with
that reason attached rather than deleted, because the gap it names is real:
every other test that calls initialize runs on the VM, which is why a
dart:io call sat on the boot path unnoticed.
The per-bundle JavaScript runtime now resolves per platform. JsToolIsolate became a conditional export: the existing embedded-engine implementation on
JsToolIsolate
became a conditional export: the existing embedded-engine implementation on
platforms with dart:io, and a Web Worker implementation elsewhere. The
native implementation is the same code, relocated to js_tool_isolate_io.dart
— no behavior change off the web.src/js/js_bridge_protocol.dart and
is shared by both branches: host.<atom>.<verb>() returns a Promise resolved
through __hostResolve / __hostReject exactly as before. Only the transport
differs — an isolate port natively, postMessage on the web — so a bundle's
JavaScript behaves identically on both.web dependency, used only by that branch.function (p) {...} in statement position parses as a function declaration
and is rejected for having no name, so the host bridge failed to install.
This was introduced by the refactor and reached the native branch too, where
no test could see it — the embedded engine cannot start inside flutter test,
so nothing had ever executed that bootstrap. The browser suite now evaluates
the native variant of the string in a real engine, which closes that gap.worker-src blob:, and the Worker needs
'unsafe-eval' — executing caller-supplied JavaScript is the feature. This
fails far from its cause, so it is also recorded in the source.dart:ffi out of the web dependency graph, which is what stopped
flutter build web for any application depending on this package.`openAppFromServer(..., entry:, identity:)` and `openAppFromBundle(..., entry:, identity:)` — carry how an app was reached and who is looking at it. A
openAppFromServer(..., entry:, identity:) and openAppFromBundle(..., entry:, identity:) — carry how an app was reached and who is looking at it. An entry naming a route opens the app on that page instead of its own initialRoute, which is what lets a scanned code, a deep link, or an app-to-app open land where it asked. Both parameters are optional and absent for a launcher open, so every existing call is unchanged.
Re-exports the entry value types (EntryContext, EntryIssuer, EntryNotice, IdentityContext, IdentityState, IdentitySubjectKind, EntrySession, EntryStateKeys) so a host consuming only appplayer_core can build them.
Deferred entry (DeferredEntryResolver, DeferredEntrySource, FirstLaunchStore) — an entry that sent someone to an app store resumes on first launch (§3.5). The policy is pure and the platform pieces are injected, so the rules hold on a platform that can carry a code and on one that cannot.
EntryOpener — turns a resolved target into an open session. Tiers differ in chrome, not in what a target means: "a server target is an endpoint you register and open" is the same sentence everywhere, so it stopped being rewritten per tier. A server learned from an entry is registered under an id derived from its endpoint, so scanning the same medium twice reuses one row instead of accumulating one per scan. localServer needs a discoverer the tier wires (discovery is a host capability); without one the entry fails visibly rather than dialling something else. A listing deliberately has no path to a screen here — acquisition is the marketplace's act, and a path from a listing id to a render would blur install and run.
EntryLink.parse — reads the opaque code out of a claimed https link. Host matching is exact: a suffix match would accept evil-entry.example.test for entry.example.test, and a build resolving codes from a host it does not claim is resolving someone else's registry. Everything after the path prefix is the code, so an issuer may partition its code space however it likes and this side stays ignorant of the shape. A link this build does not claim is rejected with a reason, not swallowed — the host falls through to whatever it normally does with a URL.
Entry resolution pipeline (EntryResolverPort, EntryPipeline, EntryTarget and friends) — the host side of platform spec 19. Every acquisition path (an intercepted link, a scanner, a deferred entry recovered after an install) produces the same code and takes this one path, so the rules hold whichever door the code came through. The resolver itself is a port: the platform never assumes where the medium registry lives.
canIdentify — a build with no sign-in refuses a required entry instead of rendering it as a guest. Answering a demand for identity by ignoring it produces a screen that looks like it worked, which is the failure mode this whole layer exists to prevent. Distinct from an unsupported target: the destination is fine, the viewer cannot be established.status is not ok, and an unparsed identityPolicy demands identity rather than assuming guest. Guessing open would render a guest surface for an entry we failed to understand; guessing required merely asks someone to sign in.EntryTarget.toEntryContext() is the only thing that crosses into the document — route, params, issuer, grant scope. The grant token and the medium's owner/holder never do.launchRoute: alongside entry: on both open paths. An in-app open (DSL §4.3.1 navigation.openApp) names a page without being an arrival, so it sets the route and leaves the document's entry.* tree absent (§8.9.1).
AppSession.launchRouteMissing — true when the entry named a page this app no longer declares. Spec 19 §9.6 puts the disclosure on the host, and a host cannot render a log line; without a surface to read, "fell back" and "worked" are the same outcome from outside.
A route the app no longer declares is not honoured silently: the runtime falls back to the app's own initial route and the miss is logged (entry.route.missing) for the host to disclose. A stale binding that quietly renders the home page is indistinguishable from a working one, and bindings outlive app versions.
Re-opening a handle whose runtime is still alive adopts the newer entry. Without that the same medium scanned twice would render the first scan's context.
openSavedDeviceAsOrigin(id) — opens a saved device as a composition origin through the same ConnectionManager the launcher uses, and adoptConnectionAsOrigin({id, client}) under it. Origins used to be opened on a private stack while the launcher used its own, both keyed by the same device id, so neither could see the other's link: opening a device from a composed screen and then from its own app dialled twice, and the board — single-peer — refused the second with Transport disconnected. One connection per device now, shared by both.Client.onNotification keeps one handler per method, so once one connection per device is shared the notification router took the slot from the kernel connection the tiles listen on. Both now register through SharedClientNotifications, which takes the slot once and fans out. Subscriptions are reference-counted the same way (SharedResourceSubscriptions), so one consumer releasing no longer stops the other's stream.ui://app + ui://page/main over the wire, so the tile spun and looked like it was reconnecting while the standalone screen came back instantly. Resolved definitions are now cached per origin and dropped when the origin is (re)opened, which is the only moment the document can have changed: a device that rebooted with new UI necessarily got a new connection first. Measured after the fix: re-entry 1.1s against 1.2s for the first open, both tiles fully rendered. (Fixed in the composition_host recipe and re-vendored.)composition_host recipe and re-vendored.)registerDefinitionResolver(resolve) — the host resolver behind a view / route DefinitionSource that names an origin. Registering it is how this host claims the Composition Profile; without it view fails closed and renders its fallback rather than resolving a foreign $ref against the app's own server (spec §18.7.3), which would put one device's UI under another's identity.useKernelDefinitionResolver({readOwn}) — the canonical wiring, reading through the kernel's outbound mcp.* surface. Platform spec 06-tool-registry.md already declares "the app/bundle drives mcp.* directly and fetches a resource (e.g. a dashboard UI)" as the default path, and those tools are already on this host's in-process dispatcher — so composition needs no new transport, no new connection registry, and no manifest field, only a reader. Parses the shape a board actually serves (contents[0].text carrying escaped JSON) and accepts an already-decoded map.openOrigin hook — a document names an origin; the host opens it on first use. Registering a device does not hold a connection open, and holding one would be wrong: several boards serve a single peer at a time, so a permanent connection each has the last one to connect reset the others.mcp.call_tool on the named connection; a watch reads the current value once (a subscription reports only changes) and then follows notifications/resources/updated.readOwn, an unrecognised origin key, and an empty connection id all throw rather than falling back to the app's own server (spec §7.10.1 rule 6).flutter_mcp_ui_runtime floor raised ^0.5.2 → ^0.5.3 (the view widget + registerDefinitionResolver seam).brain_kernel floor raised ^0.1.8 → ^0.2.0 (resource subscription on a kernel connection).Additive (0.x → patch). No public API removed.
Additive (0.x → patch). No public API removed.
ServerReGrant seam. A marketplace server's credential
is a short-lived per-user connectionToken baked into
ServerConfig.transportConfig. When a connect attempt fails and the server
carries a bearer token, ConnectionManager now calls an optional host-supplied
re-grant hook, refreshes the token, and retries the connect once (the retry
runs without re-grant so a persistently bad server can't loop). This closes the
gap where opening a saved server app with an expired token 401'd until a manual
reinstall — openAppFromServer, reconnect() and ConnectionHealthMonitor all
funnel through connect(), so all three are covered.
typedef ServerReGrant = Future<ServerConfig?> Function(ServerConfig stale)
(barrel-exported), ConnectionManager.tokenReGrant (mutable, optional),
AppPlayerCoreService.serverReGrant setter (host wires it after the
marketplace session exists).docs/, specs/, spec §N)
from source comments so nothing points outside the published package. No code
change.Additive across the tracks landed since 0.1.12. ^0.1.12 consumers pick these up on floor-bump; no public API removed.
Additive across the tracks landed since 0.1.12. ^0.1.12 consumers pick these
up on floor-bump; no public API removed.
appplayer_core declares a flutter: plugin: with native Android/iOS
adapters (background execution, OS permission, notification); desktop/web
degrade to the Dart ports' NoOp. onLifecyclePhase(AppLifecyclePhase) drives
the foundation.fetchServerMetadata(serverId) /
fetchBundleMetadata(BundleRef) read a card's name/icon WITHOUT rendering, so
install ≠ run (the launcher tile is populated, the UI loads on first open).AppLoader.wrapAsApplication(...)
promotes a bare served page into a single-route application (app name = page
title), so a server app renders with the standard chrome (AppBar/Close) and no
separate metadata serving.enableDebugMcp, settings-gated, non-web) MCP
server on 127.0.0.1:<port>/mcp exposing ui.screenshot / ui.tree /
ui.tap / ui.type for test automation; debugCaptureWrap(child) gives the
capture/tap primitives a stable render boundary._handleTransportDrop
with DisconnectReason) + keepAliveSweep(...) for reconnect/resume.mcp_client ^2.1.0,
mcp_bundle ^0.4.8, flutter_mcp_ui_core ^0.4.1,
flutter_mcp_ui_runtime ^0.5.1, brain_kernel ^0.1.8.AppSession.buildWidget / buildDashboardWidget rebuild the theme state on every entry: the runtime's ThemeManager is a process-wide singleton, so the p
AppSession.buildWidget / buildDashboardWidget rebuild the theme state on
every entry: the runtime's ThemeManager is a process-wide singleton, so the
previous app's palette/mode leaks into the next one and any runtime widget's
dispose clears the brightness pin. Entry now applies the app's own declared
theme — or a SYSTEM BASELINE with real light AND dark token sets — whenever
ownership changes hands, then re-pins the current host brightness. The bare
defaultLight() default has no dark tokens, which is why undeclared apps
rendered "weird dark" on first entry until another app left a full palette
behind in the singleton.MCPUIRuntime.destroy → reset()), so a stale tag made
same-app re-entry skip over the bare default — re-open of the same app
rendered weird while a detour through another app healed it.streamableHttp transport carries accessToken as Authorization: Bearer (+ headers passthrough, explicit header wins); it was dropped entirely, so token
accessToken as Authorization: Bearer
(+ headers passthrough, explicit header wins); it was dropped entirely, so
token-gated servers rejected the handshake (401 → "Transport disconnected").mcp_client ^2.0.1 (spec-optional description parse fix).AppPlayerCoreService.connectExtensionTransport now delegates to the brain_kernel core connectExtension(clientHost, …) helper off the abstract KernelCl
AppPlayerCoreService.connectExtensionTransport now delegates to the
brain_kernel core connectExtension(clientHost, …) helper off the abstract
KernelClientHost, dropping the redundant concrete McpClientKernelHost
field/ref and the inline probe-and-cast (the is-no-promotion footgun is now
sealed inside the kernel helper). Behaviour unchanged.brain_kernel ^0.1.2 → ^0.1.7 (the ExtensionTransportConnect
capability interface + connectExtension helper). No API change to
appplayer_core's own surface.AppPlayerCoreService.registerCapabilityTools(tools) — additive public seam to register host-supplied in-process capability tools (e.g. a desktop io.*
AppPlayerCoreService.registerCapabilityTools(tools) — additive public seam
to register host-supplied in-process capability tools (e.g. a desktop io.*
process/device tool-pack) after boot. The core depends on no capability
package, so platform-specific adapters (dart:io process execution, etc.)
stay in the host layer; the tools share the same in-process dispatcher as the
standard bk.* / mcp.* surface. Safe to call more than once.AppPlayerCoreService.connectExtensionTransport({id, transport}) — new public method that lets a host app connect to an external MCP server over a host
AppPlayerCoreService.connectExtensionTransport({id, transport}) — new
public method that lets a host app connect to an external MCP server over a
host-supplied extension transport (serial / usb / ble / tcp / ws) without
adding the transport's FFI / platform dependencies to appplayer_core. The
transport is built by the calling app (e.g. using classes exported from
mcp_bridge) and injected here; the core routes it through
McpClientKernelHost.connectWith (brain_kernel 0.1.2). Returns a
KernelClientConnection whose callTool / readResource / listTools
reach the remote server. Throws StateError when the kernel is not booted.brain_kernel ^0.1.1 → ^0.1.2 — connectExtensionTransport delegates
to McpClientKernelHost.connectWith and re-exports clientTools, both new
in brain_kernel 0.1.2. The floor guarantees these symbols are present.AppPlayerCoreService method or constructor
changed. Apps that do not use extension transports see no behavior change.analyze 0 issues.BrainBridge removed (phase D · 2026-05-24) — 451 lines + 6 facade wrappers + 2 tests dropped. AppPlayerCoreService now calls KernelApp.boot(...) direc
AppPlayerCoreService now calls KernelApp.boot(...) directly, registers standardTools(app), and delegates setActiveBundle / scopeIdFor. Zero external-shell cascade.BundleSessionBridge lifecycle wired at 5 points of AppPlayerCoreService (boot / activate / onClose / closeApp / dispose). _sessions is a per-bundleId map. McpAtom + AgentAtom gain optional bridge / session arguments and wrap dispatch in bridge.runScoped(session, ...)._activateBundleSections exposes the active bundle at the well-known bundle://manifest.json resource (shared by the local-bundle and served-bundle paths). openAppFromServer reconstructs a served bundle: it detects the document, parses it with McpBundleLoader.fromJson, and runs the same kernel activation a local bundle uses (knowledge / settings / behavior come live); tool execution stays remote and the UI loads via ui://app. New servedResources / readServedResource accessors. ApplicationLoader.load gains an optional resources parameter so the server is listed only once.brain_kernel/lib/src/system/bridge/), so hosts import only package:brain_kernel/brain_kernel.dart.unnecessary_import hints across test/src/dashboard/dashboard_bundle_test.dart + test/src/session/app_session_impl_test.dart (info → 0).brain_kernel ^0.1.0 → ^0.1.1 — the served-bundle path relies on brain_kernel 0.1.1 (bundle behavior activation + the MCP serving surface). This transitively raises mcp_bundle to 0.4.1; appplayer_core references no 0.4.1-only symbol directly, so its own mcp_bundle floor stays ^0.4.0.Nothing published for this version
Nothing published for this version
Supersedes the misaligned 0.1.3 release. The logging primitives shipped in 0.1.3 conflated AppPlayer Core's own diagnostic logger with the MCP notific
Supersedes the misaligned 0.1.3 release. The logging primitives shipped in 0.1.3 conflated AppPlayer Core's own diagnostic logger with the MCP notifications/message log channel; this release re-architects them so a single in-app LogBuffer collects both sources, distinguished by LogEntry.source, and the MCP logging spec (notifications/message + logging/setLevel) is wired to its own routing path.
LogEntry now requires a source: LogSource (enum core / mcp). level field is now McpLogLevel (RFC 5424 8 levels — verbatim) instead of the 4-level LogLevel. Construct via LogEntry.fromCore(LogLevel) (4→8 mapping) or LogEntry.fromMcp({serverId, params}).LogBuffer.atLeast parameter changed from LogLevel to McpLogLevel. Added withSource(LogSource) filter.BufferLogger — Logger adapter that pushes records into a LogBuffer as source=core entries. Pair with a console adapter inside CompositeLogger so a single Core diagnostic call lands in DevTools (development) AND the in-app LogBuffer (field report).NotificationRouter routes notifications/message into a host-provided McpLogMessageHandler callback (serverId, params).AppPlayerCoreService.initialize(... onMcpLogMessage: ...) parameter.AppPlayerCoreService.setMcpLoggingLevel(serverId, McpLogLevel) — sends logging/setLevel so the server filters its own emission (server-side filter, spec-canonical).McpLogMessageHandler typedef and McpLogLevel (re-export from mcp_client) in the public barrel.Two log layers, one destination:
ConsoleLogger → dart:developer.log → DevTools / Console.app / logcat). Core diagnostics also flow there via CompositeLogger.BufferLogger (Core diagnostics) and onMcpLogMessage (server logs) both feed the same LogBuffer, distinguished by LogEntry.source.LogEntry — structured record (timestamp, level, message, context, error, stackTrace).
LogEntry — structured record (timestamp, level, message, context, error, stackTrace).LogBuffer — ChangeNotifier ring buffer (default 1000 entries) with scope/level filters. Tier shells (Pro / X / Custom) read this buffer to render in-app log viewers.ScopedLogger — Logger decorator that injects a fixed scope map (e.g. {serverId, handle}) into every log call's context, so downstream filters can isolate logs per connection/app.CompositeLogger — fan-out to multiple inner loggers (typical use: console adapter + LogBuffer adapter side-by-side).Core internal modules (ConnectionManager / ToolDispatcher / AppSession / NotificationRouter / ResourceSubscriber) are unchanged — composition roots inject a ScopedLogger and the existing _logger.debug(...) calls automatically carry the scope.
Note: This release misaligned the LogBuffer wiring with the MCP logging spec — see 0.1.4 for the corrected design (LogEntry.source, LogEntry.fromMcp, NotificationRouter
notifications/messagehandler,setMcpLoggingLevelAPI).
ToolDispatcher.call now returns Future (the decoded JSON response) instead of Future . Host self-fold removed; the runtime applies auto-merge against
ToolDispatcher.call now returns Future<dynamic> (the decoded JSON response) instead of Future<void>. Host self-fold removed; the runtime applies auto-merge against its own state.runtime parameter dropped from ToolDispatcher.call — no longer needed.AppSessionImpl._onToolCall returns the dispatcher's response so the runtime can fold it.flutter_mcp_ui_runtime: ^0.4.3 (carries auto-merge + event variable + errorBoundary/errorRecovery event.{error, stack} fixes).Upgraded mcp_client constraint to ^2.0.0. Public API of appplayer_core is unchanged — mcp_client is consumed internally and not re-exported.
mcp_client constraint to ^2.0.0. Public API of appplayer_core is unchanged — mcp_client is consumed internally and not re-exported.AppPlayerCoreService orchestrator owning connection lifecycle, sessions, bundle install pipeline, and tool dispatch.
AppPlayerCoreService orchestrator owning connection lifecycle, sessions, bundle install pipeline, and tool dispatch.AppSession, DashboardSession, AppHandle.ConnectionInfo, ConnectionResult, ConnectionState, ConnectionHealthMonitor with HealthMonitorConfig.BundleRef, BundleEntryPoint, BundleFetcher, InstalledAppBundle.DashboardBundleRef, BundleSource, SlotDefinition, SlotBindingRule.AppsRegistry + RegistryMetadataSink automatic metadata refresh.TenantContext, TenantSource for multi-tenant variants.ServerStorage, CredentialVault, AppMetadataSink.Logger, MetricsPort.flutter_mcp_ui_runtime — FormFactor, FormFactorScope, ViewMode/ViewModeResolver, AppSpacing / AppIconSizes / AppTypography / AppDensity (and their scale companions), TrustLevel, TrustLevelManager.MCPUIDSLVersion from flutter_mcp_ui_core.app_activity.dart.Your coding agent can read these notes before it upgrades. Set up the MCP server →