Compile a Model Context Protocol® (MCP) tool server to a platform-specific native executable.
dotnet add package RecursiveAI.OneModelContextRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using RecursiveAI.OneModelContext; await ModelContext.ServeAsync(new Calculator()); public partial class Calculator { [Tool] public int Add(int a, int b) => a + b; }
Publish the downloaded project for Linux x64, then run the native executable:
dotnet publish -c Release -r linux-x64 -o publish ./publish/project
The binary is named after the project: the download has project.csproj, so its executable is project (project.exe on Windows). To serve an external client, run ./publish/project --stdio; waiting silently for protocol input is normal.
Unzip the example, then dotnet run.
The snippet at the top serves over standard input and output, with no application text. The core NuGet package includes the generator; it needs a partial class.
The download includes these lines inside the project file’s <PropertyGroup>:
<PublishAot>true</PublishAot> <InvariantGlobalization>true</InvariantGlobalization>
dotnet run exercises the tool through an in-memory client and exits. Add --stdio to serve an external client instead. A native publish also requires the platform’s Native AOT build prerequisites.
The full program
using System.IO.Pipelines; using RecursiveAI.OneModelContext; if (args.Contains("--stdio")) { await ModelContext.ServeAsync(new ExampleTools()); return; } var toServer = new Pipe(); var toClient = new Pipe(); var server = new ContextServer("example", "1.0.0"); new ExampleTools().AddTo(server); using var stop = new CancellationTokenSource(); var serving = server.RunAsync(ContextTransport.Stream( toServer.Reader.AsStream(), toClient.Writer.AsStream()), stop.Token); await using var client = await ModelContext.ConnectAsync( ContextTransport.Stream(toClient.Reader.AsStream(), toServer.Writer.AsStream())); Console.WriteLine((await client.CallToolAsync("add", System.Text.Json.JsonDocument.Parse("""{"a":2,"b":40}""").RootElement)).Text); await stop.CancelAsync(); try { await serving; } catch (OperationCanceledException) { } public partial class ExampleTools { [Tool(ReadOnly = true)] public int Add(int a, int b) => a + b; }
dotnet run prints:
42Variations
The publish command above builds a native version of the downloaded program. Running ./publish/project prints:
42To wait for an external stdio client, use ./publish/project --stdio instead. Keep standard output reserved for the protocol in this mode.
Related recipes
The short answer
To publish an MCP server with Native AOT (Model Context Protocol®), use OneModelContext’s generated [Tool] registration and enable PublishAot in a .NET 10 project. Publish for the operating system and architecture where the server will run. The downloadable source works both as a native server and as a small client/server demonstration.
How do I build a Native AOT server in C#?
Start with a console application and install the core package. Mark the tool class partial, then add [Tool] to its methods. The generator supplies schema and invocation code at build time. The calculator takes two integers and returns their sum; it has no user-defined types requiring additional serialization metadata.
The project file in the download contains the AOT property. dotnet run still builds and runs managed code; it is not the command that produces the native deployment artifact. Use dotnet publish, as shown above, to produce the native executable. Running that published executable exercises the same protocol exchange and returns 42.
For an external consuming application, select the --stdio branch. The native binary then waits for protocol input instead of starting the demonstration client. Configure the consumer to launch that binary with the argument. A silent console in this mode is expected: application text would corrupt the protocol stream.
What changes for Native AOT tool registration?
The generated path and reflection quick path are not interchangeable for deployment. OneModelContext’s tool-schema documentation documents generated invokers that use source-generated System.Text.Json metadata. A missing context for a user-defined record or collection is a build diagnostic, not a runtime switch to reflection.
Keep tool and serializer classes partial when adding more complex signatures. A record result needs a JsonSerializerContext that lists it. This is particularly important when code works during development but would otherwise discover types dynamically at runtime. The structured-output recipe shows the complete record-and-context arrangement.
The download uses explicit JSON arguments for its client call. That avoids making its demonstration depend on runtime discovery of an anonymous argument type. It also keeps the native example focused on generated server tools, not on a separate model integration.
How do I publish for another operating system?
Choose a runtime identifier appropriate to the target, and build in an environment supported by the .NET Native AOT deployment guide. The command shown here was run for Linux x64. It is not a universal executable for every platform. Microsoft’s guide also lists the native compiler and system prerequisites; installing the .NET SDK alone is not the whole native toolchain.
The project enables invariant globalization for this integer-only example. That is an application choice, not a requirement that every tool server ignore culture. If your tools format dates, compare localized text or use culture-specific rules, review that setting alongside the platform’s globalization requirements before reusing it.
Keep deployment and transport decisions separate. Native compilation changes the artifact you ship. Stdio still means the client starts a process and owns its input and output streams. A native executable does not turn that process into a remotely reachable HTTP service.