ADR 0001 — Capability kernel and tool-name prefix model

Context

Pre-v3.0, the dependency graph was upside-down at the server boundary:

  • flutter_live_edit_toolkit → mcp_toolkit was correct (app-side).
  • mcp_server_dart → flutter_live_edit_toolkit was a leak: the server statically depended on Flutter, which it never renders. This was the source of the long-standing uses-material-design warnings in server builds and tests.
  • Every binary advertised live-edit's tool surface unconditionally; there was no way to compose a custom server with a different capability set.
  • Tool names were flat across the MCP boundary — tap_widget from core and a hypothetical future tap_widget from another capability would collide silently.

Decision

Introduce a capability kernel (mcp_capability_kernel): a Dart package that defines the contracts a unit of MCP functionality must satisfy. Each capability is its own package, registered into a host (server or CLI) at compile time. The kernel applies a <capabilityId>_ prefix to every tool and resource name the capability exposes.

The shape of the contract:

abstract interface class Capability {
  /// Stable id for the tool-name prefix (`<id>_<tool>`).
  /// Must match ^[a-z][a-z0-9_]*$. Reserved: 'app'.
  String get id;
  String get description;
  String get version;

  Future<void> register(CapabilityContext context);
  Future<void> dispose();
}

abstract interface class CapabilityContext {
  CapabilityConfig get config;
  void registerTool(ToolRegistration registration);
  void registerResource(ResourceRegistration registration);
  T require<T extends HostService>();
}

HostService is a marker interface for capabilities to request host machinery (today: CommandRunner for the shared-core executor). Host services are provided by the host at registration time; missing services throw HostServiceUnavailableError.

Prefix enforcement (kernel-only)

All rules apply at registration time and throw on violation:

  • Capability id must match ^[a-z][a-z0-9_]*$ (InvalidCapabilityIdError).
  • Capability id must not be app — reserved for unscoped dynamic registrations (InvalidCapabilityIdError).
  • Tool name passed to registerTool must not start with the capability's prefix (PrePrefixedToolNameError). Capabilities pass bare names; the kernel composes the public name.
  • Two static registrations resolving to the same fully-qualified name throw ToolNameCollisionError at host startup.
  • A dynamic entry whose name collides with a static registration is rejected at the bridge with a structured error.

Hard cut at v3.0.0

No deprecation aliases. No soft errors. No --no-use-capability-kernel opt-out.

Reasoning: a capability rename is rare, and a clean tool_not_found is easier to debug than a silent fallback. Migration tooling would have outlived the migration window by years.

Consequences

What changed:

  • mcp_server_dart's static dep tree contains zero Flutter packages. uses-material-design warnings in server builds disappeared.
  • All MCP tool names from the bundled fmt capability are prefixed: tap_widgetfmt_tap_widget, hot_reload_and_capturefmt_hot_reload_and_capture, etc. Unprefixed names return tool_not_found. The locked surface is in tool/contracts/expected_tool_surface.txt.
  • The dynamic-registry host trio is exposed as fmt_list_client_tools_and_resources, fmt_client_tool, and fmt_client_resource on MCP and in the CLI catalog, matching the fmt_ naming used in tools/list.
  • CLI catalog names remain bare for intrinsic fmt tools (flutter-mcp-toolkit exec --name tap_widget, …); the MCP kernel still publishes those as fmt_tap_widget. Dynamic-registry commands are the exception: their catalog name already includes the fmt_ prefix.
  • mcp_server_dart and flutter-mcp-toolkit became thin shells. Their default main.dart registers the bundled capability set; power users write their own main.dart to compose a different set.

What we paid:

  • Every existing client-side configuration referencing a tool by name had to be updated. v3.0.0 ships the breaking change without a shim.
  • Future capability work (live-edit re-integration, network introspection) has to shape itself as a capability rather than as a server feature.

Non-goals

  • Plugin-discovery / runtime capability loading. Capabilities register at compile time. A user wanting a different surface composes a different binary.
  • Cross-process or remote capabilities.
  • Per-capability semantic versioning beyond what the version field surfaces in doctor.

Open follow-ups (optional v3.x)

  • Capability-level configuration via a config file (today: CLI flags only).
  • A flutter-mcp-toolkit capabilities list subcommand.
  • DynamicRegistryBridge host service interface — designed but not yet implemented; planned alongside live-edit re-integration.

Related work