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
45 changes: 45 additions & 0 deletions src/Netclaw.Actors.Tests/Tools/McpToolResultFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// </copyright>
// -----------------------------------------------------------------------
using System.Text.Json;
using Microsoft.Extensions.AI;
using Netclaw.Actors.Tools;
using Netclaw.Configuration;
using Netclaw.Tools;
Expand Down Expand Up @@ -134,4 +135,48 @@ public void Plain_string_result_is_passed_through()
[Fact]
public void Null_result_is_empty()
=> Assert.Equal(string.Empty, McpToolResultFormatter.Format(null, "srv/tool"));

[Fact]
public void Multi_content_AIContent_array_projects_text_and_image_marker()
{
var chartJson = """{"title":"Example title","series":[]}""";
var result = new AIContent[]
{
new DataContent(new byte[] { 1, 2, 3 }, "image/png"),
new TextContent(chartJson),
};

var message = McpToolResultFormatter.Format(result, "srv/chart");

Assert.Equal($"[image: image/png]\n{chartJson}", message);
Assert.DoesNotContain("AIContent", message);
}

[Fact]
public void Image_only_AIContent_array_projects_marker_only()
{
var result = new AIContent[] { new DataContent(Array.Empty<byte>(), "image/jpeg") };

Assert.Equal("[image: image/jpeg]", McpToolResultFormatter.Format(result, "srv/tool"));
}

[Fact]
public void Text_only_AIContent_array_projects_text()
{
var result = new AIContent[] { new TextContent("hello world") };

Assert.Equal("hello world", McpToolResultFormatter.Format(result, "srv/tool"));
}

[Fact]
public void Single_TextContent_is_passed_through()
=> Assert.Equal("done", McpToolResultFormatter.Format(new TextContent("done"), "srv/tool"));

[Fact]
public void Non_image_DataContent_projects_attachment_marker()
{
var result = new AIContent[] { new DataContent(new byte[] { 4, 5 }, "application/pdf") };

Assert.Equal("[attachment: application/pdf]", McpToolResultFormatter.Format(result, "srv/tool"));
}
}
42 changes: 42 additions & 0 deletions src/Netclaw.Actors/Tools/McpToolResultFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// </copyright>
// -----------------------------------------------------------------------
using System.Text.Json;
using Microsoft.Extensions.AI;
using Netclaw.Tools;

namespace Netclaw.Actors.Tools;
Expand Down Expand Up @@ -57,6 +58,15 @@ public static string Format(object? result, string toolName)
return string.IsNullOrWhiteSpace(detail) ? element.GetRawText() : detail;
}

if (result is AIContent singleContent)
return FormatAiContent(singleContent);

if (result is AIContent[] contentArray)
return FormatAiContents(contentArray);

if (result is IEnumerable<AIContent> contents)
return FormatAiContents(contents);

return result?.ToString() ?? string.Empty;
}

Expand Down Expand Up @@ -121,4 +131,36 @@ private static string JoinTextContent(JsonElement element)

return string.Join("\n", parts);
}

private static string FormatAiContents(IEnumerable<AIContent> contents)
{
var parts = new List<string>();
foreach (var content in contents)
{
var part = FormatAiContent(content);
if (!string.IsNullOrEmpty(part))
parts.Add(part);
}

return string.Join("\n", parts);
}

private static string FormatAiContent(AIContent content)
=> content switch
{
TextContent text when !string.IsNullOrEmpty(text.Text) => text.Text,
DataContent data => FormatDataContentMarker(data),
_ => string.Empty,
};

private static string FormatDataContentMarker(DataContent data)
{
var mediaType = string.IsNullOrWhiteSpace(data.MediaType)
? "application/octet-stream"
: data.MediaType;

return mediaType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)
? $"[image: {mediaType}]"
: $"[attachment: {mediaType}]";
}
}