Return a Model Context Protocol® (MCP) JSON object with an output schema describing its fields.
dotnet add package RecursiveAI.OneModelContextRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
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, 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
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:
{"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:
Console.WriteLine(result.StructuredContent .GetProperty("Celsius").GetInt32());
That line prints:
21Related recipes
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 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 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 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.