cd /news/developer-tools/mcp-c-sdk-array-tool-outputs-stop-lo… · home topics developer-tools article
[ARTICLE · art-105326] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

MCP C# SDK Array Tool Outputs: Stop Looking for a `result` Wrapper

A developer demonstrated that the MCP C# SDK's array tool outputs no longer require a `result` wrapper after the protocol upgrade to 2026-07-28. The SDK handles negotiation between protocol versions, emitting a compatibility envelope for older clients. The developer provided a verifier that tests both protocol versions offline to make the contract change explicit.

read4 min views5 publishedAug 21, 2026

MCP C# SDK array tool outputs are easy to misread after a protocol upgrade. A client written around the older wire shape may always reach for structuredContent.result

. Once both sides negotiate MCP 2026-07-28

, an array is an array and a scalar is a scalar. There is no required wrapper to unwrap.

I treat that as a contract change worth testing at the transport boundary. A unit test against the C# return type cannot tell me what tools/list

advertised or what tools/call

actually carried. The small verifier below runs both protocol versions offline and makes the difference explicit.

The MCP 2026-07-28 tools specification allows structuredContent

to contain any JSON value: object, array, string, number, Boolean, or null. An outputSchema

may likewise describe an array or primitive at its root. The schema still matters: servers must return content that conforms to it, and clients should validate the result.

This is different from the older object-only convention. Under 2025-11-25

, a tool returning string[]

needs an object envelope such as this:

{
  "structuredContent": {
    "result": ["starter", "growth", "enterprise"]
  }
}

The modern form is the value itself:

{
  "structuredContent": ["starter", "growth", "enterprise"]
}

The stable MCP C# SDK handles this negotiation. Its v2.0.0 release notes call out direct non-object tool results, while the current v2.2.0 tools guide documents UseStructuredContent

. For a down-level client, the SDK still emits the compatibility envelope. I do not need a second handler or a hand-written version switch.

The sample pins the current stable package and targets .NET 10:

<ItemGroup>
  <PackageReference Include="ModelContextProtocol" Version="2.2.0" />
</ItemGroup>

It registers one array tool and one scalar tool. UseStructuredContent = true

tells the SDK to generate the output schema and serialize the return value into structuredContent

.

toolCollection.Add(McpServerTool.Create(
    (Func<string[]>)ListTiers,
    new McpServerToolCreateOptions
    {
        Name = "list_tiers",
        UseStructuredContent = true,
        ReadOnly = true,
    }));

toolCollection.Add(McpServerTool.Create(
    (Func<int>)CountTiers,
    new McpServerToolCreateOptions
    {
        Name = "count_tiers",
        UseStructuredContent = true,
        ReadOnly = true,
    }));

Two System.IO.Pipelines.Pipe

instances connect an McpClient

and McpServer

. Nothing opens a port and no model is involved, but discovery and calls still cross the SDK's stream transport. I create one pair with McpClientOptions.ProtocolVersion = "2026-07-28"

and a fresh pair with "2025-11-25"

.

Using separate sessions matters. The negotiated protocol belongs to a connection, so changing an option after discovery would not prove the real compatibility path.

For the modern session, I assert both the advertised schema and the returned value:

JsonElement arraySchema = arrayTool.ProtocolTool.OutputSchema!.Value;
CallToolResult arrayResult = await arrayTool.CallAsync();

Check(arraySchema.GetProperty("type").GetString() == "array");
Check(arrayResult.StructuredContent?.ValueKind == JsonValueKind.Array);
Check(!TryReadLegacyResult(arrayResult.StructuredContent!.Value, out _));

That last check is a deliberate negative control. It models the parser I want to remove: one that only succeeds when the root is an object containing result

. If the contract test only checks the values, an accidental wrapper can slip back in unnoticed.

The legacy session checks the opposite shape:

Check(IsLegacyEnvelope(arraySchema, "array"));
Check(
    arrayResult.StructuredContent?
        .GetProperty("result")
        .ValueKind == JsonValueKind.Array);

Both sessions then verify the three tier values and the text content fallback. That fallback is useful for older or text-oriented consumers and is recommended by the specification when structured content is returned.

On the client side, I branch from the discovered schema before reading the result. I do not infer the shape from the tool name or from a C# type in my own codebase; a client may be talking to a server implemented in another language. For an array root, I enumerate the value directly. For the down-level object schema, I read the required result

property.

Checking discovery and invocation together also catches an asymmetric bug: a server could advertise an array schema but return a wrapped object, or advertise the legacy envelope and emit a bare array. Either response may contain the expected values while still violating the declared contract. The paired assertions fail on that mismatch before application parsing hides it.

Running the verifier produces a stable summary:

PASS 15/15
  modern: array schema has an array root
  modern: array result has no result wrapper
  modern: legacy result-wrapper parser is rejected
  legacy: array schema advertises a result envelope
  legacy: array result keeps the result envelope

The complete sample on main includes the scalar checks, setup commands, expected output, and vulnerability audit. The associated

This verifier proves the official MCP C# SDK 2.2.0 behavior over its stream transport. It does not prove that every host accepts every JSON Schema 2020-12 construct. A third-party client may lag the protocol, apply a narrower schema profile, or ignore structured content entirely. I would add an end-to-end test for each real host before relying on array or primitive roots in production.

An output schema also checks shape, not business meaning. The tool must still validate inputs, authorization, and domain rules. If every consumer already expects an object with named fields, returning a small response record may be clearer than returning a bare array just because the protocol permits it.

For mixed fleets, I let negotiation select the wire shape and make the client read according to the advertised outputSchema

. I avoid hard-coding either a wrapper or a natural root globally.

Which client contract would you add to this compatibility matrix first?

Happy coding!

── more in #developer-tools 4 stories · sorted by recency
── more on @mcp c# sdk 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mcp-c-sdk-array-tool…] indexed:0 read:4min 2026-08-21 ·