Architecture Overview - Garume/Manifold GitHub Wiki

Architecture Overview

This page describes the high-level architecture of the Manifold framework, a source-generated .NET library that enables developers to define operations once and expose them through both CLI and MCP (Model Context Protocol) surfaces. The framework is organized into four NuGet packages with clearly defined responsibilities and dependency relationships, connected by a compile-time source generation pipeline that eliminates runtime reflection.

For the detailed directory layout and build tooling, see Project Structure and Tech Stack. For the foundational types that all packages share, see Core Contracts — Manifold Package.

Package Architecture

Manifold is structured as four packages that form a layered dependency graph. The core Manifold package defines contracts with zero external dependencies. The two runtime packages (Manifold.Cli and Manifold.Mcp) each depend on the core but not on each other, enabling host applications to reference only the surface they need. The Manifold.Generators source generator operates at compile time and has no runtime project references.

flowchart TD
    subgraph Compile["Compile Time"]
        GEN["Manifold.Generators<br/>(Source Generator)"]
    end

    subgraph Runtime["Runtime Packages"]
        CORE["Manifold<br/>(Core Contracts)"]
        CLI["Manifold.Cli<br/>(CLI Runtime)"]
        MCP["Manifold.Mcp<br/>(MCP Runtime)"]
    end

    subgraph Host["Host Applications"]
        CLIHOST["CLI Host"]
        MCPHOST["MCP Host"]
    end

    CLI -->|project reference| CORE
    MCP -->|project reference| CORE
    MCP -->|NuGet| MCPSDK["ModelContextProtocol SDK"]

    GEN -.->|generates code into| CLIHOST
    GEN -.->|generates code into| MCPHOST

    CLIHOST --> CLI
    CLIHOST --> CORE
    MCPHOST --> MCP
    MCPHOST --> CORE
Loading

Package Summary

Package Target Role Dependencies
Manifold .NET 10.0 Core contracts: IOperation, OperationDescriptor, OperationContext, attributes None
Manifold.Cli .NET 10.0 CLI parsing, dispatch, result formatting Manifold
Manifold.Mcp .NET 10.0 MCP tool invocation, JSON argument binding, response writing Manifold, ModelContextProtocol SDK
Manifold.Generators netstandard2.0 Roslyn incremental source generator Roslyn APIs (analyzer reference)

Sources: Manifold/src/Manifold/Manifold.csproj, Manifold/src/Manifold.Cli/Manifold.Cli.csproj, Manifold/src/Manifold.Mcp/Manifold.Mcp.csproj, Manifold/src/Manifold.Generators/Manifold.Generators.csproj

Core Contracts

The Manifold package defines the shared vocabulary used by all other packages. Every operation, regardless of surface, is described by the same metadata types.

Key Types

Type Kind Purpose
IOperation<TRequest, TResult> Interface Contract for class-based operations with typed request/result
OperationContext Class Per-invocation context carrying surface, services, and cancellation
OperationDescriptor Record Complete metadata for a single operation
ParameterDescriptor Record Metadata for a single parameter (name, type, source, position)
InvocationSurface Enum Identifies the calling surface: Cli, Mcp, or Protocol
OperationVisibility Enum Controls surface availability: Both, CliOnly, McpOnly
ParameterSource Enum Identifies binding origin: Option, Argument, Service, CancellationToken
classDiagram
    class IOperation~TRequest, TResult~ {
        +ExecuteAsync(TRequest, OperationContext) ValueTask~TResult~
    }

    class OperationContext {
        +OperationId string
        +Surface InvocationSurface
        +Services IServiceProvider
        +CancellationToken CancellationToken
        +ForCli() OperationContext
        +ForMcp() OperationContext
    }

    class OperationDescriptor {
        +OperationId string
        +DeclaringType Type
        +ResultType Type
        +Visibility OperationVisibility
        +Parameters IReadOnlyList~ParameterDescriptor~
        +CliCommandPath IReadOnlyList~string~
        +McpToolName string
    }

    class ParameterDescriptor {
        +Name string
        +ParameterType Type
        +Source ParameterSource
        +Required bool
        +Position int
    }

    OperationDescriptor --> "*" ParameterDescriptor
    IOperation~TRequest, TResult~ ..> OperationContext : receives
Loading

The OperationContext provides factory methods (ForCli, ForMcp, ForProtocol) that stamp the correct InvocationSurface value, allowing operation implementations to branch on which surface invoked them.

Sources: Manifold/src/Manifold/IOperation.cs:3-6, Manifold/src/Manifold/OperationContext.cs:3-84, Manifold/src/Manifold/DescriptorModels.cs:1-52

Source Generation Pipeline

The Manifold.Generators package contains an incremental Roslyn source generator (OperationDescriptorGenerator) that runs at compile time. It discovers all [Operation]-attributed methods and classes, validates them, and emits up to six generated source files depending on which runtime packages the host application references.

flowchart TD
    SRC["User Source Code<br/>[Operation] methods/classes"] --> ROSLYN["Roslyn Compilation"]
    ROSLYN --> GEN["OperationDescriptorGenerator<br/>IIncrementalGenerator"]

    GEN --> DISCOVER["Discover Candidates<br/>ForAttributeWithMetadataName"]
    DISCOVER --> VALIDATE["Validate & Analyze<br/>DMCF001-DMCF005"]
    VALIDATE --> COLLECT["Collect & Sort"]

    COLLECT --> REG["GeneratedOperationRegistry.g.cs"]
    COLLECT --> OPINV["GeneratedOperationInvoker.g.cs"]
    COLLECT --> CLIINV["GeneratedCliInvoker.g.cs"]
    COLLECT --> MCPTOOLS["GeneratedMcpTools.g.cs"]
    COLLECT --> MCPCAT["GeneratedMcpCatalog.g.cs"]
    COLLECT --> MCPINV["GeneratedMcpInvoker.g.cs"]

    style REG fill:#e8f5e9
    style OPINV fill:#e8f5e9
    style CLIINV fill:#e8f5e9
    style MCPTOOLS fill:#e8f5e9
    style MCPCAT fill:#e8f5e9
    style MCPINV fill:#e8f5e9
Loading

Generated Artifacts

Each artifact is conditionally emitted based on whether the corresponding runtime types are available in the compilation:

Generated File Condition Contents
GeneratedOperationRegistry.g.cs Always Static OperationDescriptor[] array and TryFind() lookup
GeneratedOperationInvoker.g.cs IOperationInvoker exists Generic invoker for class-based operations
GeneratedCliInvoker.g.cs ICliInvoker exists Implements ICliInvoker, IFastSyncCliInvoker, IFastCliInvoker
GeneratedMcpTools.g.cs MCP SDK types exist MCP tool methods with [McpServerTool] attributes
GeneratedMcpCatalog.g.cs McpToolDescriptor exists Static McpToolDescriptor[] catalog
GeneratedMcpInvoker.g.cs MCP invoker types exist Implements IMcpToolInvoker, IFastMcpToolInvoker, IFastSyncMcpToolInvoker

The conditional emission means a CLI-only host that does not reference Manifold.Mcp will never have MCP invoker code generated, and vice versa. The GeneratedOperationRegistry is always emitted because both surfaces depend on it.

Sources: Manifold/src/Manifold.Generators/OperationDescriptorGenerator.cs:9-10, Manifold/src/Manifold.Generators/OperationDescriptorGenerator.cs:63-76, Manifold/src/Manifold.Generators/OperationDescriptorGenerator.cs:620-688

Discovery and Validation

The generator uses ForAttributeWithMetadataName to find all symbols decorated with [Operation]. Each candidate is classified as either a static method or a class implementing IOperation<TRequest, TResult>. The generator validates candidates at compile time and reports diagnostics for invalid configurations:

Diagnostic ID Description
Conflicting visibility DMCF001 Operation marked with both [CliOnly] and [McpOnly]
Conflicting binding DMCF002 Parameter marked with both [Option] and [Argument]
Unsupported binding DMCF003 Parameter missing a required binding attribute
Missing interface DMCF004 Class does not implement IOperation<TRequest, TResult>
Non-writable property DMCF005 Request property lacks a public init or set accessor

For the full diagnostics reference, see Diagnostics and Compile-Time Validation.

Sources: Manifold/src/Manifold.Generators/OperationDescriptorGenerator.cs:27-61

Dual-Surface Dispatch Model

Manifold's central design principle is that a single operation definition produces two independent dispatch paths: CLI and MCP. Each surface has its own invoker interface hierarchy, parameter binding strategy, and result type, but both ultimately call the same user-defined operation logic.

flowchart TD
    OP["[Operation] Definition"] --> GEN["Source Generator"]

    GEN --> CLINV["GeneratedCliInvoker"]
    GEN --> MCPINV["GeneratedMcpInvoker"]

    subgraph CLI["CLI Surface"]
        CLIAPP["CliApplication"] --> PARSE["Parse Arguments"]
        PARSE --> RESOLVE["Resolve Operation"]
        RESOLVE --> CLINV
        CLINV --> CLIBIND["CliBinding<br/>string → typed values"]
    end

    subgraph MCP_SURFACE["MCP Surface"]
        MCPHOST["MCP Host"] --> MCPSDK["MCP SDK"]
        MCPSDK --> MCPINV
        MCPINV --> MCPBIND["McpBinding<br/>JSON → typed values"]
    end

    CLIBIND --> EXEC["Operation.ExecuteAsync()"]
    MCPBIND --> EXEC
Loading

CLI Dispatch Pipeline

The CLI surface is driven by CliApplication, which accepts the generated OperationDescriptor list and GeneratedCliInvoker at construction. When ExecuteAsync is called, the application follows a tiered invocation strategy:

sequenceDiagram
    participant User
    participant App as CliApplication
    participant Fast as IFastSyncCliInvoker
    participant Async as IFastCliInvoker
    participant Slow as ICliInvoker

    User->>App: ExecuteAsync(args)
    App->>App: TryExecuteArrayFastPath()

    alt Fast sync path available
        App->>Fast: TryInvokeFastSync(tokens)
        Fast-->>App: FastCliInvocationResult
    else Fast async path available
        App->>Async: TryInvokeFast(tokens)
        Async-->>App: ValueTask of FastCliInvocationResult
    else Slow path
        App->>App: ParseArguments()
        App->>App: TryResolveOperation()
        App->>App: ParseCommandInput()
        App->>Slow: TryInvoke(operationId, options, arguments)
        Slow-->>App: ValueTask of CliInvocationResult
    end

    App->>User: Exit code + formatted output
Loading
  1. Fast sync path (IFastSyncCliInvoker): The generated invoker receives raw string[] tokens and performs command resolution, argument parsing, and invocation in a single synchronous call. Returns FastCliInvocationResult, a union struct that avoids heap allocation for common primitive types.

  2. Fast async path (IFastCliInvoker): Same as above but returns ValueTask<FastCliInvocationResult> for operations that require asynchronous execution.

  3. Slow path (ICliInvoker): Falls back to full argument parsing. CliApplication parses tokens into a dictionary of named options and a list of positional arguments, resolves the target operation by matching CliCommandPath, and delegates to ICliInvoker.TryInvoke. Returns CliInvocationResult containing the boxed result object.

The fast path is attempted first for string[] input. It bypasses the token parsing and option/argument decomposition entirely, relying on the generated invoker to handle everything inline.

Sources: Manifold/src/Manifold.Cli/CliApplication.cs:62-76, Manifold/src/Manifold.Cli/CliApplication.cs:94-149, Manifold/src/Manifold.Cli/CliApplication.cs:151-195, Manifold/src/Manifold.Cli/ICliInvoker.cs:3-13, Manifold/src/Manifold.Cli/IFastCliInvoker.cs:1-19

MCP Dispatch Pipeline

The MCP surface integrates with the ModelContextProtocol SDK. Host applications register the generated MCP server components via AddGeneratedMcpServer(), which wires up the generated tool methods, catalog, and invoker. The MCP SDK routes incoming tool calls to the generated McpServerTool methods, which in turn delegate to the GeneratedMcpInvoker.

sequenceDiagram
    participant Client as MCP Client
    participant SDK as MCP SDK
    participant Tools as GeneratedMcpTools
    participant Invoker as GeneratedMcpInvoker
    participant Binding as McpBinding
    participant Op as Operation Logic

    Client->>SDK: tools/call (toolName, JSON args)
    SDK->>Tools: Invoke generated tool method
    Tools->>Invoker: TryInvokeFastSync / TryInvokeFast / TryInvoke
    Invoker->>Binding: Parse JSON arguments
    Binding->>Binding: GetRequiredProperty / TryGetProperty
    Binding->>Binding: ParseInt32 / ParseString / ConvertValue
    Invoker->>Op: Execute operation
    Op-->>Invoker: Result
    Invoker-->>Tools: FastMcpInvocationResult
    Tools-->>SDK: TextContent response
    SDK-->>Client: Tool result
Loading

The MCP invoker follows the same three-tier pattern as the CLI invoker:

Interface Input Output Allocation
IFastSyncMcpToolInvoker string toolName, JsonElement? arguments FastMcpInvocationResult Zero-allocation for primitives
IFastMcpToolInvoker string toolName, JsonElement? arguments ValueTask<FastMcpInvocationResult> Zero-allocation for primitives
IMcpToolInvoker string toolName, JsonElement? arguments ValueTask<OperationInvocationResult> Boxed result

McpBinding provides typed parsing methods (ParseInt32, ParseString, ParseBoolean, etc.) and a general-purpose ConvertValue method that handles nullable types, enums, and complex JSON deserialization.

Sources: Manifold/src/Manifold.Mcp/McpInvoker.cs:7-35, Manifold/src/Manifold.Mcp/McpInvoker.cs:37-105

Parameter Binding Comparison

Both surfaces bind operation parameters from their respective input formats, but the binding mechanics differ:

Aspect CLI Surface MCP Surface
Input format string[] command-line tokens JsonElement from MCP protocol
Options --name value parsed into IReadOnlyDictionary<string, string> JSON object properties
Arguments Positional strings in IReadOnlyList<string> JSON object properties (no positional concept)
Type conversion CliBinding.ConvertValue(Type, string, name) McpBinding.ConvertValue(Type, JsonElement, name)
Service injection CliBinding.GetRequiredService<T>(IServiceProvider) McpBinding.GetRequiredService<T>(IServiceProvider)
Name resolution [CliName] override or parameter name [McpName] override or parameter name

For the full parameter binding reference, see Parameter Binding and Type Conversion.

Host Application Wiring

CLI Host

A CLI host wires CliApplication with the generated registry and invoker:

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: Manifold/samples/Manifold.Samples.CliHost/Program.cs:1-22

MCP Stdio Host

An MCP stdio host uses the DI extension to register all generated components:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<WeatherPreviewOperation>();
builder.Services.AddGeneratedMcpServer()
    .WithStdioServerTransport();

IHost host = builder.Build();
await host.RunAsync();

Sources: Manifold/samples/Manifold.Samples.McpStdioHost/Program.cs:1-12

MCP HTTP Host

An MCP HTTP host uses ASP.NET Core with the HTTP transport:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<WeatherPreviewOperation>();
builder.Services.AddGeneratedMcpServer()
    .WithHttpTransport();

WebApplication app = builder.Build();
app.MapMcp("/mcp");
await app.RunAsync();

Sources: Manifold/samples/Manifold.Samples.McpHttpHost/Program.cs:1-19

For detailed walkthroughs, see Sample — CLI Host and Samples — MCP Hosts (Stdio and HTTP).

End-to-End Operation Lifecycle

The following diagram shows the complete lifecycle from operation definition through compile-time generation to runtime invocation on both surfaces:

flowchart TD
    subgraph Author["Author Time"]
        DEF["Define Operation<br/>[Operation] + [CliCommand] + [McpTool]"]
    end

    subgraph Compile["Compile Time"]
        DEF --> GEN["OperationDescriptorGenerator"]
        GEN --> DIAG{"Validation<br/>Passed?"}
        DIAG -->|No| ERR["Compile Error<br/>DMCF001-005"]
        DIAG -->|Yes| EMIT["Emit Generated Code"]
        EMIT --> REG["OperationRegistry"]
        EMIT --> CLINV["CliInvoker"]
        EMIT --> MCPINV["McpInvoker + Tools"]
    end

    subgraph Runtime["Runtime"]
        subgraph CLISurface["CLI Surface"]
            CLIAPP["CliApplication<br/>+ OperationRegistry<br/>+ CliInvoker"]
            CLIARGS["args: string[]"] --> CLIAPP
            CLIAPP --> CLIFAST{"Fast Path?"}
            CLIFAST -->|Yes| CLISYNC["Sync/Async Fast Invoke"]
            CLIFAST -->|No| CLISLOW["Parse → Resolve → Invoke"]
        end

        subgraph MCPSurface["MCP Surface"]
            MCPAPP["MCP Server<br/>+ McpTools<br/>+ McpInvoker"]
            MCPREQ["JSON tool call"] --> MCPAPP
            MCPAPP --> MCPFAST{"Fast Path?"}
            MCPFAST -->|Yes| MCPSYNC["Sync/Async Fast Invoke"]
            MCPFAST -->|No| MCPSLOW["Parse JSON → Invoke"]
        end

        CLISYNC --> EXEC["User Operation Logic"]
        CLISLOW --> EXEC
        MCPSYNC --> EXEC
        MCPSLOW --> EXEC

        EXEC --> RESULT["Operation Result"]
        RESULT --> CLIFMT["CLI Formatting<br/>Text / JSON"]
        RESULT --> MCPFMT["MCP Formatting<br/>TextContent"]
    end
Loading

Key Architectural Decisions

Decision Rationale
Source generation over reflection Eliminates runtime reflection, enables AOT compilation, and produces compile-time errors for invalid operations
Conditional code emission Generated artifacts depend on which runtime packages are referenced, preventing unused code
Three-tier invoker hierarchy Fast sync → fast async → slow path provides zero-allocation hot paths while maintaining full-featured fallback
Frozen dictionary dispatch CliApplication pre-computes a FrozenDictionary of command candidates for O(1) lookup at runtime
Core package has zero dependencies Ensures the contract library can be referenced without pulling in CLI or MCP runtime dependencies
Separate CLI and MCP naming [CliName]/[McpName] and [CliCommand]/[McpTool] allow each surface to use idiomatic naming conventions

Sources: Manifold/src/Manifold.Cli/CliApplication.cs:23-24, Manifold/src/Manifold/OperationAttribute.cs:1-15

Related Pages

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