Skip to content

Commit c8a229a

Browse files
SergeyMenshykhwestey-m
authored andcommitted
.Net: MCP prompt sample (microsoft#11342)
### Motivation, Context and Description This PR adds sample showing how to power MCP prompt by SK prompt server side and consume this prompt client side. Contributes to: microsoft#11199 --------- Co-authored-by: westey <[email protected]>
1 parent 8c6db32 commit c8a229a

File tree

10 files changed

+409
-2
lines changed

10 files changed

+409
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using Microsoft.SemanticKernel;
5+
using Microsoft.SemanticKernel.ChatCompletion;
6+
using ModelContextProtocol.Protocol.Types;
7+
8+
namespace MCPClient;
9+
10+
/// <summary>
11+
/// Extension methods for <see cref="GetPromptResult"/>.
12+
/// </summary>
13+
internal static class PromptResultExtensions
14+
{
15+
/// <summary>
16+
/// Converts a <see cref="GetPromptResult"/> to a <see cref="ChatHistory"/>.
17+
/// </summary>
18+
/// <param name="result">The prompt result to convert.</param>
19+
/// <returns>The corresponding <see cref="ChatHistory"/>.</returns>
20+
public static ChatHistory ToChatHistory(this GetPromptResult result)
21+
{
22+
ChatHistory chatHistory = [];
23+
24+
foreach (PromptMessage message in result.Messages)
25+
{
26+
ChatMessageContentItemCollection items = [];
27+
28+
switch (message.Content.Type)
29+
{
30+
case "text":
31+
items.Add(new TextContent(message.Content.Text));
32+
break;
33+
case "image":
34+
items.Add(new ImageContent(Convert.FromBase64String(message.Content.Data!), message.Content.MimeType));
35+
break;
36+
default:
37+
throw new InvalidOperationException($"Unexpected message content type '{message.Content.Type}'");
38+
}
39+
40+
chatHistory.Add(new ChatMessageContent(message.Role.ToAuthorRole(), items));
41+
}
42+
43+
return chatHistory;
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using Microsoft.SemanticKernel.ChatCompletion;
5+
using ModelContextProtocol.Protocol.Types;
6+
7+
namespace MCPClient;
8+
9+
/// <summary>
10+
/// Extension methods for the <see cref="Role"/> enum.
11+
/// </summary>
12+
internal static class RoleExtensions
13+
{
14+
/// <summary>
15+
/// Converts a <see cref="Role"/> to a <see cref="AuthorRole"/>.
16+
/// </summary>
17+
/// <param name="role">The MCP role to convert.</param>
18+
/// <returns>The corresponding <see cref="AuthorRole"/>.</returns>
19+
public static AuthorRole ToAuthorRole(this Role role)
20+
{
21+
return role switch
22+
{
23+
Role.User => AuthorRole.User,
24+
Role.Assistant => AuthorRole.Assistant,
25+
_ => throw new InvalidOperationException($"Unexpected role '{role}'")
26+
};
27+
}
28+
}

dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/Program.cs

+96
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,41 @@
77
using System.Threading.Tasks;
88
using Microsoft.Extensions.Configuration;
99
using Microsoft.SemanticKernel;
10+
using Microsoft.SemanticKernel.ChatCompletion;
1011
using Microsoft.SemanticKernel.Connectors.OpenAI;
1112
using ModelContextProtocol;
1213
using ModelContextProtocol.Client;
1314
using ModelContextProtocol.Protocol.Transport;
15+
using ModelContextProtocol.Protocol.Types;
1416

1517
namespace MCPClient;
1618

1719
internal sealed class Program
1820
{
1921
public static async Task Main(string[] args)
2022
{
23+
// Use the MCP tools with the Semantic Kernel
24+
await UseMCPToolsWithSKAsync();
25+
26+
// Use the MCP tools and MCP prompt with the Semantic Kernel
27+
await UseMCPToolsAndPromptWithSKAsync();
28+
}
29+
30+
/// <summary>
31+
/// Demonstrates how to use the MCP tools with the Semantic Kernel.
32+
/// The code in this method:
33+
/// 1. Creates an MCP client.
34+
/// 2. Retrieves the list of tools provided by the MCP server.
35+
/// 3. Creates a kernel and registers the MCP tools as Kernel functions.
36+
/// 4. Sends the prompt to AI model together with the MCP tools represented as Kernel functions.
37+
/// 5. The AI model calls DateTimeUtils-GetCurrentDateTimeInUtc function to get the current date time in UTC required as an argument for the next function.
38+
/// 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.
39+
/// 7. Having received the weather information from the function call, the AI model returns the answer to the prompt.
40+
/// </summary>
41+
private static async Task UseMCPToolsWithSKAsync()
42+
{
43+
Console.WriteLine($"Running the {nameof(UseMCPToolsWithSKAsync)} sample.");
44+
2145
// Create an MCP client
2246
await using IMcpClient mcpClient = await CreateMcpClientAsync();
2347

@@ -43,10 +67,67 @@ public static async Task Main(string[] args)
4367
FunctionResult result = await kernel.InvokePromptAsync(prompt, new(executionSettings));
4468

4569
Console.WriteLine(result);
70+
Console.WriteLine();
4671

4772
// The expected output is: The likely color of the sky in Boston today is gray, as it is currently rainy.
4873
}
4974

75+
/// <summary>
76+
/// Demonstrates how to use the MCP tools and MCP prompt with the Semantic Kernel.
77+
/// The code in this method:
78+
/// 1. Creates an MCP client.
79+
/// 2. Retrieves the list of tools provided by the MCP server.
80+
/// 3. Retrieves the list of prompts provided by the MCP server.
81+
/// 4. Creates a kernel and registers the MCP tools as Kernel functions.
82+
/// 5. Requests the `GetCurrentWeatherForCity` prompt from the MCP server.
83+
/// 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.
84+
/// 7. Converts the MCP server prompt: list of messages where each message is represented by content and role to a chat history.
85+
/// 8. Sends the chat history to the AI model together with the MCP tools represented as Kernel functions.
86+
/// 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.
87+
/// 10. Having received the weather information from the function call, the AI model returns the answer to the prompt.
88+
/// </summary>
89+
private static async Task UseMCPToolsAndPromptWithSKAsync()
90+
{
91+
Console.WriteLine($"Running the {nameof(UseMCPToolsAndPromptWithSKAsync)} sample.");
92+
93+
// Create an MCP client
94+
await using IMcpClient mcpClient = await CreateMcpClientAsync();
95+
96+
// Retrieve and display the list provided by the MCP server
97+
IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
98+
DisplayTools(tools);
99+
100+
// Retrieve and display the list of prompts provided by the MCP server
101+
IList<McpClientPrompt> prompts = await mcpClient.ListPromptsAsync();
102+
DisplayPrompts(prompts);
103+
104+
// Create a kernel and register the MCP tools
105+
Kernel kernel = CreateKernelWithChatCompletionService();
106+
kernel.Plugins.AddFromFunctions("Tools", tools.Select(aiFunction => aiFunction.AsKernelFunction()));
107+
108+
// Enable automatic function calling
109+
OpenAIPromptExecutionSettings executionSettings = new()
110+
{
111+
Temperature = 0,
112+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
113+
};
114+
115+
// Retrieve the `GetCurrentWeatherForCity` prompt from the MCP server and convert it to a chat history
116+
GetPromptResult promptResult = await mcpClient.GetPromptAsync("GetCurrentWeatherForCity", new Dictionary<string, object?>() { ["city"] = "Boston" });
117+
118+
ChatHistory chatHistory = promptResult.ToChatHistory();
119+
120+
// Execute a prompt using the MCP tools and prompt
121+
IChatCompletionService chatCompletion = kernel.GetRequiredService<IChatCompletionService>();
122+
123+
ChatMessageContent result = await chatCompletion.GetChatMessageContentAsync(chatHistory, executionSettings, kernel);
124+
125+
Console.WriteLine(result);
126+
Console.WriteLine();
127+
128+
// The expected output is: The weather in Boston as of 2025-04-02 16:39:40 is 61°F and rainy.
129+
}
130+
50131
/// <summary>
51132
/// Creates an instance of <see cref="Kernel"/> with the OpenAI chat completion service registered.
52133
/// </summary>
@@ -129,5 +210,20 @@ private static void DisplayTools(IList<McpClientTool> tools)
129210
{
130211
Console.WriteLine($"- {tool.Name}: {tool.Description}");
131212
}
213+
Console.WriteLine();
214+
}
215+
216+
/// <summary>
217+
/// Displays the list of available MCP prompts.
218+
/// </summary>
219+
/// <param name="prompts">The list of the prompts to display.</param>
220+
private static void DisplayPrompts(IList<McpClientPrompt> prompts)
221+
{
222+
Console.WriteLine("Available MCP prompts:");
223+
foreach (var prompt in prompts)
224+
{
225+
Console.WriteLine($"- {prompt.Name}: {prompt.Description}");
226+
}
227+
Console.WriteLine();
132228
}
133229
}

dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/MCPServer.csproj

+9
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@
77
<NoWarn>$(NoWarn);VSTHRD111;CA2007;SKEXP0001</NoWarn>
88
</PropertyGroup>
99

10+
<ItemGroup>
11+
<Content Remove="Prompts\getCurrentWeatherForCity.json" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<EmbeddedResource Include="Prompts\getCurrentWeatherForCity.json" />
16+
</ItemGroup>
17+
1018
<ItemGroup>
1119
<PackageReference Include="Microsoft.Extensions.Hosting" />
1220
<PackageReference Include="ModelContextProtocol" />
1321
</ItemGroup>
1422

1523
<ItemGroup>
24+
<ProjectReference Include="..\..\..\..\src\Extensions\PromptTemplates.Handlebars\PromptTemplates.Handlebars.csproj" />
1625
<ProjectReference Include="..\..\..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
1726
<ProjectReference Include="..\..\..\..\src\SemanticKernel.Core\SemanticKernel.Core.csproj" />
1827
</ItemGroup>

dotnet/samples/Demos/ModelContextProtocolClientServer/MCPServer/Program.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using MCPServer;
4+
using MCPServer.Prompts;
45
using MCPServer.Tools;
56
using Microsoft.SemanticKernel;
67

@@ -12,10 +13,16 @@
1213
// Build the kernel
1314
Kernel kernel = kernelBuilder.Build();
1415

16+
// Register prompts
17+
PromptRegistry.RegisterPrompt(PromptDefinition.Create(EmbeddedResource.ReadAsString("getCurrentWeatherForCity.json"), kernel));
18+
1519
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1620
builder.Services
1721
.AddMcpServer()
1822
.WithStdioServerTransport()
1923
// Add all functions from the kernel plugins to the MCP server as tools
20-
.WithTools(kernel.Plugins);
24+
.WithTools(kernel.Plugins)
25+
// Register prompt handlers
26+
.WithListPromptsHandler(PromptRegistry.HandlerListPromptRequestsAsync)
27+
.WithGetPromptHandler(PromptRegistry.HandlerGetPromptRequestsAsync);
2128
await builder.Build().RunAsync();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Reflection;
4+
5+
namespace MCPServer.Prompts;
6+
7+
/// <summary>
8+
/// Reads embedded resources.
9+
/// </summary>
10+
public static class EmbeddedResource
11+
{
12+
private static readonly string? s_namespace = typeof(EmbeddedResource).Namespace;
13+
14+
internal static string ReadAsString(string fileName)
15+
{
16+
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
17+
Assembly assembly =
18+
typeof(EmbeddedResource).GetTypeInfo().Assembly ??
19+
throw new InvalidOperationException($"[{s_namespace}] {fileName} assembly not found");
20+
21+
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
22+
string resourceName = $"{s_namespace}.{fileName}";
23+
24+
Stream stream =
25+
assembly.GetManifestResourceStream(resourceName) ??
26+
throw new InvalidOperationException($"{resourceName} resource not found");
27+
28+
// Return the resource content, in text format.
29+
using StreamReader reader = new(stream);
30+
return reader.ReadToEnd();
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.SemanticKernel;
4+
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;
5+
using ModelContextProtocol.Protocol.Types;
6+
using ModelContextProtocol.Server;
7+
8+
namespace MCPServer.Prompts;
9+
10+
/// <summary>
11+
/// Represents a prompt definition.
12+
/// </summary>
13+
internal sealed class PromptDefinition
14+
{
15+
/// <summary>
16+
/// Gets or sets the prompt.
17+
/// </summary>
18+
public required Prompt Prompt { get; init; }
19+
20+
/// <summary>
21+
/// Gets or sets the handler for the prompt.
22+
/// </summary>
23+
public required Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>> Handler { get; init; }
24+
25+
/// <summary>
26+
/// Gets this prompt definition.
27+
/// </summary>
28+
/// <param name="jsonPrompt">The JSON prompt template.</param>
29+
/// <param name="kernel">An instance of the kernel to render the prompt.</param>
30+
/// <returns>The prompt definition.</returns>
31+
public static PromptDefinition Create(string jsonPrompt, Kernel kernel)
32+
{
33+
PromptTemplateConfig promptTemplateConfig = PromptTemplateConfig.FromJson(jsonPrompt);
34+
35+
return new PromptDefinition()
36+
{
37+
Prompt = GetPrompt(promptTemplateConfig),
38+
Handler = (context, cancellationToken) =>
39+
{
40+
IPromptTemplate promptTemplate = new HandlebarsPromptTemplateFactory().Create(promptTemplateConfig);
41+
42+
return GetPromptHandlerAsync(context, promptTemplateConfig, promptTemplate, kernel, cancellationToken);
43+
}
44+
};
45+
}
46+
47+
/// <summary>
48+
/// Creates an MCP prompt from SK prompt template.
49+
/// </summary>
50+
/// <param name="promptTemplateConfig">The prompt template configuration.</param>
51+
/// <returns>The MCP prompt.</returns>
52+
private static Prompt GetPrompt(PromptTemplateConfig promptTemplateConfig)
53+
{
54+
// Create the MCP prompt arguments
55+
List<PromptArgument>? arguments = null;
56+
57+
foreach (var inputVariable in promptTemplateConfig.InputVariables)
58+
{
59+
(arguments ??= []).Add(new()
60+
{
61+
Name = inputVariable.Name,
62+
Description = inputVariable.Description,
63+
Required = inputVariable.IsRequired
64+
});
65+
}
66+
67+
// Create the MCP prompt
68+
return new Prompt
69+
{
70+
Name = promptTemplateConfig.Name!,
71+
Description = promptTemplateConfig.Description,
72+
Arguments = arguments
73+
};
74+
}
75+
76+
/// <summary>
77+
/// Handles the prompt request by rendering the prompt.
78+
/// </summary>
79+
/// <param name="context">The MCP request context.</param>
80+
/// <param name="promptTemplateConfig">The prompt template configuration.</param>
81+
/// <param name="promptTemplate">The prompt template.</param>
82+
/// <param name="kernel">The kernel to render the prompt.</param>
83+
/// <param name="cancellationToken">The cancellation token.</param>
84+
/// <returns>The prompt.</returns>
85+
private static async Task<GetPromptResult> GetPromptHandlerAsync(RequestContext<GetPromptRequestParams> context, PromptTemplateConfig promptTemplateConfig, IPromptTemplate promptTemplate, Kernel kernel, CancellationToken cancellationToken)
86+
{
87+
// Render the prompt
88+
string renderedPrompt = await promptTemplate.RenderAsync(
89+
kernel: kernel,
90+
arguments: context.Params?.Arguments is { } args ? new KernelArguments(args!) : null,
91+
cancellationToken: cancellationToken);
92+
93+
// Create prompt result
94+
return new GetPromptResult()
95+
{
96+
Description = promptTemplateConfig.Description,
97+
Messages =
98+
[
99+
new PromptMessage()
100+
{
101+
Content = new Content()
102+
{
103+
Type = "text",
104+
Text = renderedPrompt
105+
},
106+
Role = Role.User
107+
}
108+
]
109+
};
110+
}
111+
}

0 commit comments

Comments
 (0)