Sample CLI Host - Garume/Manifold GitHub Wiki
The CLI host sample (Manifold.Samples.CliHost) demonstrates the minimal wiring required to build a command-line application powered by Manifold. It shows how to connect the source-generated GeneratedOperationRegistry and GeneratedCliInvoker to the CliApplication entry point, configure dependency injection, and dispatch operations from command-line arguments. This page provides a line-by-line walkthrough of the sample, explains the role of each component, and describes the runtime execution flow.
The sample resides in the samples/Manifold.Samples.CliHost directory and depends on the shared Manifold.Samples.Operations project for its operation definitions. For background on the CLI runtime, see CLI Runtime — Manifold.Cli. For details on the source generator that produces the registry and invoker, see Source Generator — Manifold.Generators. For the shared sample operations themselves, see Sample Operations Reference.
The CLI host sample consists of a single Program.cs file and a project file that references the core Manifold packages.
samples/
├── Manifold.Samples.CliHost/
│ ├── Program.cs ← Application entry point
│ └── Manifold.Samples.CliHost.csproj ← Project configuration
└── Manifold.Samples.Operations/
├── SampleOperations.cs ← Shared operation definitions
└── Manifold.Samples.Operations.csproj ← References Manifold + source generator
| Dependency | Purpose |
|---|---|
Manifold.Cli |
Provides CliApplication, ICliInvoker, and the CLI dispatch pipeline |
Manifold.Samples.Operations |
Contains shared operation definitions annotated with [Operation], [CliCommand], [McpTool]
|
Microsoft.Extensions.DependencyInjection (v10.0.0) |
Provides ServiceCollection and IServiceProvider for service resolution |
Sources: samples/Manifold.Samples.CliHost/Manifold.Samples.CliHost.csproj:1-12
The Manifold.Samples.Operations project itself references Manifold, Manifold.Cli, Manifold.Mcp, and the Manifold.Generators project as a source generator (via OutputItemType="Analyzer"). This means the source generator runs during compilation of the operations project and emits GeneratedOperationRegistry, GeneratedCliInvoker, and MCP-related generated types into the Manifold.Generated namespace.
Sources: samples/Manifold.Samples.Operations/Manifold.Samples.Operations.csproj:1-14
The entire CLI host application is 22 lines of top-level C# code:
using System.Text.Json;
using Manifold.Cli;
using Manifold.Generated;
using Manifold.Samples.Operations;
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
services.AddTransient<WeatherPreviewOperation>();
IServiceProvider serviceProvider = services.BuildServiceProvider();
CliApplication application = new(
GeneratedOperationRegistry.Operations,
new GeneratedCliInvoker(),
serviceProvider,
rawOutput: Console.OpenStandardOutput(),
jsonSerializerOptions: new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = true
});
return await application.ExecuteAsync(args, Console.Out, Console.Error);Sources: samples/Manifold.Samples.CliHost/Program.cs:1-22
ServiceCollection services = new();
services.AddTransient<WeatherPreviewOperation>();
IServiceProvider serviceProvider = services.BuildServiceProvider();A ServiceCollection is created and the WeatherPreviewOperation class is registered as a transient service. This is necessary because WeatherPreviewOperation is a class-based operation (implementing IOperation<TRequest, TResult>) and the generated invoker resolves it from the service provider at invocation time. Static-method operations like SampleOperations.Add do not require service registration.
Sources: samples/Manifold.Samples.CliHost/Program.cs:7-10, samples/Manifold.Samples.Operations/SampleOperations.cs:19-40
For full details on service injection patterns, see Dependency Injection and Service Resolution.
CliApplication application = new(
GeneratedOperationRegistry.Operations,
new GeneratedCliInvoker(),
serviceProvider,
rawOutput: Console.OpenStandardOutput(),
jsonSerializerOptions: new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = true
});The CliApplication constructor accepts five parameters:
| Parameter | Type | Description |
|---|---|---|
operations |
IReadOnlyList<OperationDescriptor> |
The list of all registered operations, provided by the generated registry |
cliInvoker |
ICliInvoker |
The generated invoker that dispatches CLI calls to operation methods |
services |
IServiceProvider? |
Optional service provider for resolving class-based operations and [FromServices] parameters |
rawOutput |
Stream? |
Optional raw byte stream for efficient JSON output (bypasses TextWriter encoding) |
jsonSerializerOptions |
JsonSerializerOptions? |
Optional JSON serialization settings for --json output mode |
Sources: src/Manifold.Cli/CliApplication.cs:25-46
return await application.ExecuteAsync(args, Console.Out, Console.Error);The ExecuteAsync method receives the raw args array, a TextWriter for standard output, and a TextWriter for standard error. It returns an integer exit code that becomes the process exit code via return.
Sources: src/Manifold.Cli/CliApplication.cs:62-76
The source generator (Manifold.Generators) analyzes all [Operation]-attributed types and methods in the compilation and emits two key artifacts consumed by the CLI host.
A static class that exposes Operations — an IReadOnlyList<OperationDescriptor> containing metadata for every discovered operation. Each OperationDescriptor includes the operation ID, declaring type, result type, visibility, parameter descriptors, CLI command path, and MCP tool name.
public static class GeneratedOperationRegistry
{
private static readonly OperationDescriptor[] operations = [ ... ];
public static IReadOnlyList<OperationDescriptor> Operations => operations;
public static bool TryFind(string operationId, out OperationDescriptor? descriptor) { ... }
}Sources: src/Manifold.Generators/OperationDescriptorGenerator.cs:697-722
A sealed class that implements three interfaces for CLI dispatch:
| Interface | Purpose |
|---|---|
ICliInvoker |
Standard invocation path — receives parsed options and arguments, returns CliInvocationResult
|
IFastSyncCliInvoker |
Synchronous fast-path — operates directly on string[] command tokens, returns FastCliInvocationResult
|
IFastCliInvoker |
Asynchronous fast-path — same as above but returns ValueTask<FastCliInvocationResult>
|
public sealed class GeneratedCliInvoker
: ICliInvoker, IFastSyncCliInvoker, IFastCliInvoker
{
public bool TryInvokeFastSync(string[] commandTokens, IServiceProvider? services,
CancellationToken cancellationToken, out FastCliInvocationResult invocation) { ... }
public bool TryInvokeFast(string[] commandTokens, IServiceProvider? services,
CancellationToken cancellationToken, out ValueTask<FastCliInvocationResult> invocation) { ... }
public bool TryInvoke(string operationId, IReadOnlyDictionary<string, string> options,
IReadOnlyList<string> arguments, IServiceProvider? services, bool jsonRequested,
CancellationToken cancellationToken, out ValueTask<CliInvocationResult> invocation) { ... }
}Sources: src/Manifold.Generators/OperationDescriptorGenerator.cs:769-790, src/Manifold.Cli/ICliInvoker.cs:1-13, src/Manifold.Cli/IFastCliInvoker.cs:1-19
The following diagram shows the complete execution flow from command-line arguments to output:
flowchart TD
A["Program.cs entry"] --> B["Configure DI"]
B --> C["Build ServiceProvider"]
C --> D["Construct CliApplication"]
D --> E["ExecuteAsync with args"]
E --> F{"Array fast-path?"}
F -->|Yes, sync| G["TryInvokeFastSync"]
F -->|Yes, async| H["TryInvokeFast"]
F -->|No| I["ParseArguments"]
G --> J["WriteFastResult"]
H --> J
I --> K{"--help flag?"}
K -->|Yes| L["Print usage text"]
K -->|No| M["TryResolveOperation"]
M --> N{"Found?"}
N -->|No| O["Print unknown command"]
N -->|Yes| P["ParseCommandInput"]
P --> Q["ICliInvoker.TryInvoke"]
Q --> R["WriteResultAsync"]
J --> S["Return exit code"]
L --> S
O --> S
R --> S
The CliApplication attempts fast-path dispatch first. The fast path operates directly on the raw string[] command tokens without parsing into separate options and arguments. The generated invoker pattern-matches command tokens using ordinal string comparisons and inline parameter binding.
If the fast path cannot handle the invocation (e.g., --json or --help flags are present, or the invoker returns false), the slow path takes over. The slow path parses arguments into a structured ParsedArguments record, resolves the operation via a frozen dictionary lookup, validates options and arguments, and delegates to ICliInvoker.TryInvoke.
Sources: src/Manifold.Cli/CliApplication.cs:72-76, src/Manifold.Cli/CliApplication.cs:151-195
The CLI host uses operations defined in the shared Manifold.Samples.Operations project. Two operations are included:
[Operation("math.add", Description = "Add two integers.", Summary = "Returns the sum of x and y.")]
[CliCommand("math", "add")]
[McpTool("math_add")]
public static int Add(
[Argument(0, Name = "x", Description = "Left operand")] int x,
[Argument(1, Name = "y", Description = "Right operand")] int y)
{
return x + y;
}This operation uses positional arguments and returns a synchronous int result. It does not require service registration.
Sources: samples/Manifold.Samples.Operations/SampleOperations.cs:5-13
[Operation("weather.preview", Description = "Return a pretend weather summary.")]
[CliCommand("weather", "preview")]
[McpTool("weather_preview")]
public sealed class WeatherPreviewOperation : IOperation<WeatherPreviewOperation.Request, string>
{
public ValueTask<string> ExecuteAsync(Request request, OperationContext context) { ... }
public sealed class Request
{
[Option("city", Description = "Target city")]
public string City { get; init; } = string.Empty;
[Option("days", Description = "Number of forecast days")]
public int Days { get; init; } = 3;
}
}This operation uses named options bound via a nested Request class and returns an async string result. It must be registered in the DI container (services.AddTransient<WeatherPreviewOperation>()).
Sources: samples/Manifold.Samples.Operations/SampleOperations.cs:16-40
# Add two numbers (positional arguments)
dotnet run -- math add 3 5
# Output: 8
# Weather forecast (named options)
dotnet run -- weather preview --city Tokyo --days 7
# Output: Forecast for Tokyo: mild for the next 7 day(s). Surface=Cli.# Show help
dotnet run -- --help
# Output: Usage listing of all commands
# Command-specific help
dotnet run -- math add --help
# Output: Usage: app math add <x> <y>
# JSON output mode
dotnet run -- --json math add 3 5
# Output: 8 (as JSON)| Exit Code | Constant | Meaning |
|---|---|---|
0 |
CliExitCodes.Success |
Operation completed successfully |
1 |
CliExitCodes.UnhandledFailure |
Unhandled exception occurred |
2 |
CliExitCodes.UsageError |
Invalid arguments or unknown command |
3 |
CliExitCodes.Unavailable |
Operation could not be invoked |
Sources: src/Manifold.Cli/CliExitCodes.cs:1-9
The following diagram shows how the components connect at application startup:
flowchart TD
subgraph Build Time
A["[Operation] attributes"] --> B["Source Generator"]
B --> C["GeneratedOperationRegistry"]
B --> D["GeneratedCliInvoker"]
end
subgraph Runtime
E["ServiceCollection"] --> F["IServiceProvider"]
C -->|Operations list| G["CliApplication"]
D -->|ICliInvoker| G
F -->|Service resolution| G
G --> H["ExecuteAsync"]
H --> I["Console output"]
end
The CLI host sample follows a simpler pattern compared to the MCP host samples. While MCP hosts require transport configuration (stdio or HTTP), the CLI host only needs Console.Out, Console.Error, and the args array.
| Aspect | CLI Host | MCP Stdio Host | MCP HTTP Host |
|---|---|---|---|
| Entry point | CliApplication |
Host.CreateDefaultBuilder |
WebApplication.CreateBuilder |
| Generated invoker | GeneratedCliInvoker |
MCP catalog + invoker | MCP catalog + invoker |
| Transport | Process stdin/stdout | Stdio pipe | HTTP / Streamable HTTP |
| DI setup | Manual ServiceCollection
|
Host builder services | Host builder services |
| Output format | Text or JSON | MCP protocol JSON | MCP protocol JSON |
For details on the MCP host samples, see Samples — MCP Hosts (Stdio and HTTP).
-
Top-level statements: The sample uses C# top-level statements to minimize boilerplate, making the wiring pattern immediately visible.
-
Raw output stream: The
rawOutput: Console.OpenStandardOutput()parameter provides a direct byte stream for JSON output, avoiding the overhead of UTF-8 encoding throughTextWriterfor the--jsonmode. -
Custom JSON options: The sample configures
JsonSerializerDefaults.WebwithWriteIndented = truefor human-readable JSON output. If not provided,CliApplicationfalls back to the same defaults internally. -
Explicit service registration: Only class-based operations need DI registration. The generated invoker resolves them via
IServiceProvider.GetRequiredService<T>()at invocation time.
Sources: src/Manifold.Cli/CliApplication.cs:39-44
- Architecture Overview — High-level system architecture and the dual-surface dispatch model
- CLI Runtime — Manifold.Cli — Complete documentation of the CLI dispatch pipeline
-
Source Generator — Manifold.Generators — How
GeneratedOperationRegistryandGeneratedCliInvokerare emitted - Sample Operations Reference — Documentation of the shared sample operations
- Samples — MCP Hosts (Stdio and HTTP) — The MCP host counterparts to this CLI sample
- Dependency Injection and Service Resolution — Service injection patterns for operations
-
Attributes and Operation Definition — Reference for
[Operation],[CliCommand], and parameter attributes - Result Types and Formatting — How results are formatted for CLI and JSON output
- Performance and Benchmarks — Fast-path invocation design and benchmark results
-
Core Contracts — Manifold Package —
OperationDescriptor,ParameterDescriptor, and other core types