.NET: Delegate MCP ContentBlock to AIContent conversion to the MCP SDK (#5903)

* Add sample for invoking Foundry Toolbox tools from declarative workflows

* Addressed initial PR comments.

* Delegate MCP ContentBlock to AIContent conversion to the MCP SDK

* Addressed additional properties metadata in the conversion fallback.
This commit is contained in:
Peter Ibekwe
2026-05-18 20:39:56 +00:00
committed by GitHub
parent aad20c2b33
commit 3ebbdb01b4
2 changed files with 222 additions and 112 deletions
@@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
using ModelContextProtocol;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
@@ -27,6 +28,8 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Mcp;
/// </remarks>
public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
{
private const string FilenameAdditionalPropertyName = "filename";
/// <summary>
/// Reserved <c>toolName</c> value that maps an <see cref="IMcpToolHandler.InvokeToolAsync"/> request
/// to the MCP protocol <c>tools/list</c> discovery operation.
@@ -272,46 +275,46 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
internal static AIContent ConvertContentBlock(ContentBlock block)
{
return block switch
// Delegate to the MCP SDK's canonical converter. It maps every known
// ContentBlock subtype (Text/Image/Audio/EmbeddedResource/ToolUse/ToolResult)
// and sets RawRepresentation + AdditionalProperties from block.Meta.
// It intentionally returns null for ResourceLinkBlock — map that to
// UriContent here so callers always receive a usable AIContent.
return block.ToAIContent() ?? block switch
{
TextContentBlock text => new TextContent(text.Text),
ImageContentBlock image => CreateDataContent(image.Data, image.MimeType ?? "image/*"),
AudioContentBlock audio => CreateDataContent(audio.Data, audio.MimeType ?? "audio/*"),
EmbeddedResourceBlock embedded => ConvertEmbeddedResource(embedded),
_ => new TextContent(block.ToString() ?? string.Empty),
ResourceLinkBlock link => new UriContent(link.Uri, link.MimeType ?? "application/octet-stream")
{
RawRepresentation = link,
AdditionalProperties = CreateAdditionalProperties(link),
},
_ => new TextContent(block.ToString() ?? string.Empty)
{
RawRepresentation = block,
AdditionalProperties = CreateAdditionalProperties(block),
},
};
}
private static AIContent ConvertEmbeddedResource(EmbeddedResourceBlock block)
private static AdditionalPropertiesDictionary? CreateAdditionalProperties(ContentBlock block)
{
return block.Resource switch
{
TextResourceContents text => new TextContent(text.Text),
BlobResourceContents blob => CreateDataContent(blob.Blob, blob.MimeType ?? "application/octet-stream"),
_ => new TextContent(block.ToString() ?? string.Empty),
};
}
AdditionalPropertiesDictionary? properties = null;
private static DataContent CreateDataContent(ReadOnlyMemory<byte> base64Utf8Data, string mediaType)
{
if (base64Utf8Data.IsEmpty)
if (block.Meta is not null)
{
return new DataContent($"data:{mediaType};base64,", mediaType);
foreach (var property in block.Meta)
{
properties ??= new AdditionalPropertiesDictionary();
properties.Add(property.Key, property.Value);
}
}
#if NET8_0_OR_GREATER
string base64 = Encoding.UTF8.GetString(base64Utf8Data.Span);
#else
string base64 = Encoding.UTF8.GetString(base64Utf8Data.ToArray());
#endif
// If it's already a data URI, use it directly
if (base64.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
if (block is ResourceLinkBlock { Name: { Length: > 0 } name })
{
return new DataContent(base64, mediaType);
properties ??= new AdditionalPropertiesDictionary();
properties.TryAdd(FilenameAdditionalPropertyName, name);
}
return new DataContent($"data:{mediaType};base64,{base64}", mediaType);
return properties;
}
private static string SerializeToolsList(IEnumerable<Tool> tools)