appplayer_core
AppPlayer Core — shared Flutter library for MCP server connection, bundle handling, and UI runtime orchestration.
0.1.26
1.0K downloads/mo
#578 most downloaded on pub.dev
app-appplayer/appplayer_core
What this package is like to depend on
Last release 11 days ago
13 Aug 2026
Ships fairly regularly
a new release about every 2 weeks
Nearly every release is documented
notes for 25 of 27 stable releases
Nothing withdrawn
no release was ever pulled
3 months old
27 releases · first in 2026
27 releases in the last 12 months
see the full history below
Release timeline
27 releases · Apr 2026 to Aug 2026Releases
latest 27-
0.1.2613 Aug 2026Release notes
Open source →Fixed
-
Debug MCP
typeTextwrites throughEditableTextState.updateEditingValue, so the field'sonChangedruns. It previously assignedcontroller.value, which does not fire it — bindings written fromonChangedstayed empty. The response carriesasKeystroke, false when the field had no input connection and the value had to be assigned. AreadOnlyfield is now a no-op. -
An unregistered tool with no connected server raises
ToolExecutionExceptioninstead of returningnull. The runtime reads a null return as a successful call with no payload, so a misspelled tool name reachedonSuccess. -
An in-process tool that reports failure in its payload (
{ok: false, code, error}— the kernelmcp.*andbk.*shape) is handed to the runtime as an error result, matching what the external endpoint already did. The two routes disagreed, so a failedmcp.connectresolved in-process firedonSuccesswith the error as its payload.
Added
ui.taptakeslongPress(600 ms),holdMsandbutton(primary|secondary). The response echoes both; an unknown button is refused.
Changed
- Floors:
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 a0.xminor 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.
-
-
0.1.2508 Aug 2026Release notes
Open source →No source change.
flutter_mcp_ui_core ^0.6.3andflutter_mcp_ui_runtime ^0.7.4: a caret bound on a0.xminor cannot reach the next one, so without this a host keeps resolving the previous runtime and never sees the fix for a boundnetworkGraph.nodesthat arrives after the first frame. -
0.1.2408 Aug 2026Release notes
Open source →The runtime's
ThemeManageris 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.
-
0.1.2307 Aug 2026Release notes
Open source →app.bundles/app.openon 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.openroutes through the shell instead: the core knows what is installed, the tier suppliesAppPlayerCoreService.debugOpenBundleand 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>. -
0.1.2207 Aug 2026Release notes
Open source →ui.dragon 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.bundleRootPathreaches the media path. A bundled sound arrives as afile:reference after the core rewritesbundle://, 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 reconnectloggedcount: <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.- The line now carries
resubscribedandfailedseparately. reattachreturns aReattachResultinstead of nothing, so the two numbers are assertable rather than only printable.isTotalFailureis the case that matters — the connection is up and every stream on it is dead.appplayer_corewarns 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.
- The line now carries
-
0.1.2106 Aug 2026Release notes
Open source →ConnectionHealthMonitorgave a server up for good once its attempt count ran out. The count only clears when a health check observesconnected, 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.
checkIntervalwas 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.- The two cadences are now separate.
checkIntervalpaces detection only; retries pace themselves with an exponential backoff (reconnectDelaydoubling up to the newmaxReconnectDelay, default 30s). maxReconnectAttemptsdefaults to0= 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.- One reconnect in flight per server: a fast sweep no longer stacks a dial on top of the one already waiting out its backoff.
startMonitoring()clears grown backoff (a foreground return is new information),stopMonitoring()stands down a reconnect already waiting, and a wake-drivensweepStale()resets the backoff for what it sweeps.- An app open on a server does not back off at all. The backoff exists to stop
dialling a server nobody is looking at; a full-screen app is the explicit
statement that this connection is supposed to exist, and the user is watching
it fail. Engaged servers retry at the first interval (Pro: every ~1s) for as
long as the app is open, and an explicit host cap does not strand them either.
Hosts declare this through the new
isEngagedseam;appplayer_corewires it to "a runtime is open forAppHandle.server(id)". A dashboard tile is deliberately NOT engaged — treating every tiled server that way would dial the whole home screen every second. - That fixed interval is fixed against the clock, not against the sweep. An
engaged retry re-arms from inside its own chain; leaving the next attempt to
the health sweep made the real spacing
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. - The "max attempts reached" warning is logged on the transition instead of on every tick — at a 2s cadence it was filling the log.
Behaviour change, intended: every tier now retries indefinitely while foreground. Nothing to change at call sites —
maxReconnectDelayis 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. - The two cadences are now separate.
-
0.1.2005 Aug 2026Release notes
Open source →No source change in this package. The floors move because a caret bound on a
0.xminor cannot reach the next one, so without this bump a consumer ofappplayer_corekeeps resolving the previous runtime and never sees the cut:flutter_mcp_ui_core— the registry narrows in four places (retired legacy enum spellings onlinear.distributionandqrCode.errorCorrection, theotpInput.autoSubmitproperty, and a requiredvalueon option objects), which is what makes that release a minor rather than a patch.flutter_mcp_ui_runtime— vector assets (SVG) draw in everyAssetRefslot includingicon, union-typed slots read every branch they declare (Dimensionobjects, action lists, bindings in enum andEdgeInsetsslots), ink overlays paint above what the document paints, and ten declared properties gained implementations. Bringsflutter_svgtransitively.
Consumers that pin
appplayer_coreneed only this bump; the surface they compile against is unchanged. -
0.1.1903 Aug 2026Release notes
Open source →Floors
flutter_mcp_ui_core ^0.4.3 → ^0.5.0andflutter_mcp_ui_runtime ^0.5.3 → ^0.6.0.Both are minor because the schema narrows:
AssetRefslots reject a bare string carrying no scheme, the icon slots take the newIconRef, and thirteen string properties that documented their values in prose now declareenum. A caret bound on^0.5.xcannot 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
AssetRefslot (image/avatar/icon/box.decorationconverge, andbundle://andclient://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. -
0.1.1802 Aug 2026Release notes
Open source →Added
AppPlayerCoreService.installBundleFromBytes— install from.mcpbbytes already in hand. A host that fetched the archive itself has bytes and never a path;installBundleFromFileis the same call with a read in front of it.AppPlayerCoreServicebundleInstallStore:— install into and read installed bundles from host-provided storage instead of a directory. When given,bundleInstallRootis not used as one.BundleInstallerAdapter.onStoreandBundleLoaderAdapter(installStore:)— the same seam one layer down.
Changed
BundleApplicationAdapterrequires the bundle to carry readable files, not a filesystem directory. A bundle installed into host storage now adapts; previously it was refused outright withBundleAdaptException(unsupportedEntryPoint)before anything was read.- JS tool entry scripts are read through the bundle's own file surface
rather than
File(<directory>/<entry>), so bundles that carry JS tools work on hosts with no filesystem.
Desktop and mobile behaviour is unchanged — omit
bundleInstallStoreand the filesystem path is taken exactly as before.Fixed
- An app left open across a background round trip stopped streaming. A
subscription and a notification handler both live on the CONNECTION, and
mobile tears the connection down in the background and rebuilds it with a
NEW client on return. Tool calls kept working (those resolve the live
client per call), so the screen looked healthy while only the stream was
dead — and pressing Subscribe again did nothing, because the runtime
binding had never been lost and there was nothing left for the runtime to
do. Re-attached on the new client:
ConnectionManager.onClientAttached— hook fired whenever a server's client is replaced (first connect included).ResourceSubscriber.reattach— re-issues the wireresources/subscribefor every URI recorded under anownerKey, 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.AppPlayerCoreServicewires 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).
-
0.1.1702 Aug 2026Release notes
Open source →Fixed
-
initializethrewUnsupported operation: Platform._operatingSystemon the web and took the whole host down before the first frame — a blank page. One line built theLifecycleCoordinatorwithplatformSuspends: Platform.isAndroid || Platform.isIOS, andPlatformisdart:io. It is now!kIsWeb && (...).falseis 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 butinitializehardcoded 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.
Verified
Against the real app, both directions.
appplayer_cloudin 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 namingapp_player_core_service.dart 621:34. Same app, same browser, one line different.Not in this release — the browser regression
A
@TestOn('browser')boot regression is written (test/integration/web_boot_test.dart) and is skipped, because it hangs: headless Chrome loads andinitializenever completes there, while the sameinitializecompletes 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 callsinitializeruns on the VM, which is why adart:iocall sat on the boot path unnoticed. -
-
0.1.1630 Jul 2026Release notes
Open source →Changed
- The per-bundle JavaScript runtime now resolves per platform.
JsToolIsolatebecame a conditional export: the existing embedded-engine implementation on platforms withdart:io, and a Web Worker implementation elsewhere. The native implementation is the same code, relocated tojs_tool_isolate_io.dart— no behavior change off the web. - The JS-side host bridge contract moved to
src/js/js_bridge_protocol.dartand is shared by both branches:host.<atom>.<verb>()returns a Promise resolved through__hostResolve/__hostRejectexactly as before. Only the transport differs — an isolate port natively,postMessageon the web — so a bundle's JavaScript behaves identically on both.
Added
- Web branch of the JS tool runtime. Bundle JavaScript runs in a Web Worker, never on the page: a Worker has its own global scope and no DOM, so the bundle cannot reach the document, application state, storage or cookies. Main-thread evaluation is not offered as an alternative path. Everything crossing the boundary is a JSON string, so a bundle cannot hand the host a live JavaScript object; a dispatcher error becomes a rejected Promise on the JS side rather than a runtime failure.
webdependency, used only by that branch.
Fixed
- The shared bootstrap parenthesises the transport call. A bare
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 insideflutter 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.
Notes
- Hosting requirement for the web branch: the Worker is created from a blob, so
the page policy must allow
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. - This lifts
dart:ffiout of the web dependency graph, which is what stoppedflutter build webfor any application depending on this package.
- The per-bundle JavaScript runtime now resolves per platform.
-
0.1.1529 Jul 2026Release notes
Open source →Added — entry context on the open paths (platform spec 19 §4.3, MCP UI DSL §8.9)
-
openAppFromServer(..., entry:, identity:)andopenAppFromBundle(..., entry:, identity:)— carry how an app was reached and who is looking at it. Anentrynaming a route opens the app on that page instead of its owninitialRoute, 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 onlyappplayer_corecan 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.- The absence of a source is meaningful. A host with no mechanism supplies none, and that turns the obligation into an offer of manual recovery rather than silence. A source that answers "this install did not begin at an entry" produces nothing instead — prompting there would be a question about something that never happened.
- A mechanism that threw is treated as a mechanism we do not have.
- The launch is marked seen either way: a recovery offered twice is a nag, and a code recovered twice would reopen the same entry on a launch nobody connected to it.
-
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.localServerneeds a discoverer the tier wires (discovery is a host capability); without one the entry fails visibly rather than dialling something else. Alistingdeliberately 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 acceptevil-entry.example.testforentry.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,EntryTargetand 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 arequiredentry 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.- Enforced here because each of these failures is invisible from the outside: a stale answer is never replayed (custody may have changed since it was minted), a guest entry never resolves to an account-gated target, and an entry this host cannot open is reported rather than substituted — a silently swapped target looks exactly like a working one.
- Wire parsing defaults to the safe reading: an unknown
statusis notok, and an unparsedidentityPolicydemands identity rather than assuming guest. Guessingopenwould render a guest surface for an entry we failed to understand; guessingrequiredmerely 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:alongsideentry:on both open paths. An in-app open (DSL §4.3.1navigation.openApp) names a page without being an arrival, so it sets the route and leaves the document'sentry.*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.
Added
openSavedDeviceAsOrigin(id)— opens a saved device as a composition origin through the sameConnectionManagerthe launcher uses, andadoptConnectionAsOrigin({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 withTransport disconnected. One connection per device now, shared by both.
Fixed
- Opening a device's own screen silenced every composed tile watching the same device — permanently, and with nothing to show for it: the subscription stayed live, the socket stayed up, and closing the screen did not bring it back.
Client.onNotificationkeeps 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 throughSharedClientNotifications, 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. - A composed tile re-read the device's UI document on every mount. The connection was never dropped — the socket stayed up across open and close — but entering the screen again cost
ui://app+ui://page/mainover 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 thecomposition_hostrecipe and re-vendored.) - Composition treated "the host lists this connection id" as "the origin is
open". A connection whose link had gone was still listed, so a composed tile
decided it had nothing to open and never asked again while every call it made
landed on a dead link — the device worked when opened on its own and never
appeared in a multi-device screen. Openness is now decided by liveness.
(Fixed in the
composition_hostrecipe and re-vendored.)
Added
registerDefinitionResolver(resolve)— the host resolver behind aview/ routeDefinitionSourcethat names an origin. Registering it is how this host claims the Composition Profile; without itviewfails closed and renders itsfallbackrather than resolving a foreign$refagainst 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 outboundmcp.*surface. Platform spec06-tool-registry.mdalready declares "the app/bundle drivesmcp.*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].textcarrying escaped JSON) and accepts an already-decoded map.- The registered resolver is applied to every app/bundle runtime at creation, alongside the existing stream sources.
openOriginhook — 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.- Origin-scoped acting and watching — the resolver wiring now also installs a tool caller and a resource watcher for a named origin. Rendering and acting are separate halves and only the first existed: a composed screen drew each device's UI while every control in it reached a session with no client for that device, and a live reading rendered its label and never a value. Tool calls go out as
mcp.call_toolon the named connection; a watch reads the current value once (a subscription reports only changes) and then followsnotifications/resources/updated.
Security
- Fails closed by design: an empty origin with no
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).
Changed
flutter_mcp_ui_runtimefloor raised^0.5.2 → ^0.5.3(theviewwidget +registerDefinitionResolverseam).brain_kernelfloor raised^0.1.8 → ^0.2.0(resource subscription on a kernel connection).
-
-
0.1.1421 Jul 2026Release notes
Open source →Additive (0.x → patch). No public API removed.
- Durable reconnect —
ServerReGrantseam. A marketplace server's credential is a short-lived per-userconnectionTokenbaked intoServerConfig.transportConfig. When a connect attempt fails and the server carries a bearer token,ConnectionManagernow 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()andConnectionHealthMonitorall funnel throughconnect(), so all three are covered.- New:
typedef ServerReGrant = Future<ServerConfig?> Function(ServerConfig stale)(barrel-exported),ConnectionManager.tokenReGrant(mutable, optional),AppPlayerCoreService.serverReGrantsetter (host wires it after the marketplace session exists). - Fully backward-compatible: when no hook is wired (or the server carries no bearer token) connect/reconnect behave byte-for-byte as before — static-token, hand-typed and discovered (tcp/ble/serial) servers are untouched.
- New:
- Doc hygiene: removed dangling doc/spec references (
docs/,specs/,spec §N) from source comments so nothing points outside the published package. No code change.
- Durable reconnect —
-
0.1.1320 Jul 2026Release notes
Open source →Additive across the tracks landed since 0.1.12.
^0.1.12consumers pick these up on floor-bump; no public API removed.Added
- Flutter-plugin promotion (Platform Integration Foundation, FR-PLATFORM) —
appplayer_coredeclares aflutter: plugin:with native Android/iOS adapters (background execution, OS permission, notification); desktop/web degrade to the Dart ports' NoOp.onLifecyclePhase(AppLifecyclePhase)drives the foundation. - Metadata-only install —
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). - Single-route application wrapping —
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. - Debug MCP host — opt-in (
enableDebugMcp, settings-gated, non-web) MCP server on127.0.0.1:<port>/mcpexposingui.screenshot/ui.tree/ui.tap/ui.typefor test automation;debugCaptureWrap(child)gives the capture/tap primitives a stable render boundary. - Connection continuity — transport-drop handling (
_handleTransportDropwithDisconnectReason) +keepAliveSweep(...)for reconnect/resume.
Dependencies
- Floors raised to current latest at cut time:
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.
- Flutter-plugin promotion (Platform Integration Foundation, FR-PLATFORM) —
-
0.1.1214 Jul 2026Release notes
Open source →Fixed
AppSession.buildWidget/buildDashboardWidgetrebuild 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 baredefaultLight()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.- The rebaseline skip-gate verifies the singleton's FINGERPRINT (not just an
ownership tag): runtime teardown resets the ThemeManager behind the
session's back (
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.
-
0.1.1113 Jul 2026Release notes
Open source →Fixed
- streamableHttp transport carries
accessTokenasAuthorization: Bearer(+headerspassthrough, explicit header wins); it was dropped entirely, so token-gated servers rejected the handshake (401 → "Transport disconnected"). - install/uninstall invalidate the bundle's session runtime + metadata caches (FR-INSTALL-009) — a reinstall/update no longer keeps rendering the old definition until app restart.
Changed
- Floor
mcp_client ^2.0.1(spec-optionaldescriptionparse fix).
- streamableHttp transport carries
-
0.1.1012 Jul 2026Release notes
Open source →Changed
AppPlayerCoreService.connectExtensionTransportnow delegates to thebrain_kernelcoreconnectExtension(clientHost, …)helper off the abstractKernelClientHost, dropping the redundant concreteMcpClientKernelHostfield/ref and the inline probe-and-cast (theis-no-promotion footgun is now sealed inside the kernel helper). Behaviour unchanged.- Floors
brain_kernel ^0.1.2 → ^0.1.7(theExtensionTransportConnectcapability interface +connectExtensionhelper). No API change to appplayer_core's own surface.
-
0.1.921 Jun 2026Release notes
Open source →Added
AppPlayerCoreService.registerCapabilityTools(tools)— additive public seam to register host-supplied in-process capability tools (e.g. a desktopio.*process/device tool-pack) after boot. The core depends on no capability package, so platform-specific adapters (dart:ioprocess execution, etc.) stay in the host layer; the tools share the same in-process dispatcher as the standardbk.*/mcp.*surface. Safe to call more than once.
-
0.1.816 Jun 2026Release notes
Open source →Added
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 toappplayer_core. The transport is built by the calling app (e.g. using classes exported frommcp_bridge) and injected here; the core routes it throughMcpClientKernelHost.connectWith(brain_kernel 0.1.2). Returns aKernelClientConnectionwhosecallTool/readResource/listToolsreach the remote server. ThrowsStateErrorwhen the kernel is not booted.
Changed (dependency floor)
brain_kernel^0.1.1→^0.1.2—connectExtensionTransportdelegates toMcpClientKernelHost.connectWithand re-exportsclientTools, both new in brain_kernel 0.1.2. The floor guarantees these symbols are present.
Backward compatibility
- Fully additive. No existing
AppPlayerCoreServicemethod or constructor changed. Apps that do not use extension transports see no behavior change.
Tests
- 302/302 (301 + 1 skipped) PASS.
analyze0 issues.
-
0.1.701 Jun 2026Release notes
Open source →- BrainBridge removed (phase D · 2026-05-24) — 451 lines + 6 facade wrappers + 2 tests dropped.
AppPlayerCoreServicenow callsKernelApp.boot(...)directly, registersstandardTools(app), and delegatessetActiveBundle/scopeIdFor. Zero external-shell cascade. - bundle session bridge wiring (2026-05-25) —
BundleSessionBridgelifecycle wired at 5 points ofAppPlayerCoreService(boot / activate / onClose / closeApp / dispose)._sessionsis a per-bundleId map.McpAtom+AgentAtomgain optionalbridge/sessionarguments and wrap dispatch inbridge.runScoped(session, ...). - MCP serving (MCP Serving 1.0) —
_activateBundleSectionsexposes the active bundle at the well-knownbundle://manifest.jsonresource (shared by the local-bundle and served-bundle paths).openAppFromServerreconstructs a served bundle: it detects the document, parses it withMcpBundleLoader.fromJson, and runs the same kernel activation a local bundle uses (knowledge / settings / behavior come live); tool execution stays remote and the UI loads viaui://app. NewservedResources/readServedResourceaccessors.ApplicationLoader.loadgains an optionalresourcesparameter so the server is listed only once. - import unification — the bridge ships inside the kernel (
brain_kernel/lib/src/system/bridge/), so hosts import onlypackage:brain_kernel/brain_kernel.dart. - analyze cleanup — removed 5
unnecessary_importhints acrosstest/src/dashboard/dashboard_bundle_test.dart+test/src/session/app_session_impl_test.dart(info → 0).
Changed (dependency floor)
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 raisesmcp_bundleto0.4.1; appplayer_core references no 0.4.1-only symbol directly, so its ownmcp_bundlefloor stays^0.4.0.
Tests
- 301 + 1 skipped PASS · analyze 0 issues.
- BrainBridge removed (phase D · 2026-05-24) — 451 lines + 6 facade wrappers + 2 tests dropped.
-
0.1.606 May 2026Nothing published for this version
-
0.1.502 May 2026Nothing published for this version
-
0.1.401 May 2026Release notes
Open source →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/messagelog channel; this release re-architects them so a single in-appLogBuffercollects both sources, distinguished byLogEntry.source, and the MCP logging spec (notifications/message+logging/setLevel) is wired to its own routing path.Changed (breaking — 0.1.3 → 0.1.4)
LogEntrynow requires asource: LogSource(enumcore/mcp).levelfield is nowMcpLogLevel(RFC 5424 8 levels — verbatim) instead of the 4-levelLogLevel. Construct viaLogEntry.fromCore(LogLevel)(4→8 mapping) orLogEntry.fromMcp({serverId, params}).LogBuffer.atLeastparameter changed fromLogLeveltoMcpLogLevel. AddedwithSource(LogSource)filter.
Added
BufferLogger—Loggeradapter that pushes records into aLogBufferassource=coreentries. Pair with a console adapter insideCompositeLoggerso a single Core diagnostic call lands in DevTools (development) AND the in-appLogBuffer(field report).- MCP logging spec wiring (MOD-RUNTIME-005a, NFR-OBS-006~007):
NotificationRouterroutesnotifications/messageinto a host-providedMcpLogMessageHandlercallback(serverId, params).AppPlayerCoreService.initialize(... onMcpLogMessage: ...)parameter.AppPlayerCoreService.setMcpLoggingLevel(serverId, McpLogLevel)— sendslogging/setLevelso the server filters its own emission (server-side filter, spec-canonical).McpLogMessageHandlertypedef andMcpLogLevel(re-export frommcp_client) in the public barrel.
Rationale
Two log layers, one destination:
- Development uses OS standard log pipelines (host
ConsoleLogger→dart:developer.log→ DevTools / Console.app / logcat). Core diagnostics also flow there viaCompositeLogger. - Field reports require an in-app surface that production users can export when filing an issue.
BufferLogger(Core diagnostics) andonMcpLogMessage(server logs) both feed the sameLogBuffer, distinguished byLogEntry.source.
-
0.1.301 May 2026Release notes
Open source →Added
LogEntry— structured record (timestamp, level, message, context, error, stackTrace).LogBuffer—ChangeNotifierring buffer (default 1000 entries) with scope/level filters. Tier shells (Pro / X / Custom) read this buffer to render in-app log viewers.ScopedLogger—Loggerdecorator 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
ScopedLoggerand 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).
-
0.1.201 May 2026Release notes
Open source →Changed
ToolDispatcher.callnow returnsFuture<dynamic>(the decoded JSON response) instead ofFuture<void>. Host self-fold removed; the runtime applies auto-merge against its own state.runtimeparameter dropped fromToolDispatcher.call— no longer needed.AppSessionImpl._onToolCallreturns the dispatcher's response so the runtime can fold it.- Runtime dependency raised to
flutter_mcp_ui_runtime: ^0.4.3(carries auto-merge +eventvariable + errorBoundary/errorRecoveryevent.{error, stack}fixes).
-
0.1.130 Apr 2026Release notes
Open source →Changed
- Upgraded
mcp_clientconstraint to^2.0.0. Public API of appplayer_core is unchanged — mcp_client is consumed internally and not re-exported.
- Upgraded
-
0.1.029 Apr 2026Release notes
Open source →Added
AppPlayerCoreServiceorchestrator owning connection lifecycle, sessions, bundle install pipeline, and tool dispatch.- Session abstractions —
AppSession,DashboardSession,AppHandle. - Connection observability —
ConnectionInfo,ConnectionResult,ConnectionState,ConnectionHealthMonitorwithHealthMonitorConfig. - Bundle handles and host ports —
BundleRef,BundleEntryPoint,BundleFetcher,InstalledAppBundle. - Dashboard bundle composition —
DashboardBundleRef,BundleSource,SlotDefinition,SlotBindingRule. - Apps registry —
AppsRegistry+RegistryMetadataSinkautomatic metadata refresh. - Tenant model —
TenantContext,TenantSourcefor multi-tenant variants. - Host ports —
ServerStorage,CredentialVault,AppMetadataSink. - Observability ports —
Logger,MetricsPort. - Re-exports from
flutter_mcp_ui_runtime—FormFactor,FormFactorScope,ViewMode/ViewModeResolver,AppSpacing/AppIconSizes/AppTypography/AppDensity(and their scale companions),TrustLevel,TrustLevelManager. - Re-export of
MCPUIDSLVersionfromflutter_mcp_ui_core. - Active-state extension via
app_activity.dart.