flutter_mcp_ui_runtime 0.5.1
flutter_mcp_ui_runtime: ^0.5.1 copied to clipboard
Runtime for building dynamic Flutter UIs from JSON with lifecycle management, state handling, and MCP protocol support.
0.5.1 - 2026-05-23 — spec compliance Round 2 + mcp_bundle 0.4.0 cascade #
Changed (cascade) #
mcp_bundlecaret bumped from^0.3.0to^0.4.0. The downstreamUiSection.pagesfield switched toMap<String, PageDefinition>; this runtime'sbundle/bundle_page_adapter.dartnow iteratesuiSection.pages.valuesso bundle activation still walks every page. Consumers should bump to^0.5.1.flutter_mcp_ui_corecaret bumped to^0.4.1(mcp_bundle cascade).
Added #
- MCP wire shape unwrap — tool responses shaped as
{content:[{type:'text', text:S}], isError:bool}now unwrap and JSON parseSbefore auto-merge (spec §3.10). StateChangeEvent.sourcecanonical enum (action/tool/subscription/system) per spec §3.11.mergeStatedefaults to'tool',StateActionExecutorto'action', resource notification to'subscription',updateAllto'system'.RenderContext.onResourceRead/onResourceList— separate host callbacks for spec §4.5read/listsub-actions (fallback toonResourceSubscribefor backward compat).onSubscriptionErrorcallback — spec §4.5 named field, dispatched when host subscribe handler throws.event.{uri, binding, message}child context.- Non-Map response
event.valuewrap — spec §4.4.2: list / scalar / null tool responses exposeevent.value = <response>(with otherevent.*keys null). - Common
clickfield auto-wrap (spec 1.3.4 §2.2) —WidgetFactory.applyCommonWrappersnow wraps any widget carrying aclick: Actionproperty in aGestureDetectorand dispatches the action throughRenderContext.actionHandleron tap. Universal across all 97 factories that callapplyCommonWrappers; pure layout / decoration widgets (box,card,linear,stack, ...) become tappable without nesting agestureDetectorwrapper.clickis resolved through the binding engine, so action maps may be supplied inline or via{{...}}binding. Click is applied BEFORE the enabled-state wrap soenabled: false(IgnorePointer) correctly suppresses the gesture surface. Widget-local activation slots (button.onTap,iconButton.onTap,richText.spans[].onTap, ...) remain canonical for those widgets and are unaffected. - 26 new regression cases under
test/spec_compliance/runtime_spec_fix_2026_05_14_test.dartlocking the above. - 6 new regression cases under
test/renderer/widget_factory_test.dartfor the commonclickfield: GestureDetector wrap shape, end-to-end tap → action dispatch, tooltip + click coexistence, backward-compat no-click pass-through, non-Map click ignore, andenabled:falsesuppression via IgnorePointer.
Deprecated #
- Envelope
{success, result, message}auto-unwrap — slated for removal in 0.6.0. Warning logged once per process. tools.<tool>.resultnamespaced mirror — slated for removal in 0.6.0. Warning logged once per process.
Changed #
runtime_engine.handleResourceNotification/handleMCPNotificationno longer applycontent[binding]heuristic — raw content stored at binding per spec §4.5.- Lifecycle
_renderContextnull short-circuit replaced with explicit error log.
Fixed #
- Tool responses arriving in MCP wire shape were merged as
content/isErrorliteral keys instead of unwrapped — now spec §3.10 compliant. TemplateParamDefinition.validatenow skips declared-type and enum checks when the supplied argument is a binding expression ("{{...}}"). Mirrors the same fix influtter_mcp_ui_core'sTemplateDefinition.validate; covers the extended-template (resolveExtended) path. Previously a template param declaredtype: booleanrejected every expression-bound argument (always a String at validate time), causinguseinvocations to surface theTemplate not found:placeholder. Spec §9.3.1 mandates only required / default / enum / validator and expressions resolve at runtime so cannot be compared against the enum list here either. Non-expression arguments still take the type / enum path unchanged. Regression: full templates test suite 88/88.TemplateRegistry._substituteValuenow type-preserves whole-value placeholders. A param value shaped as a single"{{name}}"(with no surrounding literal text) was previously stringified by_substituteStringviavalue?.toString()— a List param landed in the expanded template body as"[a, b]", a Map as"{x: 1}", an action Map ({type:"tool",...}) as"{type: tool, ...}". Downstream factories receiving those props could no longer cast back toList/Map<String, dynamic>?(observed runtime:'String' is not a subtype of type 'Map<String, dynamic>?'frominkWell.onTapwhen atemplate params.onActivateaction Map was forwarded into a nesteduse). The substitution now short-circuits whole-value placeholders to the raw param value; partial placeholders ("Hello {{name}}") still take the string path unchanged.ListViewWidgetFactory._ensureStableConstraintsis now orientation-aware. The defensive guard that wraps the ListView in aSizedBoxwhen the parent supplies an unbounded constraint previously only checkedhasBoundedHeight, so alistwithorientation: 'horizontal'mounted inside aRowwith aSpacer/Expandedsibling threwHorizontal viewport was given unbounded widthat layout time (followed by a cascade ofRenderBox was not laid outexceptions). The guard now branches onscrollDirection: horizontal lists checkhasBoundedWidthand fall back toSizedBox(width: MediaQuery.size.width); vertical lists keep the existinghasBoundedHeight/SizedBox(height: ...)path. Author intent is unchanged — explicitshrinkWrap: trueor a parent-supplied size still short-circuits the guard.textwidget pinstheme.color.onSurfacewhenstyle.coloris unspecified. Spec §5.4.2 deliberately omits acolorfield on typography roles (Material 3 separates typography from colour), sovariant-only / no-style.colortext returnedTextStyle(color: null)and Flutter fell through to the ambientDefaultTextStyle.of(context).color— which inherits from an ancestorThemewhose brightness can briefly diverge from the ThemeManager's effective mode during host tab transitions (an AppRendererScreen remount sees a staleMediaQuery/Theme(brightness:)frame before the host wrap re-applies its override). Visible in dark mode only — both ambient branches yield near-black under light, so the divergence is invisible there; in dark the same race surfaced as black-on-dark text frames after tab cycling. The factory now resolvestheme.color.onSurface(the M3 canonical text colour) through ThemeManager at build time, breaking the ambient dependency. Author-suppliedstyle.colorstill wins via the existingmergepath. Regression: 2 new widget cases intest/runtime/text_color_pin_test.dart(onSurface pinned despite light-Theme ancestor / inline style.color overrides the pin).ThemeManager.flutterThemeModenow honours_hostBrightnessOverrideunconditionally (in lockstep with_resolveEffectiveMode). Previously the override only applied when the bundle declaredmode: 'system', so a bundle withmode: 'dark'would resolvetheme.color.onSurfaceto the host-pinned brightness viagetColorValuewhileMaterialApp.themeModestill picked dark — the colour-token path and the ambientColorSchemecame from different brightnesses, producing the "ambient onSurface flips between frames" race the tab-cycle text fix above is the second half of. Regression: 5 new cases intest/runtime/text_color_pin_test.dart::flutterThemeMode honours host override unconditionally. Testtheme/theme_manager_test.dart::TC-TH-03 flutterThemeMode ignores host brightness ...was inverted to lock the new contract (renamed to... honours host brightness ...) — policy change 2026-05-21, AppPlayer-class hosts are themselves "the system" for embedded bundles.ThemeManager._resolveEffectiveModenow treats a non-null_hostBrightnessOverrideas the unconditional winner — previously the override only applied when_themeMode == 'system', so a bundle that hard-declaredtheme.mode: 'dark'ignored the host's light/dark toggle entirely. Worse, the toggle's apparent effect depended on race-timing betweensetTheme(appDef.theme)(driven by the bundle's manifest at mount) andsetHostBrightness(...)(driven by the host's chrome preview-mode pin) — whichever landed last won, producing an intermittent "text widget stays dark / surrounding chrome flips light" leak intools/builder/vibe_studio/vibe_studio_workspace.setHostBrightnessalso now notifies unconditionally (the previousif (_themeMode == 'system') notifyListeners()guard would swallow the brightness change when a bundle declared an explicit mode). AppPlayer-class hosts are themselves "the system" for embedded bundles — when the host pins brightness, the bundle MUST follow. The identical-override early-return guard (_hostBrightnessOverride == brightness) is preserved, so no spurious rebuild. Regression: 6 new cases intest/runtime/theme_host_override_priority_test.dart(light flips dark bundle / dark flips light bundle / clear restores declared / always-notify / identical no-op / fingerprint changes for renderer cache invalidation).RuntimeEngine.initializenow forwardsThemeManagermutations to engine listeners.setTheme/setThemeDefinition/setThemeMode/setHostBrightness/applyOverridecalls already invokedThemeManager.notifyListeners()(six call sites), but the engine neveraddListenered on_themeManager, so the notification stopped at the manager and widgets never rebuilt on a theme mutation. Hosts worked around this by bridging through_stateManageras a noop forward (sentinel:tools/builder/vibe_studio/.../dsl_workspace_view.dart). Specmcp_ui_dsl/spec/1.3/05_Theme.md§L56 ("theme.mode … changes trigger theme recomputation and re-render") makes this re-render obligatory; the engine now satisfies it natively. The listener tear-off (_themeListener = notifyListeners) is held on the engine and detached indestroy()— ThemeManager is process-singleton, so an unreleased listener would outlive a destroyed engine and firenotifyListenerson a disposed receiver. Multi-host theme isolation (per-runtime ThemeManager) is unrelated to this fix and remains a separate refactor track. Regression: 3 new cases intest/runtime/theme_forward_test.dart(theme mutation forwards / destroy detaches cleanly / host bridge workaround is no longer necessary).
0.5.0 - 2026-05-03 - Spec ↔ implementation alignment (1.3.3) #
- Channel callbacks:
onMessage(canonical,onDataretained as getter alias) + newonConnect/onDisconnectdispatch. chartadds donut / polar / bubble;codeEditoraccepts 7 themes (vsLight/vsDark/monokai/solarizedLight/solarizedDark/github/dracula) + 14 languages.lazy.trigger: visible(spec rename fromviewport);manualcase recognised.services.kind: polling/subscriptionaccepted (mapped to existingperiodic/continuous).template.stylesmap field;bottomBar/railnavigation canonical (legacybottomNavigation/bottomaliases retained).- Bumps
flutter_mcp_ui_coreto^0.4.0.
Fixed #
ThemeManager.getColorValue(slot)— falls back to the fromSeed-derived M3 28-role palette when the slot is absent from the bundle's rawtheme.colormap (spec §5.3 expects bundles to declare onlyseedplus a handful of overrides; the missing roles must derive). Previously the lookup only consulted_themeData = definition.toJson()(raw, no derive), so DSL bindings liketheme.color.surfaceContainerHighresolved tonullfor any bundle that omitted the role — even thoughtoFlutterTheme().colorScheme.surfaceContainerHighwas filled correctly. The two paths now match. Light / dark schemes are cached; cache is invalidated on everysetTheme/setThemeDefinition/resetTheme/reset/applyOverriderestore. Semantic roles (success/warning/infoand theiron*variants) are not on Flutter'sColorScheme, so the fallback returnsnullfor those — bundles must declare them explicitly.box(and the sharedBoxDecorationResolver) —decoration: {color: ...}is no longer silently dropped when neither top-levelcolornorbackgroundColoris supplied. The factory previously injectedcolor: nullinto the flat property bag, and the resolver's flat-vs-nested override pass treated thecontainsKey('color')hit as an explicit erasure of the nested value. Now: (a) the factory only overlayscolorwhen the merged top-level value is non-null, and (b) the resolver ignores null entries during the override pass — they cannot shadow nested fields. Same null-tolerant treatment applies togradient/image/border/borderRadius/boxShadow/shape/backdropBlur. Surface-toned boxes (e.g.decoration: {color: "surfaceContainerHigh"}) finally render against the M3 surface tonal scale rather than transparent.
0.4.4 - 2026-05-02 - M3 + Responsive consumption layer (bug fix) #
0.3.0 announced "Material 3 + Responsive" but the runtime side was never actually wired up. 0.4.4 delivers the consumption surface so the previously advertised features finally work.
Fixed #
- M3 token shorthand on
text.variant,box.padding,card.shape,card.elevation,button.elevation,icon.size/sizeToken— resolves through the correspondingtheme.<domain>.<token>. FormFactorScopeauto-wrap on the runtime root, soAppSpacing.of(context)/AppTypography.of/AppIconSizes.of/AppDensity.ofactually track the form factor.- Per-form-factor property override map (
{compact, medium, expanded, large, extraLarge, embedded, default}) resolves on every property, per spec § 14.2. - Linux:
event_listen_cb/event_cancel_cbreturn type aligned withFlMethodErrorResponse*so the plugin compiles against the currentflutter_linux.h.
Notes #
- Bumps
flutter_mcp_ui_coreto^0.3.2for the matching schema additions.
0.4.3 - 2026-05-01 - errorBoundary / errorRecovery onError spec violation fix #
Fixed #
errorBoundary(spec §2.13.11) — onError child context was registeringerror(string) instead of the canonicaleventvariable. Now registersevent: {error, stack}so{{event.error}}and{{event.stack}}resolve as the spec specifies.errorRecovery(spec §2.13.12) — same variable-name violation; now registersevent: {error, stack}. Stack trace is captured (was previously lost).errorBoundarywas re-firingonErroron every rebuild while the boundary remained in the error state. The action now dispatches exactly once per captured exception (in the post-frame callback that flips_hasError).
0.4.2 - 2026-05-01 - Tool response spec violation fix (§3.10 + §4.4.2) #
Fixed #
§3.10auto-merge —ToolActionExecutornow callsstateManager.mergeState(response)when the tool response is a Map andbindResultis not specified, instead of leaving fold to host code. Top-level keys of the response land directly on page state.§4.4.2onSuccess/onError variable — child context now exposes the canonicaleventvariable.{{event.<key>}}resolves to the response body insideonSuccess; insideonError,eventis{code, message, details}per spec. Previously the runtime registeredresponse/error(string), so the spec patterns silently failed.
Migration #
- DSL written against the previous (non-spec)
{{response.<key>}}/{{error.message}}shapes must move to{{event.<key>}}to keep working.
0.4.1 - 2026-04-30 - Template auto-registration + theme system fixes + spec alignment #
Fixed #
RuntimeEnginenow reads thetemplatesblock from the application / page definition duringinitializeand registers each entry into theTemplateRegistry(application scope for application roots, screen scope for standalone pages). Previously the block was silently ignored, so any{ "type": "use", "template": "<name>" }reference in the DSL failed to resolve and the widget went unrendered.ThemeManager.flutterThemeModenow honourssetHostBrightnessformode: 'system'resolution: when the embedder injects a brightness override, it returnsThemeMode.light/ThemeMode.darkaccordingly instead of always emittingThemeMode.system(which Flutter resolves against OS brightness only). AppPlayer-class hosts are "the system" for embedded bundles, so launcher light/dark toggles now propagate into nestedMaterialAppinstances.ThemeManager.toFlutterTheme(isDark: true)no longer falls back to the active (light)_definitionwhen the bundle declares nodarkvariant — it now returnsThemeDefinition.defaultDark()so the M3 default dark scheme is used. Previously bundles with an empty theme block could only render light, regardless of host brightness.- Template /
itemTemplateinstance state binding across close → reopen cycles. The singletonWidgetCachewas retaining widget instances whose event-handler closures captured the prior session's destroyedRenderContext(StateManager,ActionHandler). After a host-driven close → reopen, the rebuilt UI tree showed cached widgets whoseonTapmutations targeted the dead engine, so visible state never updated even though buttons appeared responsive. Fix has three parts working together:Renderer._hasEventHandlersnow recurses throughchild/children, so an ancestor container holding event-bound descendants is also flagged non-cacheable. Previously alinear/boxwrapping anonTapbutton could be cached even though its subtree's closures captured a stale context.'use'is added tononCacheableTypes. Eachusesite is an instance — its expansion MUST be a fresh widget subtree, not a shared cached widget across invocations or sessions.MCPUIRuntime.destroy()now callsWidgetCache.instance.clear()to drop all cached entries from the dying session, so the next session starts with a clean cache and cannot inherit closures bound to a destroyed engine.- The same fix covers the
list.itemTemplate(spec §9.6.1) instantiation path, since per-item expansions also produce fresh subtrees and the recursive event-handler check now catches buttons nested anywhere in the row template.
Changed (breaking — pre-launch spec alignment) #
ExtendedTemplateDefinitionwidget tree wrapper field renamedbody→contentto align with MCP UI DSL 1.3 §9.2.2 (the canonical key for the template's widget tree). The use-site invocation key (params:on theusewidget) is unchanged.TemplateRegistry.isTemplateReferencenow accepts only the canonicaltype: "use". Legacy aliases (type: "template"/type: "useTemplate") are removed in line with the spec's no-alias-accretion policy. Bundles using the legacy types must migrate totype: "use".
0.4.0 - 2026-04-29 - Render inspector hook #
- New
MCPUIRuntime.withInspector(...)for editor tooling — pairs each rendered widget with its source JSON node. Standard constructor unchanged; no overhead when no inspector is supplied.
0.3.0 - 2026-04-28 - MCP UI DSL 1.3 (Material 3 + Responsive) #
Changed (breaking) #
ThemeManagerrewritten on top of strongly-typedThemeDefinitionfromflutter_mcp_ui_core— drops the 1.2-era 11-slot raw map and parallel default scheme.- Theme bindings use the new path scheme (
theme.color.<slot>,theme.typography.<role>,theme.spacing.<token>,theme.shape.<family>,theme.elevation.<level>.shadow,theme.motion.duration.<key>). Legacytheme.colorScheme.*,theme.borderRadius.*,theme.spacing.medium,theme.elevation.smallare removed. widget_factorysemantic color slots aligned to M3 28-role + semantic family (nobackground/divider).- DSL version constant now sourced from
flutter_mcp_ui_coreMCPUIDSLVersion(runtime's ownDSLVersionenum removed). - License changed from Apache-2.0 to MIT.
Added #
McpUiThemeBuilder— convertsThemeDefinitioninto FlutterThemeData(ColorScheme,TextTheme,VisualDensity,CardThemeData,DialogThemeData).- HCT-seed-derived default theme (
SeedPalette.lightFromSeed/darkFromSeed). - Page-level theme override —
applyOverride(Map)deep-merges 14-domain JSON, returns restore callback (spec §5.7). - Responsive form factor scaffold —
FormFactorenum (compact / medium / expanded / large / embedded),FormFactorScope,ViewModeResolverpriority chain. - Four responsive token sets with
.of(context)accessors —AppSpacing,AppIconSizes,AppTypography,AppDensity. - Auto-adaptive navigation — drawer swaps to modal drawer (compact) / NavigationRail (medium) / permanent drawer (expanded+).
- New dependency:
mcp_bundle ^0.3.0.
0.2.5 #
Bug Fixes #
- Fixed resource subscription cleanup on runtime destroy to properly unsubscribe from all active resources
0.2.4 #
0.2.3 #
Documentation #
- Added important build instructions for dynamic icons
- Documented the need for
--no-tree-shake-iconsflag when building apps with dynamic icons
0.2.2 #
Bug Fixes #
- Fixed navigation state persistence to properly save and restore tab/navigation positions
- Added SharedPreferences support to CacheManager for actual disk persistence
- Fixed setState during build error in ApplicationShell navigation initialization
0.2.1 #
Bug Fixes #
- Fixed state initialization issue where page states were not properly loaded
- Unified state management by removing duplicate StateService and using StateManager directly
- Fixed page state initialization in routing system
0.2.0 #
Refactoring #
- Major internal refactoring for improved maintainability
- Enhanced code organization and structure
- Improved type safety and validation
- Better separation of concerns
0.1.0 #
Initial Release #
- Comprehensive runtime for building dynamic, reactive UIs through JSON specifications
- Support for 77+ Flutter widgets across 9 categories
- Built-in state management with automatic UI updates
- Expression binding system with support for nested paths and transforms
- Action system (state, tool, batch, conditional, navigation)
- Multiple instance support for different MCP servers
- Tool executor injection for external API integration
- Custom widget registration support
- Custom transform registration
- Theme management with light/dark mode support
- Navigation and routing system
- Dialog and notification services
- Background service management
- Lifecycle management
- Service registry pattern
- Based on MCP UI DSL v1.0 specification