Project setup
A plugin is a .NET 10 console project with a manifest.json beside it; this page builds one by hand, or
explains what macrodeck-plugin new generated for you.
The files
Section titled “The files”MyPlugin/├── Assets/│ └── icon.svg the plugin icon, referenced by manifest.json├── MyIntegration.cs your capabilities├── MyPlugin.csproj a console project with the Macro Deck packages├── Program.cs starts the plugin├── macrodeck-build.json how macrodeck-plugin build builds each platform└── manifest.json who the plugin isnew generates the same project under src/MyPlugin/, plus a solution, a test project, a
Localization/ resource set and central package versions.
Project file
Section titled “Project file”<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
<ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="MacroDeck.Plugin.Hosting" Version="3.0.0-*" /> <PackageReference Include="MacroDeck.Plugin.Serilog" Version="3.0.0-*" /> <PackageReference Include="MacroDeck.Plugin.Analyzers" Version="3.0.0-*" PrivateAssets="all" /> </ItemGroup>
<ItemGroup> <Content Include="manifest.json" CopyToOutputDirectory="PreserveNewest" /> <Content Include="Assets/icon.svg" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup>
</Project>Microsoft.NET.Sdkplus the ASP.NET Core framework reference, notMicrosoft.NET.Sdk.Web: the hosting package builds on ASP.NET Core, but a plugin is a headless process.3.0.0-*picks the newest Macro Deck 3 preview. Pin an exact version for reproducible builds.MacroDeck.Plugin.Analyzersis optional but recommended: it reports invalid declarations at build time. See Analyzers.- Both
Contentitems are required. The SDK readsmanifest.jsonfrom the content root at startup and resolves the icon against it.
manifest.json
Section titled “manifest.json”{ "$schema": "https://schemas.macro-deck.app/plugin-manifest-v1.schema.json", "manifestVersion": 1, "id": "com.example.my-plugin", "name": "My Plugin", "version": "1.0.0", "description": "What the plugin does.", "icon": "Assets/icon.svg", "entrypoints": { "win-x64": { "executable": "runtimes/win-x64/MyPlugin.exe" }, "osx-arm64": { "executable": "runtimes/osx-arm64/MyPlugin" }, "linux-x64": { "executable": "runtimes/linux-x64/MyPlugin" } }}| Field | Rule |
|---|---|
manifestVersion |
Always 1. |
id |
Reverse-domain, lowercase, at least two segments: com.example.my-plugin. |
name |
Display name, 1-128 characters. |
version |
SemVer 2.0. |
entrypoints |
One entry per runtime identifier you have tested; executable is relative to the package root. |
Identity lives only here - the hosting builder has no WithId, WithName or WithVersion. The
runtimes/<rid>/ paths are where build stages each self-contained publish. Before
publishing you also need publisher, license, repository and compatibility; build warns about
each one that is missing. Every field is in the manifest reference.
Program.cs
Section titled “Program.cs”using MacroDeck.Plugin.Hosting;using MacroDeck.Plugin.Serilog;
var plugin = MacroDeckPlugin.CreatePlugin(args) .UseMacroDeckLogging() .RegisterIntegration<MyIntegration>() .Build();
await plugin.RunAsync();RegisterIntegration<T>()registers the integration and every capability interface it implements.Build()checks the manifest identity, duplicate capability ids, reserved routes and the DI graph, and reports every problem at once.UseMacroDeckLogging()forwards the plugin’s logs to the Macro Deck log viewer - see Logging.
The integration class
Section titled “The integration class”using MacroDeck.Sdk;using MacroDeck.Sdk.Actions;
public sealed class MyIntegration : IPluginIntegration{ public IReadOnlyList<IActionDefinition> Actions { get; } = [];
public Task InitializeAsync(IIntegrationContext context) => Task.CompletedTask;
public Task ShutdownAsync() => Task.CompletedTask;}The host reads Actions (and every other capability list) before InitializeAsync runs, so build them
in the constructor without I/O. Connect to devices or services in InitializeAsync, release them in
ShutdownAsync. Fill Actions in with Your first action.
Build recipe
Section titled “Build recipe”macrodeck-build.json, beside the manifest - one target per entrypoint:
{ "version": 1, "targets": { "win-x64": { "executable": "dotnet", "arguments": ["publish", "MyPlugin.csproj", "-c", "Release", "-r", "win-x64", "--self-contained", "true", "-o", "bin/publish/win-x64"], "output": "bin/publish/win-x64" }, "osx-arm64": { "executable": "dotnet", "arguments": ["publish", "MyPlugin.csproj", "-c", "Release", "-r", "osx-arm64", "--self-contained", "true", "-o", "bin/publish/osx-arm64"], "output": "bin/publish/osx-arm64" }, "linux-x64": { "executable": "dotnet", "arguments": ["publish", "MyPlugin.csproj", "-c", "Release", "-r", "linux-x64", "--self-contained", "true", "-o", "bin/publish/linux-x64"], "output": "bin/publish/linux-x64" } }}A manifest platform without a target fails the build. The format is in build.
Services and configuration
Section titled “Services and configuration”The builder exposes the usual ASP.NET Core Services, Configuration, Logging and Environment.
Register services before Build(), then take them in the integration’s constructor:
using MacroDeck.Plugin.Hosting;using MacroDeck.Plugin.Serilog;using Microsoft.Extensions.DependencyInjection;
var builder = MacroDeckPlugin.CreatePlugin(args) .UseMacroDeckLogging() .RegisterIntegration<MyIntegration>();
builder.Services.AddHttpClient<WeatherClient>(client => client.BaseAddress = new Uri("https://api.example.com/"));builder.Services.Configure<WeatherOptions>(builder.Configuration.GetSection("Weather"));
var plugin = builder.Build();await plugin.RunAsync();public sealed class MyIntegration(WeatherClient weather, IOptions<WeatherOptions> options) : IPluginIntegration{ // ...}builder.WebApplicationBuilder is there as an escape hatch; prefer the Macro Deck APIs for
registration and lifecycle. Runtime behaviour - registration modes, lifecycle, reserved routes - is in
Plugin hosting.
Run it
Section titled “Run it”dotnet tool install --global MacroDeck.Plugin.Cli --prereleasemacrodeck-plugin run --project MyPlugin.csproj --stub-hostStarted a disposable stub host at http://127.0.0.1:52484....Session established (negotiated plugin protocol v3).A real in-process host, no Macro Deck install needed. Drop --stub-host to connect to the running
desktop app instead - see run.
macrodeck-plugin build --output ../artifactsmacrodeck-plugin validate --artifact ../artifacts/com.example.my-plugin-1.0.0.macroDeckPluginBuilds every platform, packs one .macroDeckPlugin and checks it. Validate the artifact, not the
source manifest: the runtimes/ entrypoints only exist once build has staged them.
Never zip the output by hand - build and pack validate the manifest and write the
files[] digests a hand-made ZIP lacks. For breakpoints against a real host, see
Debugging plugins; never commit a Developer token.
See also
Section titled “See also”- Quickstart - the same project, generated.
- Samples and template - complete plugins to read.
- Your first action - add behaviour to
MyIntegration. - Manifest reference - every field.
- Testing plugins - test the integration without a host.