Skip to content

.NET: [Issue]:MCP content + structuredContent duplicate model input without a selection policy #9884

Open

Description

Category

Documentation

Problem

Description

Following the standard .NET MCP integration pattern sends both content and structuredContent to the model when a server returns the same JSON payload in both fields.
Python Agent Framework provides [tool_result_content](https://github.com/microsoft/agent-framework/pull/8274) to choose whether content, structuredContent, or both are sent to the model. Is there an equivalent option in .NET? The default .NET flow sends both even when they contain identical data, unnecessarily increasing token usage.

Evidence

Minimal reproduction

ASP.NET Core project with the packages listed below. The MCP server returns a typed object; the client passes the discovered tools directly to Agent Framework.

// Program.cs
using Microsoft.Agents.AI;
using ModelContextProtocol.Client;
using OpenAI;
using OpenAI.Responses;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://127.0.0.1:5017");
builder.Services.AddMcpServer()
    .WithHttpTransport()
    .WithTools<MyTools>();

await using var app = builder.Build();
app.MapMcp("/mcp");
await app.StartAsync();

try
{
    await using var mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
    {
        Endpoint = new Uri("http://127.0.0.1:5017/mcp"),
    }));
    var tools = await mcpClient.ListToolsAsync();

    var openAi = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!);
    AIAgent agent = openAi.GetResponsesClient().AsAIAgent(
        model: "gpt-5.4",
        instructions: "Call get_report once and report its marker.",
        tools: [.. tools]);

    Console.WriteLine(await agent.RunAsync("Get the report."));
}
finally
{
    await app.StopAsync();
}

// MyTools.cs
using System.ComponentModel;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class MyTools
{
    [McpServerTool(Name = "get_report", UseStructuredContent = true)]
    [Description("Returns a sample report.")]
    public Report GetReport() => new("PROBE_MARKER", 3);
}

public sealed record Report(string Marker, int Count);

Actual behavior

The next LLM request contains one function_call_output whose output string includes both representations. For the simplified report above, the shape is:

{
  "content": [
    { "type": "text", "text": "{\"marker\":\"PROBE_MARKER\",\"count\":3}" }
  ],
  "structuredContent": {
    "marker": "PROBE_MARKER",
    "count": 3
  }
}

Desired Outcome

Expected behavior / documentation request

Could the .NET MCP integration provide or document an explicit model-visible result policy, analogous to Python's structured_first, content_first, content_only, structured_only, and both?

At minimum, the .NET MCP sample/docs should explain:

  • Which fields are forwarded by default, including the full-envelope fallback and its token implications.
  • The supported way to choose the model-facing representation before the next request in the function-calling loop.
  • How that choice affects errors, metadata, images/audio, embedded resources, and resource links.

I am not asking for an unconditional return structuredContent: that would drop useful content for tools whose fields are complementary. A documented, explicit policy would be preferable to silently inheriting a fixed conversion behavior.

The generic FICC FunctionInvoker hook exists, but that is different from a documented MCP-specific policy and still requires the application to implement the projection correctly.

Possible Workaround

ficc.FunctionInvoker = async (context, cancellationToken) =>
{
    var result = await context.Function.InvokeAsync(
        context.Arguments, cancellationToken);

    if (context.Function.GetService<McpClientTool>() is not null &&
        result is JsonElement { ValueKind: JsonValueKind.Object } json)
    {
        if (json.TryGetProperty("isError", out var error) &&
            error.ValueKind == JsonValueKind.True)
        {
            return result;
        }
  // Example policy: `structured-first.`
        if (json.TryGetProperty("structuredContent", out var structured) &&
            structured.ValueKind is not (JsonValueKind.Null or JsonValueKind.Undefined))
        {
            return structured.Clone();
        }
    }

    return result;
};

Language

.NET

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    .NETUsage: [Issues, PRs], Target: .Net
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 30, 2026
  2. changed the title [-][Issue]:[/-] [+].NET: [Issue]:[/+] on Oct 30, 2026
  3. changed the title [-].NET: [Issue]:[/-] [+].NET: [Issue]:MCP content + structuredContent duplicate model input without a selection policy[/+] on Oct 30, 2026
  4. Manish-1kumar commented on Oct 31, 2026

    @Manish-1kumar

    Hi team,

    I noticed this discussion regarding token duplication when an MCP server returns both content and structuredContent.

    I would be interested in helping contribute a fix or sample once the preferred direction is established. Could maintainers provide guidance on whether the preferred approach is:

    1. A configurable selection policy (e.g. preferring structuredContent over raw content when both exist)?
    2. Or clarifying documentation/best-practice guidance in samples for how callers configure content projection?

    Happy to help implement or document this once the team decides on the approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETUsage: [Issues, PRs], Target: .NettriageUsage: [Issues], Target: All issues that still need to be triaged

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions