Skip to content

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.

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 is

new generates the same project under src/MyPlugin/, plus a solution, a test project, a Localization/ resource set and central package versions.

<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.Sdk plus the ASP.NET Core framework reference, not Microsoft.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.Analyzers is optional but recommended: it reports invalid declarations at build time. See Analyzers.
  • Both Content items are required. The SDK reads manifest.json from the content root at startup and resolves the icon against it.
{
"$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.dll",
"runtime": { "kind": "FrameworkDependent", "dotnetVersion": "10.0" }
},
"osx-arm64": {
"executable": "runtimes/osx-arm64/MyPlugin.dll",
"runtime": { "kind": "FrameworkDependent", "dotnetVersion": "10.0" }
},
"linux-x64": {
"executable": "runtimes/linux-x64/MyPlugin.dll",
"runtime": { "kind": "FrameworkDependent", "dotnetVersion": "10.0" }
}
}
}
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 platform’s publish. The entrypoints are framework-dependent: Macro Deck runs them on the .NET runtime it ships, so the package stays small (see Runtime, including when to publish self-contained instead). 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.

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.
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.

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", "false", "-p:UseAppHost=false",
"-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", "false", "-p:UseAppHost=false",
"-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", "false", "-p:UseAppHost=false",
"-o", "bin/publish/linux-x64"],
"output": "bin/publish/linux-x64"
}
}
}

A manifest platform without a target fails the build. The format is in build. -r <rid> keeps each publish to that platform’s native assets; -p:UseAppHost=false skips the native launcher a framework-dependent entrypoint does not use.

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.

Terminal window
dotnet tool install --global MacroDeck.Plugin.Cli --prerelease
Terminal window
macrodeck-plugin run --project MyPlugin.csproj --stub-host
Started 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.

Terminal window
macrodeck-plugin build --output ../artifacts
macrodeck-plugin validate --artifact ../artifacts/com.example.my-plugin-1.0.0.macroDeckPlugin

Builds 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.