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

> Source: <https://dev.to/ssukhpinder/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper-452c>
> Published: 2026-08-21 01:23:31+00:00

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](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) 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](https://github.com/modelcontextprotocol/csharp-sdk/releases/tag/v2.0.0) call out direct non-object tool results, while the current [v2.2.0 tools guide](https://csharp.sdk.modelcontextprotocol.io/concepts/tools/tools.html) 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](https://github.com/ssukhpinder/dev-to-code-samples/tree/main/056-mcp-array-tool-outputs) 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!
