diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Extensions/PromptResultExtensions.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Extensions/PromptResultExtensions.cs new file mode 100644 index 000000000000..0843331a2fc9 --- /dev/null +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Extensions/PromptResultExtensions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using ModelContextProtocol.Protocol.Types; + +namespace MCPClient; + +/// +/// Extension methods for . +/// +internal static class PromptResultExtensions +{ + /// + /// Converts a to a . + /// + /// The prompt result to convert. + /// The corresponding . + public static ChatHistory ToChatHistory(this GetPromptResult result) + { + ChatHistory chatHistory = []; + + foreach (PromptMessage message in result.Messages) + { + ChatMessageContentItemCollection items = []; + + switch (message.Content.Type) + { + case "text": + items.Add(new TextContent(message.Content.Text)); + break; + case "image": + items.Add(new ImageContent(Convert.FromBase64String(message.Content.Data!), message.Content.MimeType)); + break; + default: + throw new InvalidOperationException($"Unexpected message content type '{message.Content.Type}'"); + } + + chatHistory.Add(new ChatMessageContent(message.Role.ToAuthorRole(), items)); + } + + return chatHistory; + } +} diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Extensions/RoleExtensions.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Extensions/RoleExtensions.cs new file mode 100644 index 000000000000..f653ab1a991d --- /dev/null +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Extensions/RoleExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.ChatCompletion; +using ModelContextProtocol.Protocol.Types; + +namespace MCPClient; + +/// +/// Extension methods for the enum. +/// +internal static class RoleExtensions +{ + /// + /// Converts a to a . + /// + /// The MCP role to convert. + /// The corresponding . + public static AuthorRole ToAuthorRole(this Role role) + { + return role switch + { + Role.User => AuthorRole.User, + Role.Assistant => AuthorRole.Assistant, + _ => throw new InvalidOperationException($"Unexpected role '{role}'") + }; + } +} diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Program.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Program.cs index 41d50c1674a6..333246575c44 100644 --- a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Program.cs +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Program.cs @@ -7,10 +7,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using ModelContextProtocol; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; +using ModelContextProtocol.Protocol.Types; namespace MCPClient; @@ -18,6 +20,28 @@ internal sealed class Program { public static async Task Main(string[] args) { + // Use the MCP tools with the Semantic Kernel + await UseMCPToolsWithSKAsync(); + + // Use the MCP tools and MCP prompt with the Semantic Kernel + await UseMCPToolsAndPromptWithSKAsync(); + } + + /// + /// Demonstrates how to use the MCP tools with the Semantic Kernel. + /// The code in this method: + /// 1. Creates an MCP client. + /// 2. Retrieves the list of tools provided by the MCP server. + /// 3. Creates a kernel and registers the MCP tools as Kernel functions. + /// 4. Sends the prompt to AI model together with the MCP tools represented as Kernel functions. + /// 5. The AI model calls DateTimeUtils-GetCurrentDateTimeInUtc function to get the current date time in UTC required as an argument for the next function. + /// 6. The AI model calls WeatherUtils-GetWeatherForCity function with the current date time and the `Boston` arguments extracted from the prompt to get the weather information. + /// 7. Having received the weather information from the function call, the AI model returns the answer to the prompt. + /// + private static async Task UseMCPToolsWithSKAsync() + { + Console.WriteLine($"Running the {nameof(UseMCPToolsWithSKAsync)} sample."); + // Create an MCP client await using IMcpClient mcpClient = await CreateMcpClientAsync(); @@ -43,10 +67,67 @@ public static async Task Main(string[] args) FunctionResult result = await kernel.InvokePromptAsync(prompt, new(executionSettings)); Console.WriteLine(result); + Console.WriteLine(); // The expected output is: The likely color of the sky in Boston today is gray, as it is currently rainy. } + /// + /// Demonstrates how to use the MCP tools and MCP prompt with the Semantic Kernel. + /// The code in this method: + /// 1. Creates an MCP client. + /// 2. Retrieves the list of tools provided by the MCP server. + /// 3. Retrieves the list of prompts provided by the MCP server. + /// 4. Creates a kernel and registers the MCP tools as Kernel functions. + /// 5. Requests the `GetCurrentWeatherForCity` prompt from the MCP server. + /// 6. The MCP server renders the prompt using the `Boston` as value for the `city` parameter and the result of the `DateTimeUtils-GetCurrentDateTimeInUtc` server-side invocation added to the prompt as part of prompt rendering. + /// 7. Converts the MCP server prompt: list of messages where each message is represented by content and role to a chat history. + /// 8. Sends the chat history to the AI model together with the MCP tools represented as Kernel functions. + /// 9. The AI model calls WeatherUtils-GetWeatherForCity function with the current date time and the `Boston` arguments extracted from the prompt to get the weather information. + /// 10. Having received the weather information from the function call, the AI model returns the answer to the prompt. + /// + private static async Task UseMCPToolsAndPromptWithSKAsync() + { + Console.WriteLine($"Running the {nameof(UseMCPToolsAndPromptWithSKAsync)} sample."); + + // Create an MCP client + await using IMcpClient mcpClient = await CreateMcpClientAsync(); + + // Retrieve and display the list provided by the MCP server + IList tools = await mcpClient.ListToolsAsync(); + DisplayTools(tools); + + // Retrieve and display the list of prompts provided by the MCP server + IList prompts = await mcpClient.ListPromptsAsync(); + DisplayPrompts(prompts); + + // Create a kernel and register the MCP tools + Kernel kernel = CreateKernelWithChatCompletionService(); + kernel.Plugins.AddFromFunctions("Tools", tools.Select(aiFunction => aiFunction.AsKernelFunction())); + + // Enable automatic function calling + OpenAIPromptExecutionSettings executionSettings = new() + { + Temperature = 0, + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true }) + }; + + // Retrieve the `GetCurrentWeatherForCity` prompt from the MCP server and convert it to a chat history + GetPromptResult promptResult = await mcpClient.GetPromptAsync("GetCurrentWeatherForCity", new Dictionary() { ["city"] = "Boston" }); + + ChatHistory chatHistory = promptResult.ToChatHistory(); + + // Execute a prompt using the MCP tools and prompt + IChatCompletionService chatCompletion = kernel.GetRequiredService(); + + ChatMessageContent result = await chatCompletion.GetChatMessageContentAsync(chatHistory, executionSettings, kernel); + + Console.WriteLine(result); + Console.WriteLine(); + + // The expected output is: The weather in Boston as of 2025-04-02 16:39:40 is 61°F and rainy. + } + /// /// Creates an instance of with the OpenAI chat completion service registered. /// @@ -129,5 +210,20 @@ private static void DisplayTools(IList tools) { Console.WriteLine($"- {tool.Name}: {tool.Description}"); } + Console.WriteLine(); + } + + /// + /// Displays the list of available MCP prompts. + /// + /// The list of the prompts to display. + private static void DisplayPrompts(IList prompts) + { + Console.WriteLine("Available MCP prompts:"); + foreach (var prompt in prompts) + { + Console.WriteLine($"- {prompt.Name}: {prompt.Description}"); + } + Console.WriteLine(); } } diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/MCPServer.csproj b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/MCPServer.csproj index 628827a0e11a..7f16c2ffb74b 100644 --- a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/MCPServer.csproj +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/MCPServer.csproj @@ -7,12 +7,21 @@ $(NoWarn);VSTHRD111;CA2007;SKEXP0001 + + + + + + + + + diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Program.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Program.cs index 79aa59a5a180..abd3a079d1e5 100644 --- a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Program.cs +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using MCPServer; +using MCPServer.Prompts; using MCPServer.Tools; using Microsoft.SemanticKernel; @@ -12,10 +13,16 @@ // Build the kernel Kernel kernel = kernelBuilder.Build(); +// Register prompts +PromptRegistry.RegisterPrompt(PromptDefinition.Create(EmbeddedResource.ReadAsString("getCurrentWeatherForCity.json"), kernel)); + var builder = Host.CreateEmptyApplicationBuilder(settings: null); builder.Services .AddMcpServer() .WithStdioServerTransport() // Add all functions from the kernel plugins to the MCP server as tools - .WithTools(kernel.Plugins); + .WithTools(kernel.Plugins) + // Register prompt handlers + .WithListPromptsHandler(PromptRegistry.HandlerListPromptRequestsAsync) + .WithGetPromptHandler(PromptRegistry.HandlerGetPromptRequestsAsync); await builder.Build().RunAsync(); diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/EmbeddedResource.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/EmbeddedResource.cs new file mode 100644 index 000000000000..e694af06529a --- /dev/null +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/EmbeddedResource.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Reflection; + +namespace MCPServer.Prompts; + +/// +/// Reads embedded resources. +/// +public static class EmbeddedResource +{ + private static readonly string? s_namespace = typeof(EmbeddedResource).Namespace; + + internal static string ReadAsString(string fileName) + { + // Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored. + Assembly assembly = + typeof(EmbeddedResource).GetTypeInfo().Assembly ?? + throw new InvalidOperationException($"[{s_namespace}] {fileName} assembly not found"); + + // Resources are mapped like types, using the namespace and appending "." (dot) and the file name + string resourceName = $"{s_namespace}.{fileName}"; + + Stream stream = + assembly.GetManifestResourceStream(resourceName) ?? + throw new InvalidOperationException($"{resourceName} resource not found"); + + // Return the resource content, in text format. + using StreamReader reader = new(stream); + return reader.ReadToEnd(); + } +} diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/PromptDefinition.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/PromptDefinition.cs new file mode 100644 index 000000000000..f5a5e0d7959e --- /dev/null +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/PromptDefinition.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.PromptTemplates.Handlebars; +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; + +namespace MCPServer.Prompts; + +/// +/// Represents a prompt definition. +/// +internal sealed class PromptDefinition +{ + /// + /// Gets or sets the prompt. + /// + public required Prompt Prompt { get; init; } + + /// + /// Gets or sets the handler for the prompt. + /// + public required Func, CancellationToken, Task> Handler { get; init; } + + /// + /// Gets this prompt definition. + /// + /// The JSON prompt template. + /// An instance of the kernel to render the prompt. + /// The prompt definition. + public static PromptDefinition Create(string jsonPrompt, Kernel kernel) + { + PromptTemplateConfig promptTemplateConfig = PromptTemplateConfig.FromJson(jsonPrompt); + + return new PromptDefinition() + { + Prompt = GetPrompt(promptTemplateConfig), + Handler = (context, cancellationToken) => + { + IPromptTemplate promptTemplate = new HandlebarsPromptTemplateFactory().Create(promptTemplateConfig); + + return GetPromptHandlerAsync(context, promptTemplateConfig, promptTemplate, kernel, cancellationToken); + } + }; + } + + /// + /// Creates an MCP prompt from SK prompt template. + /// + /// The prompt template configuration. + /// The MCP prompt. + private static Prompt GetPrompt(PromptTemplateConfig promptTemplateConfig) + { + // Create the MCP prompt arguments + List? arguments = null; + + foreach (var inputVariable in promptTemplateConfig.InputVariables) + { + (arguments ??= []).Add(new() + { + Name = inputVariable.Name, + Description = inputVariable.Description, + Required = inputVariable.IsRequired + }); + } + + // Create the MCP prompt + return new Prompt + { + Name = promptTemplateConfig.Name!, + Description = promptTemplateConfig.Description, + Arguments = arguments + }; + } + + /// + /// Handles the prompt request by rendering the prompt. + /// + /// The MCP request context. + /// The prompt template configuration. + /// The prompt template. + /// The kernel to render the prompt. + /// The cancellation token. + /// The prompt. + private static async Task GetPromptHandlerAsync(RequestContext context, PromptTemplateConfig promptTemplateConfig, IPromptTemplate promptTemplate, Kernel kernel, CancellationToken cancellationToken) + { + // Render the prompt + string renderedPrompt = await promptTemplate.RenderAsync( + kernel: kernel, + arguments: context.Params?.Arguments is { } args ? new KernelArguments(args!) : null, + cancellationToken: cancellationToken); + + // Create prompt result + return new GetPromptResult() + { + Description = promptTemplateConfig.Description, + Messages = + [ + new PromptMessage() + { + Content = new Content() + { + Type = "text", + Text = renderedPrompt + }, + Role = Role.User + } + ] + }; + } +} diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/PromptRegistry.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/PromptRegistry.cs new file mode 100644 index 000000000000..062983dab3ba --- /dev/null +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/PromptRegistry.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; + +namespace MCPServer.Prompts; + +/// +/// Represents the prompt registry that contains the prompt definitions and provides the handlers for the prompt `List` and `Get` requests. +/// +internal static class PromptRegistry +{ + private static readonly Dictionary s_definitions = new(); + + /// + /// Registers a prompt definition. + /// + /// The prompt definition to register. + public static void RegisterPrompt(PromptDefinition definition) + { + if (s_definitions.ContainsKey(definition.Prompt.Name)) + { + throw new ArgumentException($"A prompt with the name '{definition.Prompt.Name}' is already registered."); + } + + s_definitions[definition.Prompt.Name] = definition; + } + + /// + /// Handles the `Get` prompt requests. + /// + /// The request context. + /// The cancellation token. + /// The result of the `Get` prompt request. + public static async Task HandlerGetPromptRequestsAsync(RequestContext context, CancellationToken cancellationToken) + { + // Make sure the prompt name is provided + if (context.Params?.Name is not string { } promptName || string.IsNullOrEmpty(promptName)) + { + throw new ArgumentException("Prompt name is required."); + } + + // Look up the prompt handler + if (!s_definitions.TryGetValue(promptName, out PromptDefinition? definition)) + { + throw new ArgumentException($"No handler found for the prompt '{promptName}'."); + } + + // Invoke the handler + return await definition.Handler(context, cancellationToken); + } + + /// + /// Handles the `List` prompt requests. + /// + /// Context of the request. + /// The cancellation token. + /// The result of the `List` prompt request. + public static Task HandlerListPromptRequestsAsync(RequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(new ListPromptsResult + { + Prompts = [.. s_definitions.Values.Select(d => d.Prompt)] + }); + } +} diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/getCurrentWeatherForCity.json b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/getCurrentWeatherForCity.json new file mode 100644 index 000000000000..56ec934468d9 --- /dev/null +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Prompts/getCurrentWeatherForCity.json @@ -0,0 +1,13 @@ +{ + "name": "GetCurrentWeatherForCity", + "description": "Provides current weather information for a specified city.", + "template_format": "handlebars", + "template": "What is the weather in {{city}} as of {{DateTimeUtils-GetCurrentDateTimeInUtc}}?", + "input_variables": [ + { + "name": "city", + "description": "The city for which to get the weather.", + "is_required": true + } + ] +} \ No newline at end of file diff --git a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Tools/DateTimeUtils.cs b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Tools/DateTimeUtils.cs index 80fe8affa525..8b74bb6b491f 100644 --- a/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Tools/DateTimeUtils.cs +++ b/dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Tools/DateTimeUtils.cs @@ -17,6 +17,6 @@ internal sealed class DateTimeUtils [KernelFunction, Description("Retrieves the current date time in UTC.")] public static string GetCurrentDateTimeInUtc() { - return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + return DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); } }