---
title: "Return structured tool output in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/return-structured-output/
description: "Return a C# record as structured tool content with an output schema. Run a complete server and client locally with OneModelContext."
version: "1.0.1"
---

# Return structured tool output in C#

**OneModelContext** is a free, open-source .NET SDK for the Model Context Protocol® from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModelContext) · [GitHub](https://github.com/recursive-ai-oss/onemodelcontext-csharp) · [Docs](https://recursiveai.org/onemodelcontext/docs/) · [Download example (.zip)](https://recursiveai.org/onemodelcontext/cookbook/return-structured-output/return-structured-output.zip)

Return a Model Context Protocol® (MCP) JSON object with an output schema describing its fields.

```shell
dotnet add package RecursiveAI.OneModelContext
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneModelContext;
using System.Text.Json.Serialization;
await ModelContext.ServeAsync(new WeatherTools());

public partial class WeatherTools
{
    [Tool]
    public Weather Forecast() => new("Lisbon", 21);
}
public record Weather(string City, int Celsius);
[JsonSerializable(typeof(Weather))]
public partial class WeatherJson
    : JsonSerializerContext { }
```

Unzip [the example](https://recursiveai.org/onemodelcontext/cookbook/return-structured-output/return-structured-output.zip), then `dotnet run`.

To see a result, run the downloaded program: its built-in client calls `forecast` and prints the generated output schema and returned JSON. The snippet at the top serves a `forecast` tool and prints no application text. The core package’s generator supplies the tool registration; System.Text.Json generates the `WeatherJson` metadata. Both classes must remain `partial`.

The download starts a server and client over in-memory pipes, lists the output schema, calls `forecast`, and prints the structured object followed by its text copy. The city and temperature are fixed sample values, not a weather service. A record missing its serializer context causes build diagnostic `OMCG0005`.

## The full program

```csharp
using System.IO.Pipelines;
using RecursiveAI.OneModelContext;

var toServer = new Pipe();
var toClient = new Pipe();
var server = new ContextServer("example", "1.0.0");
new ExampleTools().AddTo(server);
using var stop = new CancellationTokenSource();
var serving = server.RunAsync(ContextTransport.Stream(
    toServer.Reader.AsStream(), toClient.Writer.AsStream()), stop.Token);
await using var client = await ModelContext.ConnectAsync(
    ContextTransport.Stream(toClient.Reader.AsStream(), toServer.Writer.AsStream()));
var tool = (await client.ListToolsAsync()).Single();
Console.WriteLine(tool.OutputSchema.GetRawText());
var result = await client.CallToolAsync("forecast");
Console.WriteLine(result.StructuredContent.GetRawText());
Console.WriteLine(result.Text);
await stop.CancelAsync();
try { await serving; }
catch (OperationCanceledException) { }

public partial class ExampleTools
{
    [Tool(ReadOnly = true)]
    public Weather Forecast() => new("Lisbon", 21);
}
public record Weather(string City, int Celsius);
[System.Text.Json.Serialization.JsonSerializable(typeof(Weather))]
public partial class WeatherJson : System.Text.Json.Serialization.JsonSerializerContext { }
```

`dotnet run` prints:

```text
{"type":"object","properties":{"City":{"type":"string"},"Celsius":{"type":"integer","minimum":-2147483648,"maximum":2147483647}},"required":["City","Celsius"],"additionalProperties":false}
{"City":"Lisbon","Celsius":21}
{"City":"Lisbon","Celsius":21}
```

## Variations

Read one field instead of printing the whole object. Replace the first `result` print statement in the full program with:

```csharp
Console.WriteLine(result.StructuredContent
    .GetProperty("Celsius").GetInt32());
```

That line prints:

```text
21
```

## Related recipes

- [Build a server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Connect to a server](https://recursiveai.org/onemodelcontext/cookbook/connect-to-a-server/)
- [Require OAuth sign-in on a server](https://recursiveai.org/onemodelcontext/cookbook/require-sign-in-on-a-server/)

---

## The short answer

To return MCP tool output with a schema in C# (Model Context Protocol®), return a record, class or dictionary from a generated `[Tool]` method and list the type in a `JsonSerializerContext`. OneModelContext supplies an `outputSchema`, structured content and a text copy of the same JSON. Returning a JSON string is different: it produces text, not this object-shaped contract.

## How do I return structured output from a C# tool?

Choose a concrete result type that describes the data the caller needs. The example uses `Weather` with a string city and integer Celsius value. Returning `new Weather("Lisbon", 21)` gives the generator the object shape; `[JsonSerializable(typeof(Weather))]` supplies serialization metadata without a reflection fallback.

The download asks for the tool listing before invoking the tool. Its first output line prints the full generated output schema JSON, while the next lines read the result itself. These are separate protocol messages: a schema is the advertised contract, not the result of executing the tool. Keep result fields consistent with that contract as you evolve the tool.

Use the property names in the JSON actually returned. In this example they are `City` and `Celsius`, including capitalization. The variation reads the integer from the structured content directly, so it does not parse prose or extract JSON from a text block.

## What is a tool output schema?

The [protocol tools specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) defines `outputSchema` as an optional description of structured results. A server that advertises one must provide conforming structured data; clients should validate it. This is tool-produced data, not schema-constrained generation by a language model.

OneModelContext’s [type mapping](https://recursiveai.org/onemodelcontext/docs/tool-schemas/) describes the generated JSON Schema dialect and supported types. Object-shaped return values get structured output. Numbers, Booleans and collections returned directly instead become JSON text blocks. To give several related scalar values an object contract, put them in a record as this example does.

A `JsonElement` or `JsonNode` return is also documented as a JSON text block. It is useful when text is what you want, but it is not a shortcut to the same generated output schema. Decide whether the consumer needs an object contract before choosing the method’s return type.

## Why is the structured result also returned as text?

The [specification’s structured-content section](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content) recommends a serialized JSON text copy for backward compatibility. OneModelContext returns both forms for these object results. The duplicate lines in the download’s output are intentional: one comes from `StructuredContent`, the other from `Text`.

That duplication does not mean the tool ran twice. It means one result has two representations. Prefer `StructuredContent` when consuming fields programmatically and use `Text` when presenting the ordinary text representation.

A nullable object result has an additional distinction. The library keeps an object output schema, but a null result is sent as text `null` without `structuredContent`. Check for that case before calling `GetProperty` in an application that permits null results. Tool exceptions are separate again: a `ToolException` produces an error result with its message, while an unexpected exception produces the fixed text `The tool failed.` rather than exposing its internals.
