Parameter Binding and Type Conversion - Garume/Manifold GitHub Wiki

Parameter Binding and Type Conversion

Parameter binding is the process by which Manifold maps raw input values — command-line tokens or JSON properties — to the strongly-typed parameters of an operation method or request object. This page provides comprehensive documentation of how parameters are classified, resolved, and converted across both the CLI and MCP surfaces, including service injection, cancellation token threading, alias resolution, and surface-specific naming overrides.

Parameter binding is a cross-cutting concern that spans all four Manifold packages. The Core Contracts package defines the ParameterDescriptor and ParameterSource types. The Source Generator analyzes parameter attributes at compile time and emits binding code into both the CLI Runtime and MCP Runtime invokers. For service injection specifics, see Dependency Injection and Service Resolution.

Parameter Source Classification

Every parameter in an operation is classified into exactly one ParameterSource. The source generator determines this classification at compile time by inspecting attributes and types on each parameter or request property.

ParameterSource Enum

public enum ParameterSource
{
    Option = 0,
    Argument = 1,
    Service = 2,
    CancellationToken = 3
}

Sources: Manifold/DescriptorModels.cs:18-24

Classification Rules

The source generator applies the following decision logic for each parameter:

flowchart TD
    A[Analyze Parameter] --> B{Is CancellationToken?}
    B -->|Yes| C[Source = CancellationToken]
    B -->|No| D{Has FromServices?}
    D -->|Yes| E[Source = Service]
    D -->|No| F{Has Option attr?}
    F -->|Yes| G{Has Argument attr?}
    G -->|Yes| H[DMCF002 Diagnostic]
    G -->|No| I[Source = Option]
    F -->|No| J{Has Argument attr?}
    J -->|Yes| K[Source = Argument]
    J -->|No| L[DMCF003 Diagnostic]
Loading

The generator enforces mutual exclusivity: a parameter annotated with both [Option] and [Argument] produces the DMCF002 compile-time diagnostic. A parameter that matches none of the four sources produces DMCF003. See Diagnostics and Compile-Time Validation for full details.

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:289-353

ParameterDescriptor Model

The ParameterDescriptor record captures all metadata needed to describe a parameter at runtime. It is emitted into the GeneratedOperationRegistry by the source generator.

public sealed record ParameterDescriptor(
    string Name,
    Type ParameterType,
    ParameterSource Source,
    bool Required,
    int? Position = null,
    string? Description = null,
    IReadOnlyList<string>? Aliases = null,
    string? CliName = null,
    string? McpName = null,
    string? RequestPropertyName = null);

Sources: Manifold/DescriptorModels.cs:26-36

Field Purpose
Name Primary parameter name, derived from the [Option] or [Argument] attribute (or the C# parameter name)
ParameterType The .NET Type of the parameter
Source One of Option, Argument, Service, CancellationToken
Required Whether the parameter must be provided; defaults to true for both [Option] and [Argument]
Position Zero-based positional index; only set for Argument source
Description Help text displayed in CLI help and MCP tool schemas
Aliases Alternative names for options, from [Alias] attributes
CliName Surface-specific CLI name override from [CliName]
McpName Surface-specific MCP name override from [McpName]
RequestPropertyName Maps to the property name on an IOperation<TRequest, TResult> request object

Binding Attributes

Manifold provides a set of attributes to declare how each parameter should be bound. All attributes are defined in the core Manifold package.

[Option] Attribute

Marks a parameter as a named option, bound via --name value or --name=value syntax in CLI, or as a named JSON property in MCP.

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property,
    AllowMultiple = false, Inherited = false)]
public class OptionAttribute(string name) : Attribute
{
    public string Name { get; }
    public string? Description { get; set; }
    public bool Required { get; set; } = true;
}

Sources: Manifold/ParameterAttributes.cs:3-13

[Argument] Attribute

Marks a parameter as a positional argument, bound by its zero-based position in the CLI argument list or as a named JSON property in MCP.

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property,
    AllowMultiple = false, Inherited = false)]
public sealed class ArgumentAttribute(int position) : Attribute
{
    public int Position { get; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public bool Required { get; set; } = true;
}

Sources: Manifold/ParameterAttributes.cs:15-27

[FromServices] Attribute

Marks a parameter for dependency injection from the IServiceProvider.

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public sealed class FromServicesAttribute : Attribute;

Sources: Manifold/ParameterAttributes.cs:91-92

[Alias] Attribute

Defines alternative names for an option. Multiple [Alias] attributes can be applied. Aliases are normalized: trimmed, deduplicated (case-insensitive), and filtered for empty values.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class
    | AttributeTargets.Parameter | AttributeTargets.Property,
    AllowMultiple = true, Inherited = false)]
public sealed class AliasAttribute(params string[] aliases) : Attribute
{
    public string[] Aliases { get; } = Normalize(aliases);
}

Sources: Manifold/ParameterAttributes.cs:29-48

[CliName] and [McpName] Attributes

Override the display name of a parameter for a specific surface, allowing different naming conventions on CLI vs MCP without changing the underlying parameter identity.

public sealed class CliNameAttribute(string name) : Attribute
{
    public string Name { get; }
}

public sealed class McpNameAttribute(string name) : Attribute
{
    public string Name { get; }
}

Sources: Manifold/ParameterAttributes.cs:50-64

CLI Parameter Binding

The CLI surface binds parameters from command-line tokens. The source generator produces two binding paths: a fast path (zero-allocation, inline parsing) and a standard path (reflection-based ConvertValue).

Binding Pipeline Overview

flowchart TD
    A[Command-Line Tokens] --> B[Parse & Route Command]
    B --> C[Split Options & Arguments]
    C --> D{Parameter Source?}
    D -->|Option| E[Match --name or alias]
    D -->|Argument| F[Match by position]
    D -->|Service| G[Resolve from IServiceProvider]
    D -->|CancellationToken| H[Pass cancellationToken]
    E --> I[Convert string to type]
    F --> I
    I --> J[Invoke Operation Method]
    G --> J
    H --> J
Loading

Option Binding

Options are named parameters prefixed with --. The generated CLI invoker supports two value formats:

  1. Space-separated: --name value
  2. Inline (equals): --name=value

Option matching is case-insensitive. The generated fast-path code uses a discriminator switch on the lowercased third character of the token for efficient dispatch:

// Generated: case-insensitive option matching
if (current == "--city" ||
    string.Equals(current, "--city", StringComparison.OrdinalIgnoreCase))
{
    string value = CliBinding.ParseRequiredOptionValue(commandTokens, ref index, "city");
    __uops_city = CliBinding.ParseString(value, "city");
    continue;
}

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:1279-1340

Alias Resolution in CLI

When looking up an option value, the runtime first checks the primary name, then iterates through aliases:

public static bool TryFindOptionValue(
    IReadOnlyDictionary<string, string> options,
    string name,
    IReadOnlyList<string>? aliases,
    out string? value)
{
    if (options.TryGetValue(name, out value))
        return true;

    if (aliases is not null)
    {
        foreach (string alias in aliases)
        {
            if (options.TryGetValue(alias, out value))
                return true;
        }
    }

    value = null;
    return false;
}

In the fast-path invoker, aliases are expanded at code generation time into additional match branches alongside the primary option name.

Sources: Manifold.Cli/CliBinding.cs:214-237, Manifold.Generators/OperationDescriptorGenerator.cs:1250-1253

Argument Binding

Arguments are matched by their zero-based position in the token list. The fast-path invoker uses a switch (argumentIndex) statement that increments as each positional value is consumed:

// Generated: positional argument binding
switch (argumentIndex)
{
    case 0:
        __uops_x = CliBinding.ParseInt32(current, "x");
        __uops_xSet = true;
        argumentIndex++;
        continue;
    case 1:
        __uops_y = CliBinding.ParseInt32(current, "y");
        __uops_ySet = true;
        argumentIndex++;
        continue;
}

In the standard path, required arguments are fetched with CliBinding.GetRequiredArgument(arguments, position, displayName) which throws if the position is out of bounds.

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:1343-1359, Manifold.Cli/CliBinding.cs:200-212

Required vs Optional Parameters

For required parameters, the generated code tracks whether a value was set and throws an ArgumentException if it was not. For optional parameters, the bound variable is initialized to default (value types) or default! (reference types) and no validation is performed.

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2300-2346

MCP Parameter Binding

The MCP surface binds parameters from a JsonElement? arguments object. Like the CLI surface, the source generator produces optimized binding code with fast parsers for supported types.

Binding Pipeline Overview

flowchart TD
    A[JsonElement? arguments] --> B[TryGetObject]
    B -->|Valid JSON Object| C{Parameter Source?}
    B -->|Null/Undefined| D[Use defaults or throw]
    C -->|Option/Argument| E[TryGetProperty by name]
    C -->|Service| F[Resolve from IServiceProvider]
    C -->|CancellationToken| G[Pass cancellationToken]
    E -->|Found| H[Parse JsonElement to type]
    E -->|Not Found, Required| I[Throw ArgumentException]
    E -->|Not Found, Optional| J[Use default value]
    H --> K[Invoke Operation Method]
    F --> K
    G --> K
    J --> K
Loading

JSON Property Lookup

MCP parameters are looked up by name as properties of the JSON arguments object. The generated code uses UTF-8 byte literals for efficient property name matching:

// Generated: required MCP argument binding
if (!__uops_internal_mcpHasArgumentObject ||
    !__uops_internal_mcpArgumentObject.TryGetProperty("city"u8, out JsonElement __uops_cityElement))
    throw new ArgumentException("Missing required MCP argument 'city'.");
string __uops_city = McpBinding.ParseString(__uops_cityElement, "city");

For optional parameters, the generated code initializes to default and conditionally parses if the property exists:

// Generated: optional MCP argument binding
int __uops_days = default;
if (__uops_internal_mcpHasArgumentObject &&
    __uops_internal_mcpArgumentObject.TryGetProperty("days"u8, out JsonElement __uops_daysElement))
    __uops_days = McpBinding.ParseInt32(__uops_daysElement, "days");

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:1963-2003

MCP Tool Schema

The generator also emits McpToolDescriptor records that describe each tool's parameter schema for MCP discovery. The descriptor uses McpName ?? Name for each parameter.

public readonly record struct McpParameterDescriptor(
    string Name,
    Type ParameterType,
    bool Required,
    string? Description = null);

public readonly record struct McpToolDescriptor(
    string Name,
    string? Description,
    McpParameterDescriptor[] Parameters);

Sources: Manifold.Mcp/McpToolCatalogModels.cs:1-12

Type Conversion

Manifold provides parallel type conversion systems: CliBinding converts from string, and McpBinding converts from JsonElement. Both systems share the same set of supported types and follow similar patterns.

Supported Types

.NET Type CLI Parser Method MCP Parser Method CLI Parse Strategy MCP Parse Strategy
string Identity (passthrough) ParseString No conversion needed JsonValueKind.String check
bool ParseBoolean ParseBoolean bool.TryParse JsonValueKind.True/False
int ParseInt32 ParseInt32 int.TryParse (InvariantCulture) TryGetInt32()
long ParseInt64 ParseInt64 long.TryParse (InvariantCulture) TryGetInt64()
double ParseDouble ParseDouble double.TryParse (Float + AllowThousands) TryGetDouble()
decimal ParseDecimal ParseDecimal decimal.TryParse (Number) TryGetDecimal()
Guid ParseGuid ParseGuid Guid.TryParse String + Guid.TryParse
Uri ParseUri ParseUri Uri.TryCreate (RelativeOrAbsolute) String + Uri.TryCreate
DateTimeOffset ParseDateTimeOffset ParseDateTimeOffset DateTimeOffset.TryParse (InvariantCulture) String + DateTimeOffset.TryParse
Nullable<T> Unwrap + recurse Unwrap + recurse Empty string returns null JsonValueKind.Null returns null
Enums Enum.TryParse (case-insensitive) Enum.TryParse (case-insensitive) From string From string or raw text
Arrays Comma-split + recurse N/A (native JSON arrays) text.Split(',') Handled by JsonSerializer
Complex types Not supported (throws) JsonSerializer.Deserialize InvalidOperationException Full JSON deserialization

Sources: Manifold.Cli/CliBinding.cs:96-329, Manifold.Mcp/McpInvoker.cs:107-269

Fast Path vs Standard Path

The source generator determines at compile time whether all parameters of an operation can use fast (inline) parsers. If so, it generates a fast-path invoker that avoids boxing and reflection.

flowchart TD
    A[Source Generator Analyzes Parameters] --> B{All types in fast-path set?}
    B -->|Yes| C[Emit IFastSyncCliInvoker]
    B -->|Yes| D[Emit IFastCliInvoker]
    B -->|No| E[Emit ICliInvoker only]
    C --> F[Inline parse calls]
    D --> F
    E --> G[ConvertValue reflection path]
Loading

The fast-path type map is defined by TryGetFastCliParserMethod:

parserMethod = normalizedTypeName switch
{
    "string" or "global::System.String" => "identity",
    "bool" or "global::System.Boolean" => "ParseBoolean",
    "int" or "global::System.Int32" => "ParseInt32",
    "long" or "global::System.Int64" => "ParseInt64",
    "double" or "global::System.Double" => "ParseDouble",
    "decimal" or "global::System.Decimal" => "ParseDecimal",
    "Guid" or "global::System.Guid" => "ParseGuid",
    "Uri" or "global::System.Uri" => "ParseUri",
    "DateTimeOffset" or "global::System.DateTimeOffset" => "ParseDateTimeOffset",
    _ => null
};

Nullable<T> wrappers are transparently unwrapped before matching. If any parameter uses a type outside this set, the operation falls back to the standard ConvertValue path for the CLI surface.

For the MCP surface, unsupported fast-path types fall back to McpBinding.ConvertValue, which ultimately delegates to JsonSerializer.Deserialize for complex types.

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2354-2473

CLI ConvertValue Fallback

The standard-path ConvertValue method handles types in this order:

  1. string — identity return
  2. Nullable<T> — unwrap, return null for empty/whitespace, recurse
  3. Arrays — split by comma, recursively convert each element
  4. Enums — case-insensitive Enum.TryParse
  5. Primitive types (bool, int, long, double, decimal, Guid, Uri, DateTimeOffset) — delegate to typed parser
  6. Unsupported types — throw InvalidOperationException

Sources: Manifold.Cli/CliBinding.cs:266-329

MCP ConvertValue Fallback

The MCP ConvertValue method follows a similar pattern but uses JSON-native parsing:

  1. string — validate JsonValueKind.String
  2. Nullable<T> — return null for JsonValueKind.Null, recurse
  3. Enums — extract string or raw text, case-insensitive parse
  4. Primitive types — delegate to typed JSON parser
  5. All other types — JsonSerializer.Deserialize(value, targetType) for full deserialization

Sources: Manifold.Mcp/McpInvoker.cs:216-269

Service Injection via [FromServices]

Parameters annotated with [FromServices] are resolved from the IServiceProvider at invocation time. The source generator emits calls to surface-specific service resolution methods.

Resolution Flow

sequenceDiagram
    participant Gen as Generated Invoker
    participant CB as CliBinding / McpBinding
    participant SP as IServiceProvider

    Gen->>CB: GetRequiredService<T>(services)
    CB->>SP: GetService(typeof(T))
    alt Service found
        SP-->>CB: service instance
        CB-->>Gen: typed service
    else Service not found
        CB-->>Gen: throw InvalidOperationException
    end
Loading

The CLI surface uses CliBinding.GetRequiredService<T> and CliBinding.GetRequiredServiceOrThrow<T>. The MCP surface uses the parallel McpBinding.GetRequiredService<T> and McpBinding.GetRequiredServiceOrThrow<T>. Both throw InvalidOperationException with a descriptive message if the service is unavailable.

For class-based operations (IOperation<TRequest, TResult>), the operation class itself is resolved from the service provider using GetRequiredServiceOrThrow<T>.

Sources: Manifold.Cli/CliBinding.cs:239-264, Manifold.Mcp/McpBinding.cs:5-31

Generated Code for Service Parameters

// CLI surface — generated binding for [FromServices] parameter
IMyService __uops_myService = CliBinding.GetRequiredService<IMyService>(services);

// MCP surface — generated binding for [FromServices] parameter
IMyService __uops_myService = McpBinding.GetRequiredService<IMyService>(services);

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2294-2297, Manifold.Generators/OperationDescriptorGenerator.cs:1973-1976

CancellationToken Injection

Parameters of type System.Threading.CancellationToken are automatically detected by the source generator without any attribute. The token is passed directly from the invoker's call site.

Detection Logic

private static bool IsCancellationToken(ITypeSymbol typeSymbol)
{
    return typeSymbol is INamedTypeSymbol namedType &&
           string.Equals(namedType.ToDisplayString(),
               "System.Threading.CancellationToken", StringComparison.Ordinal);
}

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:564-568

Generated Binding

On both CLI and MCP surfaces, the generated code is identical:

CancellationToken __uops_ct = cancellationToken;

The cancellationToken value originates from the OperationContext which is constructed with surface-appropriate factory methods (OperationContext.ForCli, OperationContext.ForMcp).

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2290-2292, Manifold/OperationContext.cs:29-54

Surface-Specific Naming

The [CliName] and [McpName] attributes allow a parameter to present different names on different surfaces. This is useful when CLI conventions (kebab-case --output-dir) differ from MCP conventions (camelCase outputDir).

Name Resolution Order

Surface Display Name Resolution
CLI parameter.CliName ?? parameter.Name
MCP parameter.McpName ?? parameter.Name

The resolved name affects:

  • CLI: The --name token used for option matching and help text
  • MCP: The JSON property name used for argument lookup and tool schema

Generated Name Usage

In the CLI invoker, the display name governs option token matching:

string displayName = parameter.CliName ?? parameter.Name;

In the MCP invoker, the display name governs JSON property lookup:

string displayName = parameter.McpName ?? parameter.Name;

In the MCP tool catalog, parameter names use the MCP-specific name for discovery metadata:

// Generated tool descriptor entry
new McpParameterDescriptor("outputDir", typeof(string), true, "Output directory")

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:1296, Manifold.Generators/OperationDescriptorGenerator.cs:1966, Manifold.Generators/OperationDescriptorGenerator.cs:2015

Request Object Binding (Class-Based Operations)

For class-based operations implementing IOperation<TRequest, TResult>, parameters are bound to properties of the request object rather than directly to method parameters. The generator maps each parameter to its RequestPropertyName (or falls back to Name) and emits an object initializer.

// Generated: request object construction
WeatherPreviewOperation.Request __uops_request = new WeatherPreviewOperation.Request
{
    City = __uops_city,
    Days = __uops_days,
};

Request properties must have a public setter; the generator validates this at compile time and reports DMCF005 if a property is not writable.

Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2024-2059

Example: Class-Based Operation

[Operation("weather.preview")]
[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;
    }
}

Sources: Manifold.Samples.Operations/SampleOperations.cs:16-40

End-to-End Binding Flow

The following sequence diagram shows the complete parameter binding flow for both surfaces, from raw input to operation invocation.

sequenceDiagram
    participant User as Input Source
    participant Gen as Generated Invoker
    participant Bind as CliBinding / McpBinding
    participant SP as IServiceProvider
    participant Op as Operation Method

    User->>Gen: CLI tokens or JSON arguments
    loop For each parameter
        alt Option / Argument
            Gen->>Bind: Parse value from input
            Bind-->>Gen: Typed value
        else Service
            Gen->>Bind: GetRequiredService<T>()
            Bind->>SP: GetService(typeof(T))
            SP-->>Bind: Service instance
            Bind-->>Gen: Typed service
        else CancellationToken
            Gen->>Gen: Assign from context
        end
    end
    Gen->>Op: Invoke with bound parameters
    Op-->>Gen: Return result
Loading

Comparison: CLI vs MCP Binding

Aspect CLI Surface MCP Surface
Input format String tokens (string[]) JSON object (JsonElement?)
Option syntax --name value or --name=value JSON property by name
Argument syntax Positional by index JSON property by name (same as options)
Case sensitivity Case-insensitive option matching Case-sensitive JSON property matching
Alias support Yes, checked during option lookup Not applicable (single JSON property name)
String conversion CliBinding.Parse* from string McpBinding.Parse* from JsonElement
Complex type support Not supported (throws) JsonSerializer.Deserialize fallback
Array handling Comma-separated string splitting Native JSON array via deserializer
Name override attribute [CliName] [McpName]
Binding class Manifold.Cli.CliBinding Manifold.Mcp.McpBinding

Related Pages

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