Skip to content

AIFunctionFactory rejects missing nullable parameters marked as required in generated schema #1426

@gijswalraven

Description

@gijswalraven

Summary

When a C# MCP tool method has nullable parameters (e.g. string? filter), the SDK generates a JSON schema that marks them as both "required" and nullable via a type union ["string", "null"]. However, AIFunctionFactory.ReflectionAIFunctionDescriptor.GetParameterMarshaller independently enforces that all "required" parameters must be present in the arguments dictionary — throwing ArgumentException when the client omits them.

This means clients cannot omit nullable parameters, even though the schema correctly signals they accept null.

Steps to reproduce

  1. Define an MCP tool with a nullable parameter without a default value:
[McpServerTool(Name = "my_tool")]
public static string MyTool(
    [Description("Required param")] string name,
    [Description("Optional filter")] string? filter,
    CancellationToken ct)
{
    return $"name={name}, filter={filter ?? "(none)"}";
}

Note: Adding = null as a default value is the intuitive fix, but C# requires optional parameters to appear after all required parameters. When DI-injected services (McpServer, CancellationToken, etc.) follow the nullable param, = null causes compiler error CS1737: Optional parameters must appear after all required parameters.

  1. The SDK generates this inputSchema:
{
  "type": "object",
  "required": ["name", "filter"],
  "properties": {
    "name": { "type": "string" },
    "filter": { "type": ["string", "null"] }
  }
}
  1. A client calls the tool omitting filter:
{
  "method": "tools/call",
  "params": {
    "name": "my_tool",
    "arguments": { "name": "test" }
  }
}
  1. AIFunctionFactory throws:
System.ArgumentException: The arguments dictionary is missing a value for the required parameter 'filter'.
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunctionDescriptor.<GetParameterMarshaller>...

Expected behavior

One of the following:

  • Option A (preferred): Don't mark nullable parameters as "required" in the generated schema. A nullable param without a default value should be treated as optional in the JSON schema.
  • Option B: In ReflectionAIFunctionDescriptor.GetParameterMarshaller, treat parameters whose schema type includes "null" as optional — supply null when they're missing from the arguments dictionary, instead of throwing.

Current workaround

We added a CallToolFilter that inspects the cached tool schema before invocation, finds nullable properties missing from the arguments, and injects explicit JsonValueKind.Null values:

mcpOptions.Filters.Request.CallToolFilters.Add(next => async (context, ct) =>
{
    if (context.Params is { } p)
    {
        // For each property in the schema that allows null but is missing
        // from arguments, inject an explicit null value
        InjectMissingNullableArguments(schemaCache, p);
    }
    return await next(context, ct);
});

This works but shouldn't be necessary — the SDK should handle its own schema semantics consistently.

Environment

  • SDK version: ModelContextProtocol 1.1.0 / ModelContextProtocol.AspNetCore 1.1.0
  • Runtime: .NET 9.0
  • Transport: Streamable HTTP
  • Client: Claude Code (CLI)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions