using Microsoft.Extensions.AI; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using System.Text; 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(imageContent.DecodedData, imageContent.MimeType), AudioContentBlock audioContent => new DataContent(audioContent.DecodedData, 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(blobResource.DecodedData, 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 = EncodingUtilities.GetUtf8Bytes(dataContent.Base64Data.Span), MimeType = dataContent.MediaType, }, DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentBlock { Data = EncodingUtilities.GetUtf8Bytes(dataContent.Base64Data.Span), MimeType = dataContent.MediaType, }, DataContent dataContent => new EmbeddedResourceBlock { Resource = new BlobResourceContents { Blob = EncodingUtilities.GetUtf8Bytes(dataContent.Base64Data.Span), 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()) }], StructuredContent = resultContent.Result is JsonElement je ? je : null, }, _ => new TextContentBlock { Text = JsonSerializer.Serialize(content, options.GetTypeInfo(typeof(object))), } }; contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(options); return contentBlock; } private sealed class ToolAIFunctionDeclaration(Tool tool) : AIFunctionDeclaration { public override string Name => tool.Name; public override string Description => tool.Description ?? ""; public override IReadOnlyDictionary AdditionalProperties => field ??= tool.Meta is { } meta ? meta.ToDictionary(p => p.Key, p => (object?)p.Value) : []; public override JsonElement JsonSchema => tool.InputSchema; public override JsonElement? ReturnJsonSchema => tool.OutputSchema; public override object? GetService(Type serviceType, object? serviceKey = null) { Throw.IfNull(serviceType); return serviceKey is null && serviceType.IsInstanceOfType(tool) ? tool : base.GetService(serviceType, serviceKey); } } }