Repository navigation
Conversation
There was a problem hiding this comment.
Thanks for the fix. This approach makes sense: match protocol-defined fields exactly while preserving the existing DefaultOptions behavior for user-defined data.
I checked TypeScript, Python, Go, Rust, and Ruby. None binds params.Name as the canonical tool name, and canonical name wins when both spellings are present, regardless of order. TypeScript and Python runtime probes confirmed this in both protocol eras. This supports the change here.
The important distinction is exact matching, not blanket rejection of noncanonical JSON. Peers differ in required-field validation, defaults, and raw/custom-result handling.
Looks good overall. A few non-blocking suggestions:
- Add a client-side result test covering alternate-cased optional fields such as
ContentandIsError, and a nested user-DTO-through-tool test confirming that its existing binding behavior is preserved. - Track the remaining internal casing differences separately, particularly task-based
CallToolResultdeserialization and transport-levelJsonRpcErrorDetailparsing. These do not bypass the tool-name fix. - Include the behavior change in the planned minor-release notes. The shared context also changes OAuth model parsing, so that compatibility impact should be mentioned.
|
I'd rather finish the protocol/user-data split here than leave the remaining readers as follow-ups. Tasks tool results still bind We could add cached, case-sensitive @tarekgh I know you suggested tracking these separately, but fixing the remaining protocol-field casing inconsistencies together seems manageable in one PR. Does that sound good to you? |
|
@halter73 Yes, that sounds good to me. My suggestion to track these separately was to keep the original fix focused, but I agree that finishing the protocol/user-data split in this PR is cleaner. A cached, read-only Let's use it for the remaining protocol readers, including task-based tool results and stream/ASP.NET error details, while retaining That would let us document the protocol boundary consistently rather than carrying the known internal exceptions. @FenjuFu Would you be able to include these changes in this PR, or would you like help with the implementation or tests? |
Protocol DTOs were deserialized with the source-generated context, which uses the web defaults and binds property names case-insensitively, while the JSON-RPC envelope converter matches its properties exactly. A request such as tools/call with params.Name was therefore dispatched even though middleware inspecting params.name would not recognize it. The source-generated context now matches property names case-sensitively. DefaultOptions keeps case-insensitive matching because it is also used for user-defined types such as tool parameters. Fixes modelcontextprotocol#1897 Signed-off-by: FenjuFu <fufenjupku@gmail.com>
Add McpJsonUtilities.ProtocolOptions, a cached read-only copy of DefaultOptions that matches property names case-sensitively, and use it wherever the SDK deserializes protocol messages through options rather than the source-generated context: the stdio client and server transports, the ASP.NET Core Streamable HTTP and SSE request reader, and the Tasks extension (task-based CallToolResult, InputRequest, InputResponse and error details). DefaultOptions and caller-supplied options are still used for user-defined data. Its docs now point to ProtocolOptions and state that protocol types deserialized through DefaultOptions keep case-insensitive matching. Signed-off-by: FenjuFu <fufenjupku@gmail.com>
e09ea77 to
39ec6e5
Compare
|
@halter73 @tarekgh Sure, done in 39ec6e5 (also rebased onto main):
Two things I left for you to decide: net10.0 locally: core 2414 passed / 6 skipped, AspNetCore 636 passed / 31 skipped, 0 failed. |
|
Thanks, this addresses the earlier Tasks and transport-reader concerns. I also checked the actual immediate and polled Tasks client paths with mixed-case fields in both property orders, and canonical fields are now handled consistently. One issue needs fixing before merging: Could you add On the two remaining decisions:
|
AI assistance: Codex prepared this test-only change and validation summary. Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
|
@tarekgh Addressed in de85bc8.
Validation on Windows with .NET SDK 10.0.101:
The Apps preset and OAuth release-note decisions remain for the maintainers; this commit is limited to the missing test context. Note This comment and the test-only change were generated with AI assistance. The results above come from actual local test runs. |
|
@FenjuFu Thanks for fixing the test serialization metadata. For Apps, I recommend deriving There is a compatibility impact: consumers deserializing alternate-cased JSON through the Apps preset would no longer have those fields bound. For example, Please include regression coverage for canonical and alternate-cased Apps fields, including both property orders when both spellings are present, and confirm that the preset and |
|
I agree. Apps fields are protocol-defined, so |
Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
|
@tarekgh @halter73 Addressed in ab0f8d8. Added nine regression cases covering On Windows, all 39 |
There was a problem hiding this comment.
🔵 Needs a closer look
The compatibility changes need maintainer validation, and nested converters still introduce an unintended regression through case-insensitive options.
1 open issue
What changed in this PR
Aligns protocol property matching with the case-sensitive JSON-RPC envelope, addressing #1897 while aiming to preserve user-data binding.
Changes:
- Adds
ProtocolOptionsand makes generated protocol contracts case-sensitive. - Updates transport, Tasks, and Apps serialization paths.
- Adds casing regression tests and documents Apps compatibility changes.
| File | Description |
|---|---|
| tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs | Tests strict error-field matching. |
| tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs | Tests case-insensitive nested user arguments. |
| tests/ModelContextProtocol.Tests/Server/McpServerTests.cs | Tests request-field casing and argument preservation. |
| tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs | Tests Apps casing and caller options. |
| tests/ModelContextProtocol.Tests/McpJsonUtilitiesTests.cs | Tests serializer presets and protocol fields. |
| src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksServerExtensions.cs | Uses protocol options for task requests. |
| src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksBuilderExtensions.cs | Updates task results and input-response handling. |
| src/ModelContextProtocol.Extensions.Tasks/Protocol/TaskStatusNotificationParams.cs | Updates nested input-request serialization. |
| src/ModelContextProtocol.Extensions.Tasks/Protocol/GetTaskResult.cs | Updates nested input-request serialization. |
| src/ModelContextProtocol.Extensions.Tasks/Client/McpTasksClientExtensions.cs | Uses protocol options for task exchanges. |
| src/ModelContextProtocol.Extensions.Apps/Server/McpApps.cs | Makes the Apps preset case-sensitive. |
| src/ModelContextProtocol.Core/Server/StreamServerTransport.cs | Uses strict protocol deserialization. |
| src/ModelContextProtocol.Core/McpJsonUtilities.cs | Adds protocol options and strict generated contracts. |
| src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs | Uses strict protocol deserialization. |
| src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs | Selects protocol options for HTTP handling. |
| docs/concepts/apps/apps.md | Documents Apps casing compatibility. |
Review effort: Balanced
|
One small, non-blocking follow-up to the earlier client-side test suggestion: the new |
Add client API regression coverage for canonical tool-result fields in ordinary, immediate-task and polled-task responses. AI assistance was used to prepare this change and its tests. Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
|
Addressed in 0d92883. Nested annotations and icons now resolve their contracts from the converter's supplied options. The 12 new cases cover DefaultOptions, ProtocolOptions and explicit case-insensitive caller options; four reproduce the previous regression. The client follow-up adds six cases through the actual CallToolAsync, immediate CallToolAsTaskAsync and working/completed CallToolWithPollingAsync paths. Canonical content/isError win in both property orders. Windows validation (SDK 10.0.101): the related net9.0 selection passes 191 cases; the full net10.0 ModelContextProtocol.Tests project passes 2,453 with 4 skips. The first full net9.0 run had one missing Node test-server launch (2,452 pass / 1 fail / 4 skip); after installing pinned npm dependencies, that method's two cases pass. The selected previous ASP.NET Core CI failure families pass locally (5 pass / 1 skip) with loopback proxy bypass. Other targets/OS and the full ASP.NET Core suite were not rerun. The three new-head workflows require maintainer approval; these local results do not establish why the earlier Windows CI failed. Note This reply and follow-up code/tests were generated with AI assistance. Test counts are from actual local runs. |
| while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) | ||
| { | ||
| icons.Add(JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.Icon) ?? | ||
| icons.Add(JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<Icon>()) ?? |
There was a problem hiding this comment.
@FenjuFu Could we please address it before merging? This is an application compatibility regression: existing applications using standalone source-generated contexts can now fail to deserialize correctly cased content containing annotations or icons. I reproduced these failures on the current head, while the same cases pass on the previous commit. Preserving caller settings while providing an SDK-generated metadata fallback, with standalone-context regression coverage, would avoid breaking those applications.
Resolve nested content metadata through caller-first resolvers with an SDK-generated fallback, preserving caller settings and mutable direct-converter reads. Add standalone-context, nested collection, caller-contract precedence, and mutable-options regression coverage. AI assistance was used to prepare this change and its tests. Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
|
Nested reads now keep caller resolvers first and append the SDK's source-generated metadata as a fallback, including Added 18 regression cases covering standalone contexts registering only Final-source validation on Windows/SDK 10.0.101: 160 focused cases pass on each of net9.0 and net10.0; the net9.0 host has System.Text.Json reflection disabled. The complete final-source suite, full ASP.NET Core suite, other frameworks/platforms, and Native AOT publish/link were not completed for this follow-up. Upstream CI remains separate from these local results. This follow-up and its tests were prepared with AI assistance; the reported checks were executed locally. |

Fixes #1897
The JSON-RPC envelope converter matches
jsonrpc/id/method/params/result/errorexactly, but the typed contents ofparamsandresultwere deserialized with the internal source-generatedJsonContext, which is declared withJsonSerializerDefaults.Weband therefore binds property names case-insensitively. Atools/callrequest withparams.Namewas dispatched as if it wereparams.name, even though middleware or proxies inspectingparams.namewould not recognize it. Some transports and the Tasks extension also deserialized protocol types through the case-insensitiveDefaultOptions, so fields such aserror.Codeor a task-basedCallToolResult.IsErrorwere bound regardless of casing.Changes
McpJsonUtilities.JsonContextnow setsPropertyNameCaseInsensitive = false. Protocol DTOs (requests, results and notifications, on both client and server) only bind the property names defined by the specification, consistent with the envelope.McpJsonUtilities.ProtocolOptions: a cached, read-only copy ofDefaultOptionswith case-sensitive property matching. It is built from the same source-generated context, so trimming and Native AOT behave as before. Consumers deserializing protocol messages themselves can use it.ProtocolOptions: the stdio client and server transports (StreamClientSessionTransport,StreamServerTransport), the ASP.NET Core Streamable HTTP and SSE request reader (StreamableHttpHandler), and every protocol type inModelContextProtocol.Extensions.Tasks(task-basedCallToolResult,InputRequest,IDictionary<string, InputResponse>,JsonRpcErrorDetail). The HTTP client transports already used the case-sensitive context.McpJsonUtilities.DefaultOptionsexplicitly keepsPropertyNameCaseInsensitive = true. It is also used for user-defined types (tool, prompt and resource parameters,McpClientToolarguments,SendRequestAsync<TParameters, TResult>), so binding of user data is unchanged. Its XML docs now point toProtocolOptionsand state that protocol types deserialized throughDefaultOptionskeep case-insensitive matching.As discussed in the issue, this is exact protocol-field matching, not new validation of non-canonical JSON:
tools/callparams{"Name":"echo"}echonamemissing, JSON-RPC error, handler not invoked{"name":"echo","Name":"other"}other)echo{"Name":"other","name":"echo"}echoecho{"name":"echo","Arguments":{...}}null){"name":"echo","arguments":{"text":"a","Text":"B"}}Fields with defaults keep their defaults when only an alternately cased key is present; for example a
CallToolResultwith onlyContentstill ends up with an emptycontentlist rather than failing.Compatibility and release notes
McpApps.SerializerOptionsnow derives fromProtocolOptions, retaining the Apps source-generated context. Apps extension fields such asresourceUriandmimeTypesare case-sensitive, consistent withGetUiCapability. Alternate-cased fields are ignored, and canonical fields win in either property order. Correctly cased JSON, serialization output, strongly typed C# usage,DefaultOptions, and caller-supplied options remain unchanged. The Apps guide and XML documentation describe this intentional compatibility change.Tests
Nested content reads now resolve annotation and icon metadata from the supplied serializer options, preserving opt-in case-insensitive matching. Actual client API tests also cover canonical tool-result fields through ordinary, immediate-task and polled-task paths.
McpServerTests.Call_Tool_Request_Params_Are_Matched_Case_Sensitivelysends rawtools/callrequests through the server:Nameonly (rejected, handler not invoked), alternately casedArguments(not bound), canonical, both key orders withnameandName, and alternately cased keys insidearguments(preserved as distinct entries).McpJsonUtilitiesTests:ProtocolOptionsis a read-only singleton with case-sensitive matching; for a JSON-RPC error with bothcode/messageandCode/Message, and for aCallToolResultwithContent/IsError,ProtocolOptionsbinds only the canonical names whileDefaultOptionskeeps the previous behavior.DefaultOptions_MatchesPropertyNamesCaseInsensitivelystill covers user types.StdioServerTransportTests.ReadMessagesAsync_Should_Match_Error_Properties_Case_Sensitivelyreads such an error line through the stdio transport.McpServerToolTests.BindsNestedArgumentPropertiesCaseInsensitivelyinvokes a tool whose parameter is a nested user DTO, bound fromLOCATION/CITY, to confirm user-data binding is unchanged.The server and stdio transport tests failed without the earlier core change. The earlier core revision was tested on net10.0 (Windows):
ModelContextProtocol.Tests2414 passed / 6 skipped andModelContextProtocol.AspNetCore.Tests636 passed / 31 skipped. Those counts are historical, not a full-suite run of the latest head.For the earlier Apps follow-up (historical),
dotnet test tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj -f net10.0 --filter FullyQualifiedName~McpAppsTests -p:JsonSerializerIsReflectionEnabledByDefault=falsepassed all 39 tests on Windows using SDK 10.0.401 / runtime 10.0.12. The nine new cases cover canonical and alternate spellings, both property orders, agreement withGetUiCapability, and caller-controlled options. The net9.0 target compiled, but its test host could not start because the local .NET 9 runtime is absent. Other target frameworks, platforms and the full suite were not rerun locally for this follow-up. The latest upstream Build and Test, CodeQL and Markdown-link workflows await maintainer approval.As noted in the issue, this is a subtle behavior change for peers that relied on alternate casing, so it probably belongs in a minor release.
cc @tarekgh for review
Latest review follow-up (2026-10-22)
Commit
0d92883b1a3778e6fe922e73556c0724ecbdffb0resolves nested annotation and icon contracts from the converter's supplied options. Twelve regression cases cover canonical/alternate nested fields through DefaultOptions, ProtocolOptions, and explicit case-insensitive caller options. Six client API cases cover ordinary tool results, immediate Tasks results, and a working-then-completed Tasks poll, with canonical content/isError winning over alternate spellings in either property order.Validation on Windows with SDK 10.0.101:
Note
This follow-up summary and the code/tests were prepared with AI assistance. The reported checks were executed locally.
Standalone-context compatibility follow-up (2026-10-23)
Commit
f7017ba6d4d6a4e480ba746bea7b7c0cf461b8f5restores nested content reads for standalone source-generated contexts that register onlyContentBlockorCallToolResult. Caller resolvers take precedence over an SDK-generated fallback, including nested audience and icon-size metadata. Caller settings/converters are copied, the caller's options are not modified, and only read-only options are cached; direct calls with mutable options observe later changes.Eighteen regression cases cover standalone contexts, canonical/alternate nested casing, caller contracts, and mutable options. The initial 17-case selection on the previous head produced 16 missing-metadata failures and one passing caller-contract control. Final-source Windows/SDK 10.0.101 validation passes 160 focused cases on each of net9.0 and net10.0, with System.Text.Json reflection disabled for net9.0.
The complete final-source project suite, full ASP.NET Core suite, other frameworks/platforms, and Native AOT publish/link were not completed for this follow-up. Earlier full-suite counts above are historical. Local results do not establish upstream CI success. This follow-up and its tests were prepared with AI assistance; the reported checks were executed locally.