Build1 publisher3 min readPublished
MCP 2026-07-28 drops the `result` wrapper, and your unit tests will not notice
The new tools spec lets structuredContent be any JSON value. A client hard-coded to read structuredContent.result keeps compiling, keeps passing, and starts finding nothing on the wire.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- The MCP 2026-07-28 tools specification allows structuredContent to contain any JSON value: object, array, string, number, Boolean, or null, and an outputSchema may likewise describe an array or primitive at its root.
- Under MCP 2025-11-25, the convention was object-only: a tool returning string[] needed an object envelope such as {"structuredContent": {"result": ["starter", "growth", "enterprise"]}}, while the modern form is the bare array value.
- A client written around the older wire shape may always reach for structuredContent.result, but once both sides negotiate MCP 2026-07-28 an array is an array and a scalar is a scalar, with no required wrapper to unwrap.
- The author treats the change as a contract change worth testing at the transport boundary, because a unit test against the C# return type cannot reveal what tools/list advertised or what tools/call actually carried.
- The stable MCP C# SDK handles the version negotiation; its v2.0.0 release notes call out direct non-object tool results, and the current v2.2.0 tools guide documents UseStructuredContent.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
The MCP tools specification dated 2026-07-28 allows `structuredContent` to hold any JSON value: object, array, string, number, Boolean, or null, and an `outputSchema` may describe an array or a primitive at its root [1]. That retires the object-only convention of 2025-11-25, under which a tool returning `string[]` had to ship an envelope such as `{"structuredContent": {"result": ["starter", "growth", "enterprise"]}}` [2], so a client that unconditionally reaches for `structuredContent.result` will find nothing there once both ends negotiate the newer version [3].
The reason this is dangerous rather than merely annoying is that nothing in your codebase moves. The C# method still returns `string[]`. A unit test against that return type cannot tell you what `tools/list` advertised or what `tools/call` actually carried, which is why the author of a walkthrough published on dev.to treats the change as a contract to be verified at the transport boundary [4].
The stable MCP C# SDK performs the negotiation for you: the v2.0.0 release notes call out direct non-object tool results, and the v2.2.0 tools guide documents `UseStructuredContent` [5]. For a down-level client the SDK still emits the compatibility envelope, with no second handler and no hand-written version switch [6]. Convenient, and exactly why the difference is invisible from inside your own process. The shape on the wire is a property of the peer, not of your type.
The verifier in the post is small. It pins `ModelContextProtocol` 2.2.0 and targets .NET 10 [7], registers one array tool (`list_tiers`) and one scalar tool (`count_tiers`) with `UseStructuredContent = true`, which tells the SDK to generate the output schema and serialize the return value into `structuredContent` [8]. Two `System.IO.Pipelines.Pipe` instances connect an `McpClient` to an `McpServer`, so nothing opens a port and no model is involved, but discovery and calls still cross the SDK's stream transport [9]. One pair is created with `ProtocolVersion` "2026-07-28" and a fresh pair with "2025-11-25" [10]. Separate sessions are the load-bearing detail: the negotiated protocol belongs to a connection, so flipping an option after discovery would not exercise the real compatibility path [11]. Two tools across two sessions is four calls, which is the whole budget for covering both wire shapes [19].
The modern session asserts that the advertised schema type is `array`, that the returned `StructuredContent` has `ValueKind` `Array`, and that a legacy-style read of a root object with `result` fails [12]. That last assertion is a negative control modelling the parser being removed; without it, an accidental wrapper can slip back in while a values-only test stays green [13]. The legacy session asserts the opposite shape [14], and both check the three tier values plus the text content fallback that the specification recommends when structured content is returned [15].
Two consequences worth carrying into your own code. Branch from the discovered schema rather than from the tool name or a local C# type, because the server may be implemented in another language [16]. And assert discovery and invocation together, which is what catches the asymmetric bug: a server advertising an array schema but returning a wrapped object, or the reverse, can carry the right values while violating its declared contract [17].
What to watch: whether the SDKs you depend on in other languages also emit the down-level envelope, since the source only documents that behaviour for the C# SDK [6], and whether your own servers still conform to the schemas they advertise, because the spec puts that obligation on the server and asks clients to validate [18].