Skip to content

Adding an overload of McpClientExtensions::CallToolAsync taking JsonElement tool argument #641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

anuchandy
Copy link

@anuchandy anuchandy commented Jul 21, 2025

Adding McpClientExtensions::CallToolAsync overload that takes tool parameters as Dictionary<string, JsonElement> JsonElement.

Motivation and Context

Please see #640

How Has This Been Tested?

yes, and added the unit tests.

Breaking Changes

this is a non-breaking change.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

public static ValueTask<CallToolResult> CallToolAsync(
this IMcpClient client,
string toolName,
IReadOnlyDictionary<string, JsonElement> arguments,
Copy link
Member

@eiriktsarpalis eiriktsarpalis Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many ways in which you could represent tool arguments in STJ: IDictionary<string, JsonElement>, IDictionary<string, object?>, JsonElement, JsonObject, IReadOnlyDictionary<string, JsonNode?>, so singling out an overload for this particular representation feels oddly specific. Rather than exposing overloads for each and every one of them, I would be inclined to just ask users to populate the one representation picked by the library (or author extension methods that perform the conversion under wraps).

Copy link
Contributor

@stephentoub stephentoub Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also consider just having an overload that takes a JsonElement for the arguments. Then someone that has json wouldn't need to first parse it into a dictionary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @eiriktsarpalis and @stephentoub for taking a look.

We could also consider just having an overload that takes a JsonElement for the arguments. Then someone that has json wouldn't need to first parse it into a dictionary.

This is great feedback @stephentoub. Yes, this helps optimize the call sites even better, often user may obtain immutable JsonElement with little overhead. I’ve updated the pr. Could you please review this and let me know if there are any areas for improvement?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would still need to be converted under the hood by the overload, since AIFunction expects dictionaries. Which is fine I suppose if we only care about this being an accelerator.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would still need to be converted under the hood by the overload, since AIFunction expects dictionaries. Which is fine I suppose if we only care about this being an accelerator.

That is correct. I was not sure, for this one use case, if it is appropriate to define a variant of CallToolRequestParams that uses an Arguments property of type JsonElement, as well as to add a corresponding context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would still need to be converted under the hood by the overload, since AIFunction expects dictionaries.

This doesn't get passed to AIFunction; it's written into a json rpc request.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying request model is using Dictionary, which necessitates a conversion from JsonElement. In the future we might want to consider using a different type so that we avoid doing that.

@anuchandy anuchandy changed the title Adding an overload of McpClientExtensions::CallToolAsync taking Dictionary<string, JsonElement> tool argument Adding an overload of McpClientExtensions::CallToolAsync taking JsonElement tool argument Jul 23, 2025
new()
{
Name = toolName,
Arguments = arguments.EnumerateObject().ToDictionary(prop => prop.Name, prop => prop.Value) ?? []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw an exception if the JsonElement isn't representing an object. Perhaps it makes sense to add a check at the start of the method so that a bespoke error message can be raised?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, Just updated the pr with the check as suggested.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw an exception if the JsonElement isn't representing an object. Perhaps it makes sense to add a check at the start of the method so that a bespoke error message can be raised?

What if it's a no-parameter tool? Would JsonValueKind.Undefined be an acceptable value then, or must it be a property-less JsonElement object? I would think default(JsonElement) would an intuitive choice for someone who is passing no arguments, and that's undefined afair?

Copy link
Member

@eiriktsarpalis eiriktsarpalis Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good point. Maybe we should make the parameter nullable for that reason, needing to create a null JsonElement or empty object seems somewhat overbearing.


[Theory]
[MemberData(nameof(GetClients))]
public async Task CallTool_Stdio_EchoServer_WithJsonElementArguments_ThrowsForNonObject(string clientId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness, you could try turning this into a theory that accepts all types of invalid JSON.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants