appplayer_core 0.1.15
appplayer_core: ^0.1.15 copied to clipboard
AppPlayer Core — shared Flutter library for MCP server connection, bundle handling, and UI runtime orchestration.
0.1.15 - 2026-07-28 - Multi-origin composition (MCP UI DSL v1.4 Composition Profile) #
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.14 - 2026-07-22 - Durable reconnect (token re-grant seam) #
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.
0.1.13 - 2026-07-18 - Flutter-plugin promotion · metadata-only install · single-route apps · debug MCP · connection continuity #
Additive across the tracks landed since 0.1.12. ^0.1.12 consumers 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.
0.1.12 - 2026-07-14 - Theme state rebuilt on every app entry #
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.11 - 2026-07-13 - Marketplace connect credential + bundle cache invalidation #
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).
0.1.10 - 2026-07-12 - Extension-transport seam conformance (additive) #
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.9 - 2026-06-22 - Capability tools registration seam (additive) #
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.8 - 2026-06-10 - Extension transport connection seam (additive) #
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.7 - 2026-06-01 - brain_kernel KernelApp + bundle session bridge integration + MCP serving #
- 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.
0.1.6 - 2026-05-04 - flutter_mcp_ui 0.4.0 / 0.5.0 + mcp_bundle 0.3.1 alignment #
- Bumps
flutter_mcp_ui_coreto^0.4.0andflutter_mcp_ui_runtimeto^0.5.0for the spec 1.3.3 alignment cycle. - Bumps
mcp_bundleto^0.3.1(cascade: EthosRecord JSON round-trip,BundleFolder.agentsreserved,AgentsSection/PhilosophySectionmodels,EthosStorePortextensions).
0.1.5 - 2026-05-03 - flutter_mcp_ui 0.3.2 / 0.4.4 alignment #
Changed #
- Bump
flutter_mcp_ui_coreto^0.3.2andflutter_mcp_ui_runtimeto^0.4.4for the M3 token shorthand consumption layer (text.variant,box.padding,card.shape/elevation,button.elevation,icon.size/sizeToken),FormFactorScopetoken tracking, opt-in per-form-factor property override, andboxflat-form constraint properties.
0.1.4 - 2026-05-02 - Field-report logging stack — corrects 0.1.3 wiring #
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.
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.3 - 2026-05-02 - Logging primitives (LogEntry / LogBuffer / ScopedLogger) #
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 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).
0.1.2 - 2026-05-01 - Tool dispatcher align with runtime 0.4.3 #
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.1 - 2026-04-30 - mcp_client 2.0 dependency #
Changed #
- Upgraded
mcp_clientconstraint to^2.0.0. Public API of appplayer_core is unchanged — mcp_client is consumed internally and not re-exported.
0.1.0 - 2026-04-28 - Initial Release #
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.