Skip to content

Dotnet implementation silently drop parameters not used by tool during tool execution #1508

Description

@softnado

Describe the bug
Assume there is one tool called Respond_To_Task (Guid taskId, String update, Boolean markComplete = false), in current implementation, if AI decided to call Respond_To_Task(taskId = guidA, update = "Done", phase = "completed"), the call will succeed but the markComplete passed to tool backend is false. Both AI and Backend will not able to detect such gap until someone outside of the system checking the behavior issue.

To Reproduce
Steps to reproduce the behavior:

  1. Define some mcp client with such tool definition, and test it with the instructed call pattern, it will succeed.

Expected behavior
Error out or warning out when there is parameters mismatch instead of silently drop or ignore.

Logs
If applicable, add logs to help explain your problem.

Additional context
Add any other context about the problem here.

Activity

  1. stephentoub commented on Apr 8, 2026

    @stephentoub
    Contributor

    I'm unclear as to what the concern is. You specified a default value for markComplete, stating that it's optional for the AI to provide a value for.

    If you want markComplete to be required, don't specify a default value.

  2. softnado commented on Apr 8, 2026

    @softnado
    Author

    The concern here is that there is no signal indicate this happened and both end believe the intended operation been executed correctly while what actually been executed is not what should happen. Understand this can be tool implementation practice change but it is not obvious such behavior is what been implemented today.

  3. stephentoub commented on Apr 8, 2026

    @stephentoub
    Contributor

    I don't understand. Your "expected behavior" was to error out or warning; if that's the behavior you want, just don't add the default value. The property in the tool schema is either required or optional, and if you specify optional (which is what you get with a default value), you're explicitly telling the model it's fine to not provide... it is not erroneous or warning-worthy.

    It's no different in this regard from calling a method in C# with an optional parameter that has a default value: if the caller doesn't specify one, the callee gets the default value. It's not an error or a warning.

  4. softnado commented on Apr 8, 2026

    @softnado
    Author

    Agree adjust the tool implementation pattern can get the expected behavior as suggested. The risk is that when put the parameter as is, tool developer not aware of ["default implementation" will silently drop the not used parameter without warning or failure], to get reminded that "Shouldn't use parameter with default" because model may pass something different intended for that parameter but with wrong name, then execution succeed but result is wrong. Maybe the attribute can allow customization of the parameters mapping. Or the document of MCP dotnet sdk can call out this specifically.

  5. jeffhandley commented on Apr 9, 2026

    @jeffhandley
    Contributor

    @mikekistler, @halter73, and I were chatting with @softnado offline about this and thought it would be valuable to assess the behavior across the SDKs.

    Unmatched Tool Arguments: Cross-SDK Behavior Report (MCP SDKs)

    Key Takeaways

    1. Only 2 of 7 implementations reject unmatched arguments: The Go SDK and FastMCP. Both do so by generating schemas with additionalProperties: false and validating at runtime.

    2. The Rust SDK is a notable near-miss: It generates schemas with additionalProperties: false but does not validate against them at runtime — serde silently drops unknown fields.

    3. The Java SDK is unique in passing through: Unlike other SDKs that silently drop extra arguments, the Java SDK preserves them in the arguments map passed to the handler. The handler could theoretically detect and act on them, but the SDK provides no signal.

    4. The Go SDK's opt-out is per-tool only: There is no global configuration to disable strict validation. Users must either supply a custom InputSchema per tool or use the raw Server.AddTool API.

    5. 5 of 7 implementations silently ignore/pass through: C#, TypeScript, Python, Java, and Rust all allow unrecognized arguments without any signal — each using their language's default deserialization behavior.

    Relevant Specification Work

    • modelcontextprotocol/modelcontextprotocol#1898 — Open issue about additionalProperties: false on protocol types. Comment from @sirkirby (2026-04-08) explicitly raises the tool argument case and notes it has security implications. Comment from @pja-ant argues servers should not break on unknown fields for inter-version compatibility. This is the most active spec-level discussion about this topic.
    • SEP-1303 (accepted) — Established that input validation errors should be tool execution errors (isError: true), which is relevant to how rejections should be communicated even if the spec doesn't yet mandate that they happen.
  6. self-assigned this
    on Apr 9, 2026
  7. mikekistler commented on Apr 13, 2026

    @mikekistler
    Contributor

    The issue here is specifically that undefined tool arguments are silently ignored.

    If a client sends a tools/call request with argument names that are not part of the tool’s declared input schema, these arguments are silently dropped and the invocation may "succeed" instead of surfacing that mismatch. That can make it hard for both the tool author and the caller to detect that the request did not actually match the tool contract.

    Exploring this with Copilot revealed that the actual binding of tool call arguments to method parameters, the natural place to detect undefined tool arguments, happens inside MEAI and not in the MCP C# SDK.

    It is possible to implement a fix completely in the MCP C# SDK but that will duplicate functionality currently in MEAI.

    I think my preference would be to make this an option in MEAI that the MCP C# SDK threads through.

    I'd appreciate other folks thoughts on this.

    cc: @jeffhandley @stephentoub @halter73

  8. stephentoub commented on Apr 13, 2026

    @stephentoub
    Contributor

    MEAI specifically allows other things in the arguments dictionary because:

    1. you might be passing in state to pass down through a chain or a pipeline with different invocations picking out different things
    2. you might be accepting an AIFunctionArguments into your method such that your method can pull out whatever it wants
    3. there's nothing fundamentally wrong with an LLM choosing to supply additional information

    If we want to add an option that makes the AIFunctions created by AIFunctionFactory.Create stricter, we can. It would need to be opt-in, though. And even at the MCP level, it would really need to be opt-in.

    Alternatively, if the goal is just to be able to get some insight into when this happens, MCP tools created by McpServerTool.Create (or the attribute) could log to an ILogger when passed more information than they'll use).

    I'm still not convinced there's a problem to be solved here, though.

  9. mikekistler commented on Apr 13, 2026

    @mikekistler
    Contributor

    I completely agree that this would be strictly opt-in at any level where it is added.

    For MCP, there is already "_meta" for sending "extra" parameters/context, so thinking only about MCP tools it seems like there isn't really a need to send undefined parameters. So it would be nice to be able to warn or fail when they are present.

    We could do this completely in the MCP C# SDK -- it just seems a little gross since IMHO it really should be done within the parameter binding logic.

  10. eiriktsarpalis commented on Apr 15, 2026

    @eiriktsarpalis
    Member

    I should add that the current behavior on tool parameters reflects the defaults of STJ in when it comes to property binding. In the context of MCP tool invocation, these do materialize when tool parameter types are themselves objects. So it's generally a good idea to keep the two mutually consistent.

  11. stephentoub commented on Apr 15, 2026

    @stephentoub
    Contributor

    So it would be nice to be able to warn or fail when they are present.

    If an LLM satisfies all the requirements of the method but happens to also include additional values, I don't see why it's worth incurring the additional expense and token cost of roundtrips back to the LLM. Log? Sure. Fail? Why.

    Also note that if this is something folks actually care about, they should use LLM features that guarantee it'll never happen, like OpenAI's strict function calling which uses constrained decoding to guarantee it'll never send a payload that doesn't match the schema.

    it just seems a little gross since IMHO it really should be done within the parameter binding logic.

    The MCP implementation has parameter binding logic.

    opt-in

    If you're really keen on adding new surface area here, I'll drop it, but this really feels to me like we're spending time on a non-problem.

    It's also something someone can add on their own if they really want this behavior, in multiple ways. For example, writing a delegating McpServerTool that does the validation e.g.:

    class ValidatingMcpServerTool(McpServerTool tool) : DelegatingMcpServerTool(tool)
    {
        private readonly HashSet<string>? _validPropertyNames =
            tool.ProtocolTool.InputSchema.TryGetProperty("properties", out var properties) && properties.ValueKind == JsonValueKind.Object
                ? [.. properties.EnumerateObject().Select(p => p.Name)]
                : null;
    
        public override ValueTask<CallToolResult> InvokeAsync(RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken = default)
        {
            if (request.Params?.Arguments is { } arguments)
                foreach (var arg in arguments)
                    if (_validPropertyNames?.Contains(arg.Key) is false)
                        throw new ArgumentException($"Unexpected argument: {arg.Key}");
    
            return base.InvokeAsync(request, cancellationToken);
        }
    }

    or more broadly as a filter they can drop into their server and have it apply to all tool calls, e.g.

    requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
    {
        if (context.MatchedPrimitive is McpServerTool tool &&
            context.Params.Arguments is { } arguments &&
            tool.ProtocolTool.InputSchema.TryGetProperty("properties", out var properties) && properties.ValueKind == JsonValueKind.Object)
        {
            HashSet<string> validProperties= [.. properties.EnumerateObject().Select(p => p.Name)];
            foreach (var arg in arguments)
            if (validProperties.Contains(arg.Key) is false)
                throw new ArgumentException($"Unexpected argument: {arg.Key}");
        }
                      
        return await next(context, cancellationToken);
    }
  12. eiriktsarpalis commented on Apr 15, 2026

    @eiriktsarpalis
    Member

    I should add that the current behavior on tool parameters reflects the defaults of STJ in when it comes to property binding. In the context of MCP tool invocation, these do materialize when tool parameter types are themselves objects. So it's generally a good idea to keep the two mutually consistent.

    I forgot to add that this behavior in properties is configurable via the JsonSerializerOptions.UnmappedMemberHandling, so it is conceivable that we could make AIFunction parameters also honor that global setting.

  13. mikekistler commented on Apr 16, 2026

    @mikekistler
    Contributor

    I investigated using JsonSerializerOptions.UnmappedMemberHandling as a way to "opt-in" to strict checking of tool arguments. Unfortunately, this does not seem to work. Copilot's explanation for why it doesn't work is:

    because UnmappedMemberHandling only applies when System.Text.Json is deserializing a JSON object into a .NET object contract. That is not what’s happening for the top-level tool arguments.

    In this path, MCP first copies the incoming arguments object into an AIFunctionArguments dictionary. Then MEAI binds each declared parameter one-by-one by name: it looks for taskId, update, and markComplete. If markComplete is missing and has a default, it uses the default. The extra phase key is never bound, but it’s also never treated as an error,
    because nothing deserializes the whole arguments payload against a single schema/object where “unknown member” detection would fire.

    So the option can help for cases like a parameter typed as MyOptions where the JSON for that object parameter contains extra properties, but it does not catch stray top-level tool argument names in the current binding model.

  14. eiriktsarpalis commented on Apr 17, 2026

    @eiriktsarpalis
    Member

    I investigated using JsonSerializerOptions.UnmappedMemberHandling as a way to "opt-in" to strict checking of tool arguments. Unfortunately, this does not seem to work. Copilot's explanation for why it doesn't work is:

    Right, I'm suggesting we update AIFunctionFactory in MEAI so that this setting is honored in parameter binding. It's technically a breaking change but perhaps it's worth it.

  15. dominicdill commented on Apr 23, 2026

    @dominicdill

    I'm unclear as to what the concern is. You specified a default value for markComplete, stating that it's optional for the AI to provide a value for.

    If you want markComplete to be required, don't specify a default value.

    Say there's search tool with a bunch of optional parameters for filtering down the returned information and one of the optional parameters is a boolean named "include_unverified" which defaults to false. We might want this tool to default to false because in the majority of cases we don't want unverified information returned.

    If a human talks to an agent and they ask for unverified information and the agent calls our tool, but with a parameter named "unverified" set to true, then the tool will return only verified information, and no warning will be given to the agent or the human that the intended results weren't given. It is now up to the agent and/or the human to make sure that information marked unverified was actually included. However, both expect information marked as unverified to be included because that is what they asked the tool to do.

    Also note that if this is something folks actually care about, they should use LLM features that guarantee it'll never happen, like OpenAI's strict function calling which uses constrained decoding to guarantee it'll never send a payload that doesn't match the schema.

    As far as I understand, the only way to use strict function calling is if you disallow optional properties.

    Image

    openai_function_calling

  16. mikekistler commented on Apr 24, 2026

    @mikekistler
    Contributor

    I'm going to reclassify this as a feature request instead of a bug. There's no clear violation of the spec, and even if we did decide that this was desirable (which is still in debate), we wouldn't change the default behavior.

  17. jnm2 commented on May 12, 2026

    @jnm2

    If an LLM satisfies all the requirements of the method but happens to also include additional values, I don't see why it's worth incurring the additional expense and token cost of roundtrips back to the LLM. Log? Sure. Fail? Why.

    Logging happens too late to catch an LLM tool-call mistake if the tool acts externally/destructively. Failing is the only thing that allows the LLM to understand that it is about to do something unreversible in a way that it did not intend.

  18. KirillOsenkov commented on May 12, 2026

    @KirillOsenkov
    Contributor

    Just to add my two cents from my experience and observations, LLMs don't seem to read the tool signature or description, they just hallucinate something and call in the hopes it makes sense.

    I had to add argument validation in all my MCPs myself and be very strict, and return detailed error messages about non-existing arguments, and list all actual parameters.

    The model then quickly corrects itself and re-calls the tool with the right arguments. This to me is the best workaround (fail fast) to the problem that it confabulates invalid tool calls in the first place. Very strange to me that invalid tool calls even make it to the MCP in the first place.

    I have NO IDEA how anything would work (at least in the 3 MCPs that I maintain) if I hadn't had arg validation. The tool call would just silently not do what it was expected to do and the model would spin in circles trying things and wondering why nothing works and going off on random side quests and tangents chasing red-herrings.

    My 2 cents!

  19. eiriktsarpalis commented on May 12, 2026

    @eiriktsarpalis
    Member

    I think this can be closed given dotnet/extensions#7474 has been merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P3Nice to haves, rare edge cases

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions