Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
.WithTools<PrintEnvTool>()
.WithTools<SampleLlmTool>()
.WithTools<TinyImageTool>()
.WithTools<WeatherStructuredTool>()
.WithPrompts<ComplexPromptType>()
.WithPrompts<SimplePromptType>()
.WithResources<SimpleResourceType>()
Expand Down
48 changes: 48 additions & 0 deletions samples/EverythingServer/Tools/WeatherStructuredTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Text.Json;

namespace EverythingServer.Tools;

// Demonstrates the SEP-2200 ("Clarify tool result content visibility") recommended pattern:
// return a CallToolResult with a model-friendly Content (prose) AND a machine-friendly
// StructuredContent (strict JSON), advertising the JSON schema via OutputSchemaType.
//
// The default behaviour for [McpServerTool(UseStructuredContent = true)] returning a plain
// object is to JSON-stringify the same payload into both fields. SEP-2200 calls that
// "acceptable but may be suboptimal" — a short prose summary in Content saves tokens and
// is easier for the model to reason about, while StructuredContent stays available for
// programmatic consumers (UI, downstream tools, orchestration logic).
[McpServerToolType]
public class WeatherStructuredTool
{
public sealed record WeatherReading(string City, int TempF, string Condition, int Humidity);

[McpServerTool(
Name = "getWeather",
UseStructuredContent = true,
OutputSchemaType = typeof(WeatherReading)),
Description("Gets the current weather for a city.")]
public static CallToolResult GetWeather(
[Description("The city to look up the weather for.")] string city)
{
// In a real tool, fetch this from a weather API.
var reading = new WeatherReading(City: city, TempF: 72, Condition: "sunny", Humidity: 40);

return new CallToolResult
{
// Model-oriented: short, prose-friendly. This is what an LLM reads.
Content =
[
new TextContentBlock
{
Text = $"It's {reading.TempF}°F and {reading.Condition} in {reading.City} (humidity {reading.Humidity}%).",
},
],
// Machine-oriented: strict JSON for UIs, downstream tools, orchestrators.
// Validates against the schema generated from typeof(WeatherReading).
StructuredContent = JsonSerializer.SerializeToElement(reading),
};
}
}
14 changes: 14 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,20 @@ public Task UnsubscribeFromResourceAsync(
/// <returns>The <see cref="CallToolResult"/> from the tool execution.</returns>
/// <exception cref="ArgumentNullException"><paramref name="toolName"/> is <see langword="null"/>.</exception>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// <para>
/// The returned <see cref="CallToolResult"/> may carry both <see cref="CallToolResult.Content"/>
/// (model-oriented) and <see cref="CallToolResult.StructuredContent"/> (machine-oriented JSON).
/// Per SEP-2200, callers forwarding the result to a language model SHOULD prefer
/// <see cref="CallToolResult.Content"/>, falling back to <see cref="CallToolResult.StructuredContent"/>
/// only when <see cref="CallToolResult.Content"/> is empty, and SHOULD NOT pass both fields
/// verbatim to the model.
/// </para>
/// <para>
/// Callers consuming the result programmatically (UI rendering, downstream tools, orchestration
/// logic) should use <see cref="CallToolResult.StructuredContent"/> when present.
/// </para>
/// </remarks>
public ValueTask<CallToolResult> CallToolAsync(
string toolName,
IReadOnlyDictionary<string, object?>? arguments = null,
Expand Down
7 changes: 6 additions & 1 deletion src/ModelContextProtocol.Core/Client/McpClientTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,13 @@ internal McpClientTool(
// back to the AI service as a multi-modal tool response). However, when there is additional information
// carried by the CallToolResult outside of its ContentBlocks, just returning AIContent from those ContentBlocks
// would lose that information. So, we only do the translation if there is no additional information to preserve.
//
// Per SEP-2200 ("Clarify tool result content visibility"), Content is the model-oriented field and
// StructuredContent is the machine-oriented field. When both are populated, clients SHOULD prefer
// Content for model context and SHOULD NOT forward StructuredContent verbatim to the model. The gate
// therefore deliberately does NOT include `result.StructuredContent is null` — the presence of
// StructuredContent alongside Content is exactly the SEP-2200 recommended shape.
if (result.IsError is not true &&
result.StructuredContent is null &&
result.Meta is not { Count: > 0 })
{
switch (result.Content.Count)
Expand Down
37 changes: 35 additions & 2 deletions src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,47 @@ namespace ModelContextProtocol.Protocol;
public sealed class CallToolResult : Result
{
/// <summary>
/// Gets or sets the response content from the tool call.
/// Gets or sets the model-oriented response content from the tool call.
/// </summary>
/// <remarks>
/// <para>
/// Per the MCP specification (see SEP-2200, "Clarify tool result content visibility"),
/// <see cref="Content"/> is the representation intended for consumption by language models.
/// It may be a concise, prose-friendly summary rather than a verbatim dump of
/// <see cref="StructuredContent"/>.
/// </para>
/// <para>
/// Clients SHOULD prefer <see cref="Content"/> when forwarding a tool result to a model, falling
/// back to <see cref="StructuredContent"/> only when <see cref="Content"/> is empty. Clients SHOULD NOT
/// forward both <see cref="Content"/> and <see cref="StructuredContent"/> verbatim to the model — doing so
/// duplicates information and wastes tokens.
/// </para>
/// <para>
/// When both fields are populated, they SHOULD be semantically equivalent.
/// </para>
/// </remarks>
[JsonPropertyName("content")]
public IList<ContentBlock> Content { get; set; } = [];

/// <summary>
/// Gets or sets an optional JSON object representing the structured result of the tool call.
/// Gets or sets an optional JSON object representing the machine-oriented structured result of the tool call.
/// </summary>
/// <remarks>
/// <para>
/// Per the MCP specification (see SEP-2200, "Clarify tool result content visibility"),
/// <see cref="StructuredContent"/> is the strict JSON representation intended for programmatic consumers
/// (UI code, downstream tools, orchestrators) — not for direct submission to a language model.
/// </para>
/// <para>
/// When <see cref="StructuredContent"/> is present and a tool advertises an <see cref="Tool.OutputSchema"/>,
/// the value SHOULD validate against that schema.
/// </para>
/// <para>
/// <see cref="Content"/> and <see cref="StructuredContent"/> SHOULD be semantically equivalent when both
/// are populated, but <see cref="Content"/> MAY be a summary or prose form of the same data rather than a
/// verbatim JSON dump.
/// </para>
/// </remarks>
[JsonPropertyName("structuredContent")]
public JsonElement? StructuredContent { get; set; }

Expand Down
21 changes: 21 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,23 @@ public bool ReadOnly
/// The default is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// When enabled, the tool will attempt to populate the <see cref="Tool.OutputSchema"/>
/// and provide structured content in the <see cref="CallToolResult.StructuredContent"/> property.
/// </para>
/// <para>
/// When the tool method returns an arbitrary type (not <see cref="CallToolResult"/>), the SDK
/// serializes the return value into both <see cref="CallToolResult.Content"/> (as a single text block
/// containing the JSON) and <see cref="CallToolResult.StructuredContent"/>. Per SEP-2200 this is
/// acceptable but may be suboptimal — model-facing prose is normally a better fit for
/// <see cref="CallToolResult.Content"/> than a JSON dump.
/// </para>
/// <para>
/// To return distinct model-friendly text and machine-friendly JSON, declare the tool's return type as
/// <see cref="CallToolResult"/>, set <see cref="OutputSchemaType"/> to the type the structured payload
/// should validate against, and populate <see cref="CallToolResult.Content"/> and
/// <see cref="CallToolResult.StructuredContent"/> separately on the returned value.
/// </para>
/// </remarks>
public bool UseStructuredContent { get; set; }

Expand All @@ -281,6 +296,12 @@ public bool ReadOnly
/// schema to clients.
/// </para>
/// <para>
/// This is the recommended way to follow SEP-2200's guidance of supplying a model-friendly
/// <see cref="CallToolResult.Content"/> alongside a strict, schema-validated
/// <see cref="CallToolResult.StructuredContent"/>: return a <see cref="CallToolResult"/> with both
/// fields set explicitly and use <see cref="OutputSchemaType"/> to advertise the JSON schema.
/// </para>
/// <para>
/// <see cref="UseStructuredContent"/> must also be set to <see langword="true"/> for this property to take effect.
/// </para>
/// </remarks>
Expand Down
21 changes: 21 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,23 @@ public sealed class McpServerToolCreateOptions
/// The default is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// When enabled, the tool will attempt to populate the <see cref="Tool.OutputSchema"/>
/// and provide structured content in the <see cref="CallToolResult.StructuredContent"/> property.
/// </para>
/// <para>
/// When the tool method returns an arbitrary type (not <see cref="CallToolResult"/>), the SDK
/// serializes the return value into both <see cref="CallToolResult.Content"/> (as a single text block
/// containing the JSON) and <see cref="CallToolResult.StructuredContent"/>. Per SEP-2200 this is
/// acceptable but may be suboptimal — model-facing prose is normally a better fit for
/// <see cref="CallToolResult.Content"/> than a JSON dump.
/// </para>
/// <para>
/// To return distinct model-friendly text and machine-friendly JSON, declare the tool's return type as
/// <see cref="CallToolResult"/>, set <see cref="OutputSchema"/> to the schema the structured payload
/// should validate against, and populate <see cref="CallToolResult.Content"/> and
/// <see cref="CallToolResult.StructuredContent"/> separately on the returned value.
/// </para>
/// </remarks>
public bool UseStructuredContent { get; set; }

Expand All @@ -144,6 +159,12 @@ public sealed class McpServerToolCreateOptions
/// needs to advertise a meaningful output schema to clients.
/// </para>
/// <para>
/// This is the recommended way to follow SEP-2200's guidance of supplying a model-friendly
/// <see cref="CallToolResult.Content"/> alongside a strict, schema-validated
/// <see cref="CallToolResult.StructuredContent"/>: return a <see cref="CallToolResult"/> with both
/// fields set explicitly and supply the JSON schema here.
/// </para>
/// <para>
/// <see cref="UseStructuredContent"/> must also be set to <see langword="true"/> for this property to take effect.
/// </para>
/// </remarks>
Expand Down
15 changes: 8 additions & 7 deletions tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -421,20 +421,21 @@ public async Task ErrorTool_ReturnsJsonElement()
}

[Fact]
public async Task StructuredContentTool_ReturnsJsonElement()
public async Task StructuredContentTool_PrefersContentBlocksForModel()
{
// SEP-2200 ("Clarify tool result content visibility"): when both Content and
// StructuredContent are populated and there is no protocol-level information
// to preserve (no IsError, no Meta), the AIFunction adapter must forward
// Content (the model-oriented field) to the model — not the full CallToolResult
// with both fields, which would duplicate information for the LLM.
await using McpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
var tool = tools.Single(t => t.Name == "structured_content_tool");

var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);

var jsonElement = Assert.IsType<JsonElement>(result);
Assert.True(jsonElement.TryGetProperty("structuredContent", out var structuredContent));
Assert.True(structuredContent.TryGetProperty("key", out var key));
Assert.Equal("value", key.GetString());
Assert.True(jsonElement.TryGetProperty("content", out var content));
Assert.Equal(JsonValueKind.Array, content.ValueKind);
var textContent = Assert.IsType<TextContent>(result);
Assert.Equal("Regular content", textContent.Text);
}

[Fact]
Expand Down
Loading