{"slug": "mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper", "title": "MCP C# SDK Array Tool Outputs: Stop Looking for a `result` Wrapper", "summary": "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.", "body_md": "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`\n\n. Once both sides negotiate MCP `2026-07-28`\n\n, an array is an array and a scalar is a scalar. There is no required wrapper to unwrap.\n\nI 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`\n\nadvertised or what `tools/call`\n\nactually carried. The small verifier below runs both protocol versions offline and makes the difference explicit.\n\nThe [MCP 2026-07-28 tools specification](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) allows `structuredContent`\n\nto contain any JSON value: object, array, string, number, Boolean, or null. An `outputSchema`\n\nmay 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.\n\nThis is different from the older object-only convention. Under `2025-11-25`\n\n, a tool returning `string[]`\n\nneeds an object envelope such as this:\n\n```\n{\n  \"structuredContent\": {\n    \"result\": [\"starter\", \"growth\", \"enterprise\"]\n  }\n}\n```\n\nThe modern form is the value itself:\n\n```\n{\n  \"structuredContent\": [\"starter\", \"growth\", \"enterprise\"]\n}\n```\n\nThe 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`\n\n. 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.\n\nThe sample pins the current stable package and targets .NET 10:\n\n```\n<ItemGroup>\n  <PackageReference Include=\"ModelContextProtocol\" Version=\"2.2.0\" />\n</ItemGroup>\n```\n\nIt registers one array tool and one scalar tool. `UseStructuredContent = true`\n\ntells the SDK to generate the output schema and serialize the return value into `structuredContent`\n\n.\n\n```\ntoolCollection.Add(McpServerTool.Create(\n    (Func<string[]>)ListTiers,\n    new McpServerToolCreateOptions\n    {\n        Name = \"list_tiers\",\n        UseStructuredContent = true,\n        ReadOnly = true,\n    }));\n\ntoolCollection.Add(McpServerTool.Create(\n    (Func<int>)CountTiers,\n    new McpServerToolCreateOptions\n    {\n        Name = \"count_tiers\",\n        UseStructuredContent = true,\n        ReadOnly = true,\n    }));\n```\n\nTwo `System.IO.Pipelines.Pipe`\n\ninstances connect an `McpClient`\n\nand `McpServer`\n\n. 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\"`\n\nand a fresh pair with `\"2025-11-25\"`\n\n.\n\nUsing separate sessions matters. The negotiated protocol belongs to a connection, so changing an option after discovery would not prove the real compatibility path.\n\nFor the modern session, I assert both the advertised schema and the returned value:\n\n```\nJsonElement arraySchema = arrayTool.ProtocolTool.OutputSchema!.Value;\nCallToolResult arrayResult = await arrayTool.CallAsync();\n\nCheck(arraySchema.GetProperty(\"type\").GetString() == \"array\");\nCheck(arrayResult.StructuredContent?.ValueKind == JsonValueKind.Array);\nCheck(!TryReadLegacyResult(arrayResult.StructuredContent!.Value, out _));\n```\n\nThat 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`\n\n. If the contract test only checks the values, an accidental wrapper can slip back in unnoticed.\n\nThe legacy session checks the opposite shape:\n\n```\nCheck(IsLegacyEnvelope(arraySchema, \"array\"));\nCheck(\n    arrayResult.StructuredContent?\n        .GetProperty(\"result\")\n        .ValueKind == JsonValueKind.Array);\n```\n\nBoth 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.\n\nOn 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`\n\nproperty.\n\nChecking 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.\n\nRunning the verifier produces a stable summary:\n\n```\nPASS 15/15\n  modern: array schema has an array root\n  modern: array result has no result wrapper\n  modern: legacy result-wrapper parser is rejected\n  legacy: array schema advertises a result envelope\n  legacy: array result keeps the result envelope\n```\n\nThe 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\n\nThis 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.\n\nAn 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.\n\nFor mixed fleets, I let negotiation select the wire shape and make the client read according to the advertised `outputSchema`\n\n. I avoid hard-coding either a wrapper or a natural root globally.\n\nWhich client contract would you add to this compatibility matrix first?\n\nHappy coding!", "url": "https://wpnews.pro/news/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper", "canonical_source": "https://dev.to/ssukhpinder/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper-452c", "published_at": "2026-08-21 01:23:31+00:00", "updated_at": "2026-08-21 01:43:48.940885+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["MCP C# SDK", "ModelContextProtocol", "McpClient", "McpServer", "McpServerTool", "McpClientOptions", "UseStructuredContent"], "alternates": {"html": "https://wpnews.pro/news/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper", "markdown": "https://wpnews.pro/news/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper.md", "text": "https://wpnews.pro/news/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper.txt", "jsonld": "https://wpnews.pro/news/mcp-c-sdk-array-tool-outputs-stop-looking-for-a-result-wrapper.jsonld"}}