Parameter Binding and Type Conversion - Garume/Manifold GitHub Wiki
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.
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.
public enum ParameterSource
{
Option = 0,
Argument = 1,
Service = 2,
CancellationToken = 3
}Sources: Manifold/DescriptorModels.cs:18-24
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]
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
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 |
Manifold provides a set of attributes to declare how each parameter should be bound. All attributes are defined in the core Manifold package.
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
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
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
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
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
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).
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
Options are named parameters prefixed with --. The generated CLI invoker supports two value formats:
-
Space-separated:
--name value -
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
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
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
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
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.
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
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
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
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.
| .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
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]
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
The standard-path ConvertValue method handles types in this order:
-
string— identity return -
Nullable<T>— unwrap, returnnullfor empty/whitespace, recurse - Arrays — split by comma, recursively convert each element
- Enums — case-insensitive
Enum.TryParse - Primitive types (
bool,int,long,double,decimal,Guid,Uri,DateTimeOffset) — delegate to typed parser - Unsupported types — throw
InvalidOperationException
Sources: Manifold.Cli/CliBinding.cs:266-329
The MCP ConvertValue method follows a similar pattern but uses JSON-native parsing:
-
string— validateJsonValueKind.String -
Nullable<T>— returnnullforJsonValueKind.Null, recurse - Enums — extract string or raw text, case-insensitive parse
- Primitive types — delegate to typed JSON parser
- All other types —
JsonSerializer.Deserialize(value, targetType)for full deserialization
Sources: Manifold.Mcp/McpInvoker.cs:216-269
Parameters annotated with [FromServices] are resolved from the IServiceProvider at invocation time. The source generator emits calls to surface-specific service resolution methods.
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
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
// 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
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.
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
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
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).
| Surface | Display Name Resolution |
|---|---|
| CLI | parameter.CliName ?? parameter.Name |
| MCP | parameter.McpName ?? parameter.Name |
The resolved name affects:
-
CLI: The
--nametoken used for option matching and help text - MCP: The JSON property name used for argument lookup and tool schema
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
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
[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
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
| 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 |
-
Core Contracts —
ParameterDescriptor,ParameterSource, andOperationDescriptordefinitions -
Attributes and Operation Definition — Full attribute reference for
[Option],[Argument],[Alias],[CliName],[McpName],[FromServices] - Source Generator — How the generator analyzes parameters and emits binding code
-
CLI Runtime —
CliBindingclass and CLI dispatch pipeline -
MCP Runtime —
McpBindingclass and MCP invocation pipeline -
Dependency Injection and Service Resolution — Service registration patterns and
[FromServices]deep-dive -
Diagnostics and Compile-Time Validation —
DMCF002,DMCF003,DMCF005diagnostics for parameter binding errors - Performance and Benchmarks — Fast-path invoker design and zero-allocation goals
- Sample Operations Reference — Practical examples of parameter binding patterns