Sample CLI Host - Garume/Manifold GitHub Wiki

Sample — CLI Host

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.

Project Structure

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

Project Dependencies

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

Complete Program Walkthrough

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

Step-by-Step Breakdown

1. Configure Dependency Injection

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.

2. Construct the CliApplication

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

3. Execute from Command-Line Arguments

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

Generated Components

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.

GeneratedOperationRegistry

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

GeneratedCliInvoker

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

Execution Flow

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
Loading

Fast-Path vs Slow-Path Dispatch

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

Sample Operations

The CLI host uses operations defined in the shared Manifold.Samples.Operations project. Two operations are included:

Static Method: math.add

[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

Class-Based: weather.preview

[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

Command-Line Usage Examples

Basic Operation Invocation

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

Built-In Flags

# 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 Codes

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

Wiring Diagram

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
Loading

Comparison with MCP Host Samples

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

Key Design Decisions

  1. Top-level statements: The sample uses C# top-level statements to minimize boilerplate, making the wiring pattern immediately visible.

  2. Raw output stream: The rawOutput: Console.OpenStandardOutput() parameter provides a direct byte stream for JSON output, avoiding the overhead of UTF-8 encoding through TextWriter for the --json mode.

  3. Custom JSON options: The sample configures JsonSerializerDefaults.Web with WriteIndented = true for human-readable JSON output. If not provided, CliApplication falls back to the same defaults internally.

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

Related Pages

⚠️ **GitHub.com Fallback** ⚠️