using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
#if !NET
using System.Runtime.InteropServices;
#endif
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol;
///
/// Provides extension methods for converting between Model Context Protocol (MCP) types and Microsoft.Extensions.AI types.
///
///
/// This class serves as an adapter layer between Model Context Protocol (MCP) types and the model types
/// from the Microsoft.Extensions.AI namespace.
///
public static class AIContentExtensions
{
///
/// Creates a sampling handler for use with that will
/// satisfy sampling requests using the specified .
///
/// The with which to satisfy sampling requests.
/// The to use for serializing user-provided objects. If , is used.
/// The created handler delegate that can be assigned to .
///
///
/// This method creates a function that converts MCP message requests into chat client calls, enabling
/// an MCP client to generate text or other content using an actual AI model via the provided chat client.
///
///
/// The handler can process text messages, image messages, resource messages, and tool use/results as defined in the
/// Model Context Protocol.
///
///
/// is .
public static Func, CancellationToken, ValueTask> CreateSamplingHandler(
this IChatClient chatClient,
JsonSerializerOptions? serializerOptions = null)
{
Throw.IfNull(chatClient);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
return async (requestParams, progress, cancellationToken) =>
{
Throw.IfNull(requestParams);
var (messages, options) = ToChatClientArguments(requestParams, serializerOptions);
var progressToken = requestParams.ProgressToken;
List updates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
{
updates.Add(update);
if (progressToken is not null)
{
progress.Report(new() { Progress = updates.Count });
}
}
ChatResponse? chatResponse = updates.ToChatResponse();
ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault();
IList? contents = lastMessage?.Contents.Select(c => c.ToContentBlock()).ToList();
if (contents is not { Count: > 0 })
{
(contents ??= []).Add(new TextContentBlock() { Text = "" });
}
return new()
{
Model = chatResponse.ModelId ?? "",
StopReason =
chatResponse.FinishReason == ChatFinishReason.Stop ? CreateMessageResult.StopReasonEndTurn :
chatResponse.FinishReason == ChatFinishReason.Length ? CreateMessageResult.StopReasonMaxTokens :
chatResponse.FinishReason == ChatFinishReason.ToolCalls ? CreateMessageResult.StopReasonToolUse :
chatResponse.FinishReason.ToString(),
Meta = chatResponse.AdditionalProperties?.ToJsonObject(serializerOptions),
Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant,
Content = contents,
};
static (IList Messages, ChatOptions? Options) ToChatClientArguments(CreateMessageRequestParams requestParams, JsonSerializerOptions serializerOptions)
{
ChatOptions? options = null;
if (requestParams.MaxTokens is int maxTokens)
{
(options ??= new()).MaxOutputTokens = maxTokens;
}
if (requestParams.Temperature is float temperature)
{
(options ??= new()).Temperature = temperature;
}
if (requestParams.StopSequences is { } stopSequences)
{
(options ??= new()).StopSequences = stopSequences.ToArray();
}
if (requestParams.SystemPrompt is { } systemPrompt)
{
(options ??= new()).Instructions = systemPrompt;
}
if (requestParams.Tools is { } tools)
{
foreach (var tool in tools)
{
((options ??= new()).Tools ??= []).Add(new ToolAIFunctionDeclaration(tool));
}
if (options.Tools is { Count: > 0 } && requestParams.ToolChoice is { } toolChoice)
{
options.ToolMode = toolChoice.Mode switch
{
ToolChoice.ModeAuto => ChatToolMode.Auto,
ToolChoice.ModeRequired => ChatToolMode.RequireAny,
ToolChoice.ModeNone => ChatToolMode.None,
_ => null,
};
}
}
List messages = [];
foreach (var sm in requestParams.Messages)
{
if (sm.Content?.Select(b => b.ToAIContent(serializerOptions)).OfType().ToList() is { Count: > 0 } aiContents)
{
ChatRole role =
aiContents.All(static c => c is FunctionResultContent) ? ChatRole.Tool :
sm.Role is Role.Assistant ? ChatRole.Assistant :
ChatRole.User;
messages.Add(new ChatMessage(role, aiContents));
}
}
return (messages, options);
}
};
}
/// Converts the specified dictionary to a .
internal static JsonObject? ToJsonObject(this IReadOnlyDictionary properties, JsonSerializerOptions options)
{
return JsonSerializer.SerializeToNode(properties, options.GetTypeInfo(typeof(IReadOnlyDictionary))) as JsonObject;
}
internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonObject obj)
{
AdditionalPropertiesDictionary d = [];
foreach (var kvp in obj)
{
d.Add(kvp.Key, kvp.Value);
}
return d;
}
///
/// Converts a to a object.
///
/// The prompt message to convert.
/// The to use for deserialization. If , is used.
/// A object created from the prompt message.
///
/// This method transforms a protocol-specific from the Model Context Protocol
/// into a standard object that can be used with AI client libraries.
///
/// is .
public static ChatMessage ToChatMessage(this PromptMessage promptMessage, JsonSerializerOptions? options = null)
{
Throw.IfNull(promptMessage);
AIContent? content = promptMessage.Content.ToAIContent(options);
return new()
{
RawRepresentation = promptMessage,
Role = promptMessage.Role == Role.User ? ChatRole.User : ChatRole.Assistant,
Contents = content is not null ? [content] : [],
};
}
///
/// Converts a to a object.
///
/// The tool result to convert.
/// The identifier for the function call request that triggered the tool invocation.
/// The to use for serialization. If , is used.
/// A object created from the tool result.
///
/// This method transforms a protocol-specific from the Model Context Protocol
/// into a standard object that can be used with AI client libraries. It produces a
/// message containing a with result as a
/// serialized .
///
/// or is .
public static ChatMessage ToChatMessage(this CallToolResult result, string callId, JsonSerializerOptions? options = null)
{
Throw.IfNull(result);
Throw.IfNull(callId);
options ??= McpJsonUtilities.DefaultOptions;
return new(ChatRole.Tool, [new FunctionResultContent(callId, JsonSerializer.SerializeToElement(result, options.GetTypeInfo()))
{
RawRepresentation = result,
}]);
}
///
/// Converts a to a list of objects.
///
/// The prompt result containing messages to convert.
/// A list of objects created from the prompt messages.
///
/// This method transforms protocol-specific objects from a Model Context Protocol
/// prompt result into standard objects that can be used with AI client libraries.
///
/// is .
public static IList ToChatMessages(this GetPromptResult promptResult)
{
Throw.IfNull(promptResult);
return promptResult.Messages.Select(m => m.ToChatMessage()).ToList();
}
///
/// Converts a to a list of objects.
///
/// The chat message to convert.
/// A list of objects created from the chat message's contents.
///
/// This method transforms standard objects used with AI client libraries into
/// protocol-specific objects for the Model Context Protocol system.
/// Only representable content items are processed.
///
/// is .
public static IList ToPromptMessages(this ChatMessage chatMessage)
{
Throw.IfNull(chatMessage);
Role r = chatMessage.Role == ChatRole.User ? Role.User : Role.Assistant;
List messages = [];
foreach (var content in chatMessage.Contents)
{
if (content is TextContent or DataContent)
{
messages.Add(new PromptMessage { Role = r, Content = content.ToContentBlock() });
}
}
return messages;
}
/// Creates a new from the content of a .
/// The to convert.
/// The to use for deserialization. If , is used.
///
/// The created . If the content can't be converted (such as when it's a resource link), is returned.
///
///
/// This method converts Model Context Protocol content types to the equivalent Microsoft.Extensions.AI
/// content types, enabling seamless integration between the protocol and AI client libraries.
///
/// is .
public static AIContent? ToAIContent(this ContentBlock content, JsonSerializerOptions? options = null)
{
Throw.IfNull(content);
options ??= McpJsonUtilities.DefaultOptions;
AIContent? ac = content switch
{
TextContentBlock textContent => new TextContent(textContent.Text),
ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType),
AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType),
EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(),
ToolUseContentBlock toolUse => FunctionCallContent.CreateFromParsedArguments(toolUse.Input, toolUse.Id, toolUse.Name,
json => JsonSerializer.Deserialize(json, options.GetTypeInfo>())),
ToolResultContentBlock toolResult => new FunctionResultContent(
toolResult.ToolUseId,
toolResult.Content.Count == 1 ? toolResult.Content[0].ToAIContent(options) : toolResult.Content.Select(c => c.ToAIContent(options)).OfType().ToList())
{
Exception = toolResult.IsError is true ? new() : null,
},
_ => null,
};
if (ac is not null)
{
ac.RawRepresentation = content;
ac.AdditionalProperties = content.Meta?.ToAdditionalProperties();
}
return ac;
}
/// Creates a new from the content of a .
/// The to convert.
/// The created .
///
/// This method converts Model Context Protocol resource types to the equivalent Microsoft.Extensions.AI
/// content types, enabling seamless integration between the protocol and AI client libraries.
///
/// is .
/// The resource type is not supported.
public static AIContent ToAIContent(this ResourceContents content)
{
Throw.IfNull(content);
AIContent ac = content switch
{
BlobResourceContents blobResource => new DataContent(Convert.FromBase64String(blobResource.Blob), blobResource.MimeType ?? "application/octet-stream"),
TextResourceContents textResource => new TextContent(textResource.Text),
_ => throw new NotSupportedException($"Resource type '{content.GetType().Name}' is not supported.")
};
(ac.AdditionalProperties ??= [])["uri"] = content.Uri;
ac.RawRepresentation = content;
return ac;
}
/// Creates a list of from a sequence of .
/// The instances to convert.
/// The to use for deserialization. If , is used.
/// The created instances.
///
///
/// This method converts a collection of Model Context Protocol content objects into a collection of
/// Microsoft.Extensions.AI content objects. It's useful when working with multiple content items, such as
/// when processing the contents of a message or response.
///
///
/// Each object is converted using ,
/// preserving the type-specific conversion logic for text, images, audio, and resources.
///
///
/// is .
public static IList ToAIContents(this IEnumerable contents, JsonSerializerOptions? options = null)
{
Throw.IfNull(contents);
return [.. contents.Select(c => c.ToAIContent(options)).OfType()];
}
/// Creates a list of from a sequence of .
/// The instances to convert.
/// A list of objects created from the resource contents.
///
///
/// This method converts a collection of Model Context Protocol resource objects into a collection of
/// Microsoft.Extensions.AI content objects. It's useful when working with multiple resources, such as
/// when processing the contents of a .
///
///
/// Each object is converted using ,
/// preserving the type-specific conversion logic: text resources become objects and
/// binary resources become objects.
///
///
/// is .
public static IList ToAIContents(this IEnumerable contents)
{
Throw.IfNull(contents);
return [.. contents.Select(ToAIContent)];
}
/// Creates a new from the content of an .
/// The to convert.
/// The to use for serialization. If , is used.
/// The created .
/// is .
public static ContentBlock ToContentBlock(this AIContent content, JsonSerializerOptions? options = null)
{
Throw.IfNull(content);
options ??= McpJsonUtilities.DefaultOptions;
ContentBlock contentBlock = content switch
{
TextContent textContent => new TextContentBlock
{
Text = textContent.Text,
},
DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentBlock
{
Data = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType,
},
DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentBlock
{
Data = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType,
},
DataContent dataContent => new EmbeddedResourceBlock
{
Resource = new BlobResourceContents
{
Blob = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType,
Uri = string.Empty,
}
},
FunctionCallContent callContent => new ToolUseContentBlock()
{
Id = callContent.CallId,
Name = callContent.Name,
Input = JsonSerializer.SerializeToElement(callContent.Arguments, options.GetTypeInfo>()!),
},
FunctionResultContent resultContent => new ToolResultContentBlock()
{
ToolUseId = resultContent.CallId,
IsError = resultContent.Exception is not null,
Content =
resultContent.Result is AIContent c ? [c.ToContentBlock(options)] :
resultContent.Result is IEnumerable ec ? [.. ec.Select(c => c.ToContentBlock(options))] :
[new TextContentBlock { Text = JsonSerializer.Serialize(content, options.GetTypeInfo