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 is a tool plugin; so is Universal Backup.

Tools reference the same SqlExplorer.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)Read the plugin's own persisted settings, as configured in Settings › Plugins.
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)Hand generated SQL to a query tab instead of running DDL yourself. Schema Diff ends this way: the user reviews the migration 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 host still renders the dialog's chrome (checklist, log, progress bar, action bar) around your view.

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": "schema-diff", "type": "tool", "name": "Schema Diff", "version": "0.2.0", "hostApiVersion": 4, "entryAssembly": "SqlExplorer.Tools.SchemaDiff.dll" }

Declare the newest version whose members you actually use — not simply the newest that exists. A tool that declares an older version keeps loading in hosts that predate the newest one.