HomeFeaturesPluginsDocsDownload

Building a tool

A tool contributes an action rather than an engine: a menu item on the schema tree that collects some input and runs against the selected connection or node. Schema Diff and Copy Table are tool plugins; so is Universal Backup.

Tools reference the same DataTray.Sdk assembly as providers, and get handed the live IDbProvider for the connection they run on — so a tool can read schema and run queries on any engine without a driver dependency of its own.

The contract: IToolPlugin

public interface IToolPlugin { string Id { get; } // stable; one assembly may ship several tools string Title { get; } // menu item / dialog title ProviderIcon? Icon => null; ToolTarget Target { get; } // where in the tree it is offered IReadOnlyList<ToolField> Fields { get; } // Route A: inputs the host renders bool IsDestructive => false; // true -> host confirms first Task ExecuteAsync( ToolExecutionContext context, IReadOnlyDictionary<string, string?> inputs, IProgress<ToolProgress> progress, CancellationToken ct); }
MemberPurpose
IdStable id. One assembly may contain several IToolPlugin classes — all are loaded — so this need not match the manifest id.
TargetWhich tree nodes offer the tool.
FieldsRoute A input declarations; the host renders a generic dialog. Empty when the tool brings its own view.
IsDestructiveShow a destructive-action confirmation before running.
ExecuteAsyncRuns the tool. inputs holds the collected values keyed by ToolField.Key; report progress through progress.

Where the tool appears: ToolTarget

public sealed record ToolTarget( IReadOnlyList<string>? ProviderIds = null, // null = every provider IReadOnlyList<DbNodeKind>? NodeKinds = null, // null = any node kind bool IncludeConnectionRoot = false); // the root has no node kind

The tool shows on a node when the node's provider is in ProviderIds and its kind is in NodeKinds. The connection root has no node kind, so a whole-connection tool sets IncludeConnectionRoot rather than trying to express the root through NodeKinds.

What a run receives

public sealed record ToolExecutionContext( ConnectionProfile Profile, // connection string resolved, secrets already fetched DbNodeRef? Node, // the node it launched on; null at the connection root IDbProvider Provider, // the live provider - read schema, run queries string ProviderId, IToolHost Host); // host services: pickers, settings, query tabs

IToolHost

MemberPurpose
GetPluginSetting(key) / SetPluginSetting(key, value)Read and write the plugin's own persisted settings — enough to remember a choice between runs.
ListConnections() / ListDatabasesAsync(id, ct)The user's other connections, and the databases on one of them. This is how a tool offers a destination.
OpenConnection(id, database)A live provider + profile for another connection, so a tool can run against a second database.
OpenQueryEditor(sql) / OpenQueryEditorOn(id, database, sql)Hand generated SQL to a query tab — on the launched connection, or on a picked one. Schema Diff and Copy Table both end this way: the user reviews the script before anything runs.

Route A: declared fields

public sealed record ToolField( string Key, string Label, ToolFieldType Type = ToolFieldType.Text, bool Required = false, string? Default = null, string? Placeholder = null, IReadOnlyList<string>? Choices = null, IReadOnlyList<string>? FileExtensions = null, bool SaveFile = false);

ToolFieldType is Text | Password | Choice | File | Bool | ConnectionPicker | DatabasePicker. A Password field is routed to the OS keychain and never written to disk; a File field gets a Browse button wired to the host's picker. ConnectionPicker and DatabasePicker are a dependent pair: pick a connection and the database dropdown fills itself from it.

Route B: your own view

When the inputs are interdependent, or the flow deserves a designed layout, supply an Avalonia view instead of the generated form:

public interface ICustomToolUi { Control CreateView(IToolUiContext context); // read/write values by ToolField.Key }

Values still flow through IToolUiContext.GetValue/SetValue, so ExecuteAsync is unchanged. The context also gives the view Localizer, the same ListConnections()/ListDatabasesAsync() pickers, and RunAsync()/CancelRun()/CloseDialog() so the view can drive the run from its own buttons.

Owning the whole dialog

By default the host still renders the chrome around your view: a checklist, a log, a progress bar and an action bar. A view that implements IToolDialogLifecycle takes all of that over.

public interface IToolDialogLifecycle { void OnRunStarted(); void OnProgress(ToolProgress progress); void OnRunFinished(ToolRunOutcome outcome, string? message); }

The host then only feeds the view the run's events and lets it render input, progress and completion itself. ToolProgress carries ItemKey and ItemStatus for a stepped checklist, Fraction for a per-step bar, and Detail for the short note beside a step. ToolRunOutcome is Succeeded | Cancelled | Failed.

Purely additive: a view that doesn't implement it keeps the host-rendered chrome, which stays the right default for a tool without a designed progress story. Copy Table is the first tool to own its dialog.

Because the returned Control is an Avalonia type shared across the ALC boundary, reference Avalonia with ExcludeAssets="runtime" — see Plugin capabilities.

Tool manifest

Identical to a provider's, but type is "tool" and hostApiVersion tracks the tool contract (ToolHostApi.Version), which versions separately from the provider contract.

{ "schemaVersion": 1, "id": "copy-table", "type": "tool", "name": "Copy Table", "version": "0.2.0", "hostApiVersion": 5, "entryAssembly": "DataTray.Tools.CopyTable.dll" }

Declare the newest version whose members you actually use — not simply the newest that exists. A tool that stays on 4 keeps loading in hosts that predate 5.