Plugin Authoring Guide
A plugin is a Dart package that ships a DowelServiceProvider subclass. The
subclass declares the CLI commands and MCP tool descriptors the plugin contributes.
When a consumer calls plugin:install <name>, dowel registers the provider in
lib/app/_plugins.g.dart, which is loaded at startup and merged into the
DowelRegistry. Commands appear in dowel list; MCP tool descriptors surface
through the stdio JSON-RPC server so LLM agents can invoke them by name.
On this page
Getting Started#
Scaffold the plugin skeleton with the make:plugin command:
dart run dowel make:plugin awesome_plugin
The generator creates the following tree inside awesome_plugin/:
awesome_plugin/
lib/
awesome_plugin.dart # barrel export
src/
awesome_plugin_dowel_provider.dart # DowelServiceProvider subclass
commands/ # one file per DowelCommand
pubspec.yaml # declares dowel dependency
CHANGELOG.md
README.md
The generated provider is immediately wirable: plugin:install awesome_plugin
injects AwesomePluginDowelProvider.new into the consumer's _plugins.g.dart.
DowelServiceProvider Contract#
Every plugin must extend DowelServiceProvider
(lib/src/console/dowel_service_provider.dart:11-25):
abstract class DowelServiceProvider {
/// Human-readable provider name used in collision error messages.
/// Defaults to the runtime class name.
String get providerName => runtimeType.toString();
/// Returns the commands this provider contributes to the application.
List<DowelCommand> commands();
/// Returns the MCP tool descriptors this provider contributes.
///
/// Defaults to an empty list so existing providers compile without
/// modification. Override to expose VM-Service-backed tools to the MCP
/// server.
List<McpToolDescriptor> mcpTools() => const <McpToolDescriptor>[];
}
Key points:
commands()is abstract: return everyDowelCommandsubclass your plugin ships. Duplicate command names throwDowelCommandCollisionException.mcpTools()defaults to an empty list. Override it to expose VM Service extension tools to the MCP server.providerNamedefaults toruntimeType.toString(). Override it with a stable identifier (e.g.'tapir') so collision messages survive class renames.- There is no
register()lifecycle hook onDowelServiceProvider. Initialisation belongs in command constructors or the host app's boot phase.
Minimal scaffold provider (assets/stubs/make_plugin/generic/provider.dart.stub):
import 'package:dowel/dowel.dart';
final class AwesomePluginDowelProvider extends DowelServiceProvider {
@override
List<DowelCommand> commands() => <DowelCommand>[
// Commands added via `make:command` land here automatically.
];
// To expose MCP tools, override mcpTools() returning your descriptor list.
}
Writing Commands#
Each entry in commands() is a DowelCommand subclass. Declare the command
surface via the Signature DSL or configure(ArgParser):
- Signature DSL (preferred):
String get signature => 'cmd:name {arg} {--flag}'. See Signature DSL reference for the full syntax. configure(ArgParser): explicit fallback for cases the DSL cannot express (subcommands, allowed values with validation, etc.).
Scaffold a new command inside the plugin directory:
dart run dowel make:command MyVerb
The generator writes lib/src/commands/my_verb_command.dart and auto-registers
it in the provider's commands() list. See
make:command for the full scaffold walkthrough.
Adding MCP Tools#
Override mcpTools() and return a List<McpToolDescriptor>. Each descriptor
(lib/src/mcp/mcp_tool_descriptor.dart:24-82) is a const-constructible value
with four required fields:
| Field | Type | Purpose |
|---|---|---|
name |
String |
Snake-case, service-prefixed tool name (e.g. awesome_start). Must be globally unique across all loaded providers. |
description |
String |
Action-oriented, LLM-targeted description. Imperative opening sentence, usage bullets, constraint-forward language. |
inputSchema |
Map<String, dynamic> |
JSON Schema draft-7 object describing accepted input. Pass {'type': 'object', 'properties': {}} when the tool takes no arguments. |
extensionMethod |
String |
VM Service extension registered in the running Flutter isolate (e.g. ext.awesome.start). Internal routing key; not included in the MCP wire shape. |
Example provider with commands and MCP tools:
import 'package:dowel/dowel.dart';
final class AwesomeStartCommand extends DowelCommand {
@override
String get signature => 'awesome:start';
@override
String get description => 'Start the awesome service.';
@override
CommandBoot get boot => CommandBoot.connected;
@override
Future<int> handle(DowelContext context) async {
context.output.success('Awesome service started.');
return 0;
}
}
final class AwesomeStopCommand extends DowelCommand {
@override
String get signature => 'awesome:stop';
@override
String get description => 'Stop the awesome service.';
@override
CommandBoot get boot => CommandBoot.connected;
@override
Future<int> handle(DowelContext context) async {
context.output.success('Awesome service stopped.');
return 0;
}
}
final class AwesomePluginDowelProvider extends DowelServiceProvider {
@override
List<DowelCommand> commands() => [
AwesomeStartCommand(),
AwesomeStopCommand(),
];
@override
List<McpToolDescriptor> mcpTools() => [
McpToolDescriptor(
name: 'awesome_start',
description: 'Start the awesome service',
inputSchema: {'type': 'object', 'properties': {}},
extensionMethod: 'ext.awesome.start',
),
];
}
extensionMethod must match the registerExtension call in the running Flutter
isolate; the descriptor is not callable without it.
The MCP server collects descriptors from all loaded providers when
runDowel(collectMcpTools: true) fires (bin/mcp.dart sets this
automatically). Tool names must be unique; a collision throws
DowelMcpToolCollisionException at startup.
Naming convention: prefix every tool name with a short service identifier
followed by an underscore (for example my_plugin_greet). The prefix should
match the package's providerName (or a recognisable abbreviation thereof).
Publishing#
-
Bump the version in
pubspec.yamlfollowing SemVer (patch for fixes, minor for new commands/tools, major for breaking provider API changes). -
Declare the dependency on
dowelwith a caret constraint, notpath::yaml dependencies: dowel: ^0.0.21 -
Validate the archive before tagging:
bash dart pub publish --dry-run -
Add a CHANGELOG entry, then publish:
bash dart pub publish
Linking from Consumer#
The consumer installs the plugin with:
dart run dowel plugin:install awesome_plugin
This registers AwesomePluginDowelProvider.new in lib/app/_plugins.g.dart and
re-generates the barrel; the provider is discovered at the next dowel start.
See plugin:install for the two routing modes
(manifest, direct registration).
Related Documentation#
- install-yaml.md -
install.yamlmanifest schema for declaring install steps declaratively. - installer-dsl.md -
PluginInstallerfluent DSL for authoring programmatic install logic. - make:plugin - scaffold command reference with all flags and the generated file tree.