Plugin System
Bowire uses a plugin architecture based on the IBowireProtocol interface. Protocol plugins are auto-discovered via assembly scanning at startup -- no manual registration is needed.
Built-in Plugins
| Plugin | Package | Protocol |
|---|---|---|
BowireGrpcProtocol |
Kuestenlogik.Bowire.Protocol.Grpc |
gRPC via Server Reflection |
BowireSignalRProtocol |
Kuestenlogik.Bowire.Protocol.SignalR |
SignalR hub discovery |
BowireSseProtocol |
Kuestenlogik.Bowire.Protocol.Sse |
Server-Sent Events |
BowireMcpProtocol |
Kuestenlogik.Bowire.Protocol.Mcp |
Model Context Protocol for AI agents |
Installing Plugins
Third-party protocol plugins can be installed via the CLI:
bowire plugin install <package-id>
bowire plugin install <package-id> --version 1.0.0
bowire plugin install <package-id> --source https://nuget.internal/v3/index.json
bowire plugin list
bowire plugin list --verbose # resolved version, sources, DLL list
bowire plugin update <package-id> # bump one plugin to latest
bowire plugin update # bump every installed plugin
bowire plugin update <package-id> --version 2.0.0
bowire plugin inspect <package-id> # load + print ALC + discovered IBowireProtocol types
bowire plugin uninstall <package-id>
plugin list is a pure disk read; plugin inspect actually loads the plugin into a dedicated BowirePluginLoadContext and reflects over it — use it to confirm that a freshly-installed NuGet package exposes an IBowireProtocol implementation and that its private deps landed in the expected context.
plugin update compares the installed resolvedVersion (stored in plugin.json) against what the configured sources advertise and skips cleanly when they match. When moving between versions, the current install is replaced in-place — same directory, fresh DLL set.
Plugins are stored in ~/.bowire/plugins/ with per-plugin subdirectories. Each plugin includes a plugin.json manifest tracking the package ID, version, install date, and included files.
Install uses the NuGet.Protocol client directly — no dotnet restore/build detour — so the host only needs the .NET runtime (not the SDK) and downloads skip the temp-csproj song-and-dance. Transitive runtime dependencies follow automatically; host-provided assemblies (Kuestenlogik.Bowire*, System.*, Microsoft.*, NETStandard.*) are filtered out at copy time so they don't shadow the loaded host versions.
Private feeds and multiple sources
Both --source on the CLI (repeatable) and Bowire:Plugin:Sources in appsettings.json feed into the same source list. When unset, nuget.org is the default. The first feed that has the package wins — put private feeds first if you want to shadow public versions.
# Single alternative feed
bowire plugin install MyCompany.Internal.Plugin --source https://nuget.corp.local/v3/index.json
# Multiple feeds (private first, public fallback)
bowire plugin install MyCompany.Plugin \
--source https://nuget.corp.local/v3/index.json \
--source https://api.nuget.org/v3/index.json
// appsettings.json
{
"Bowire": {
"Plugin": {
"Sources": [
"https://nuget.corp.local/v3/index.json",
"https://api.nuget.org/v3/index.json"
]
}
}
}
Retyping --source on the CLI replaces the appsettings list entirely — same semantics as --url in the browser UI.
Configuring the Plugin Path
The plugin directory is resolved from a standard .NET configuration stack (highest priority wins):
--plugin-dir <path>CLI flag (applies to every subcommand, top-level)BOWIRE_PLUGIN_DIRenvironment variableappsettings.jsonkeyBowire:PluginDir- Default
~/.bowire/plugins/
# All three pick the same directory — use whichever fits your workflow.
bowire plugin list --plugin-dir ./my-plugins
BOWIRE_PLUGIN_DIR=./my-plugins bowire plugin list
echo '{ "Bowire": { "PluginDir": "./my-plugins" } }' > appsettings.json
Install, list, uninstall, and the runtime plugin-load at startup all agree on the same resolved path.
Embedded Hosts
Applications that embed Bowire via AddBowire() can wire the same plugin directory through IConfiguration:
builder.Services
.AddBowirePlugins(builder.Configuration) // reads Bowire:PluginDir
.AddBowire();
Non-existent paths are a no-op so the call is safe to make unconditionally.
Per-Plugin Configuration
Plugins bind their own configuration section under Bowire:Plugins:<PluginName> via the standard .NET options pattern. Inside the plugin's IBowireProtocolServices.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<MqttPluginOptions>()
.BindConfiguration("Bowire:Plugins:Mqtt");
}
The options class is then injectable as IOptions<MqttPluginOptions> anywhere in the plugin. Users configure it per the standard .NET precedence — CLI args → env vars → appsettings.json:
{
"Bowire": {
"PluginDir": "./my-plugins",
"Plugins": {
"Mqtt": {
"BrokerPort": 1883,
"DefaultTopic": "sensors/#"
}
}
}
}
BindConfiguration resolves IConfiguration lazily at options-resolution time — no changes to the IBowireProtocolServices interface or to AddBowire required.
Browser-UI Options from Config
The browser-UI mode (plain bowire without a subcommand) also reads its own options from the shared config stack. Everything that used to need a dedicated CLI flag now has a matching Bowire:* key:
| CLI flag | Config key | Default |
|---|---|---|
--port, -p |
Bowire:Port |
5080 |
--title |
Bowire:Title |
"Bowire" |
--url, -u (repeatable) |
Bowire:ServerUrl, Bowire:ServerUrls (array) |
empty |
--no-browser |
Bowire:NoBrowser |
false |
--enable-mcp-adapter |
Bowire:EnableMcpAdapter |
false |
{
"Bowire": {
"Port": 7070,
"Title": "Staging API Browser",
"ServerUrls": [
"https://api-staging.example.com",
"https://api-canary.example.com"
],
"NoBrowser": true
}
}
CLI flags still override appsettings (standard .NET precedence), so the same config file can hold defaults while one-off invocations retype --port 8080 to override without editing the file. Repeated --url flags replace the appsettings list entirely — retyping --url is a full override, not an append.
How Discovery Works
MapBowire()triggersBowireProtocolRegistry.Discover()- The registry scans all loaded assemblies for types implementing
IBowireProtocol - Each plugin is instantiated and its
Initialize(IServiceProvider?)method is called - The registry exposes all plugins to the Bowire API endpoints
Installed CLI plugins are loaded into a dedicated AssemblyLoadContext per plugin directory (see below).
Isolation
Each installed plugin lives in its own BowirePluginLoadContext rather than sharing the default ALC. That gives two guarantees:
- Plugin-private dependencies coexist. Plugin A can ship MQTTnet 5.1 while Plugin B ships 5.2 — each context loads its own copy from its own folder without collision.
- Contract types stay identical. Assembly names starting with
Kuestenlogik.Bowire*,System.*,Microsoft.*, orNETStandard.*delegate to the default ALC, so theIBowireProtocolinterface in Plugin A's assembly is the same type as the one in the host —typeof(IBowireProtocol).IsAssignableFrom(pluginType)just works, reflection discovery finds plugin types, DI flows normally.
BowirePluginLoadContext is public, so embedded hosts can reuse it and extend the shared-prefix list with their own SDK namespace:
var ctx = new BowirePluginLoadContext(pluginDir, additionalSharedPrefixes: new[] { "Acme.Corp." });
Hot unload / replace isn't supported yet (IsCollectible = false). The bowire plugin update path will add that once the update subcommand lands.
Writing a Custom Plugin
Implement IBowireProtocol and package it as a NuGet package. See Building Custom Protocols for a complete guide.
The fastest way to get a working skeleton is the dotnet new template maintained in the Bowire.Templates sister repo:
dotnet new install Kuestenlogik.Bowire.Templates
dotnet new bowire-plugin --name <Org>.Bowire.Protocol.<YourProtocol>
Both <Org> and <YourProtocol> are placeholders — replace <Org> with your organisation prefix (e.g. Acme, MyCompany, com.yourname) and <YourProtocol> with the protocol you're adding (Amqp, Nats, Stomp, …). The convention <Org>.Bowire.Protocol.<Name> keeps the package discoverable alongside the bundled Kuestenlogik.Bowire.Protocol.* plugins but isn't enforced — any package id works.
Concrete example:
dotnet new bowire-plugin --name Acme.Bowire.Protocol.Amqp
dotnet pack ./Acme.Bowire.Protocol.Amqp
bowire plugin install Acme.Bowire.Protocol.Amqp --source ./nupkgs
It scaffolds the csproj with the right Kuestenlogik.Bowire reference, a sample IBowireProtocol implementation, and a test project.
See also: Plugin Architecture, Custom Protocols