Skip to content

Commit 072c476

Browse files
.Net: Samples for MCP Resources and Resource Templates (#11433)
### Motivation, Context and Description This PR adds two new MCP samples that demonstrate how to use SK to power MCP resources and resource templates on the server side, and how to use them with the SK client side. Additionally, it updates to the latest version of the ModelContextProtocol NuGet package.
1 parent 7b3c378 commit 072c476

16 files changed

+876
-53
lines changed

dotnet/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<PackageVersion Include="Grpc.AspNetCore.Server" Version="2.70.0" />
3535
<PackageVersion Include="Grpc.AspNetCore.Server.Reflection" Version="2.70.0" />
3636
<PackageVersion Include="Grpc.Tools" Version="2.70.0" />
37-
<PackageVersion Include="ModelContextProtocol" Version="0.1.0-preview.4" />
37+
<PackageVersion Include="ModelContextProtocol" Version="0.1.0-preview.6" />
3838
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.13" />
3939
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.14" />
4040
<PackageVersion Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="1.0.1" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using Microsoft.SemanticKernel;
6+
using Microsoft.SemanticKernel.ChatCompletion;
7+
using ModelContextProtocol.Protocol.Types;
8+
9+
namespace MCPClient;
10+
11+
/// <summary>
12+
/// Extension methods for <see cref="ReadResourceResult"/>.
13+
/// </summary>
14+
public static class ReadResourceResultExtensions
15+
{
16+
/// <summary>
17+
/// Converts a <see cref="ReadResourceResult"/> to a <see cref="ChatMessageContentItemCollection"/>.
18+
/// </summary>
19+
/// <param name="readResourceResult">The MCP read resource result to convert.</param>
20+
/// <returns>The corresponding <see cref="ChatMessageContentItemCollection"/>.</returns>
21+
public static ChatMessageContentItemCollection ToChatMessageContentItemCollection(this ReadResourceResult readResourceResult)
22+
{
23+
if (readResourceResult.Contents.Count == 0)
24+
{
25+
throw new InvalidOperationException("The resource does not contain any contents.");
26+
}
27+
28+
ChatMessageContentItemCollection result = [];
29+
30+
foreach (var resourceContent in readResourceResult.Contents)
31+
{
32+
Dictionary<string, object?> metadata = new()
33+
{
34+
["uri"] = resourceContent.Uri
35+
};
36+
37+
if (resourceContent is TextResourceContents textResourceContent)
38+
{
39+
result.Add(new TextContent()
40+
{
41+
Text = textResourceContent.Text,
42+
MimeType = textResourceContent.MimeType,
43+
Metadata = metadata,
44+
});
45+
}
46+
else if (resourceContent is BlobResourceContents blobResourceContent)
47+
{
48+
if (blobResourceContent.MimeType?.StartsWith("image", System.StringComparison.InvariantCulture) ?? false)
49+
{
50+
result.Add(new ImageContent()
51+
{
52+
Data = Convert.FromBase64String(blobResourceContent.Blob),
53+
MimeType = blobResourceContent.MimeType,
54+
Metadata = metadata,
55+
});
56+
}
57+
else if (blobResourceContent.MimeType?.StartsWith("audio", System.StringComparison.InvariantCulture) ?? false)
58+
{
59+
result.Add(new AudioContent
60+
{
61+
Data = Convert.FromBase64String(blobResourceContent.Blob),
62+
MimeType = blobResourceContent.MimeType,
63+
Metadata = metadata,
64+
});
65+
}
66+
else
67+
{
68+
result.Add(new BinaryContent
69+
{
70+
Data = Convert.FromBase64String(blobResourceContent.Blob),
71+
MimeType = blobResourceContent.MimeType,
72+
Metadata = metadata,
73+
});
74+
}
75+
}
76+
}
77+
78+
return result;
79+
}
80+
}

dotnet/samples/Demos/ModelContextProtocolClientServer/MCPClient/MCPClient.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</ItemGroup>
1919

2020
<ItemGroup>
21-
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj" />
21+
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
2222
<ProjectReference Include="..\..\..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
2323
<ProjectReference Include="..\..\..\..\src\SemanticKernel.Core\SemanticKernel.Core.csproj" />
2424
</ItemGroup>

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

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,113 @@ internal sealed class Program
2020
{
2121
public static async Task Main(string[] args)
2222
{
23-
// Use the MCP tools with the Semantic Kernel
24-
await UseMCPToolsWithSKAsync();
23+
await UseMCPToolsAsync();
2524

26-
// Use the MCP tools and MCP prompt with the Semantic Kernel
27-
await UseMCPToolsAndPromptWithSKAsync();
25+
await UseMCPToolsAndPromptAsync();
26+
27+
await UseMCPResourcesAsync();
28+
29+
await UseMCPResourceTemplatesAsync();
30+
}
31+
32+
/// <summary>
33+
/// Demonstrates how to use the MCP resources with the Semantic Kernel.
34+
/// The code in this method:
35+
/// 1. Creates an MCP client.
36+
/// 2. Retrieves the list of resources provided by the MCP server.
37+
/// 3. Retrieves the `image://cat.jpg` resource content from the MCP server.
38+
/// 4. Adds the image to the chat history and prompts the AI model to describe the content of the image.
39+
/// </summary>
40+
private static async Task UseMCPResourcesAsync()
41+
{
42+
Console.WriteLine($"Running the {nameof(UseMCPResourcesAsync)} sample.");
43+
44+
// Create an MCP client
45+
await using IMcpClient mcpClient = await CreateMcpClientAsync();
46+
47+
// Retrieve list of resources provided by the MCP server and display them
48+
IList<Resource> resources = await mcpClient.ListResourcesAsync();
49+
DisplayResources(resources);
50+
51+
// Create a kernel
52+
Kernel kernel = CreateKernelWithChatCompletionService();
53+
54+
// Enable automatic function calling
55+
OpenAIPromptExecutionSettings executionSettings = new()
56+
{
57+
Temperature = 0,
58+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
59+
};
60+
61+
// Retrieve the `image://cat.jpg` resource from the MCP server
62+
ReadResourceResult resource = await mcpClient.ReadResourceAsync("image://cat.jpg");
63+
64+
// Add the resource to the chat history and prompt the AI model to describe the content of the image
65+
ChatHistory chatHistory = [];
66+
chatHistory.AddUserMessage(resource.ToChatMessageContentItemCollection());
67+
chatHistory.AddUserMessage("Describe the content of the image?");
68+
69+
// Execute a prompt using the MCP resource and prompt added to the chat history
70+
IChatCompletionService chatCompletion = kernel.GetRequiredService<IChatCompletionService>();
71+
72+
ChatMessageContent result = await chatCompletion.GetChatMessageContentAsync(chatHistory, executionSettings, kernel);
73+
74+
Console.WriteLine(result);
75+
Console.WriteLine();
76+
77+
// The expected output is: The image features a fluffy cat sitting in a lush, colorful garden.
78+
// The garden is filled with various flowers and plants, creating a vibrant and serene atmosphere...
79+
}
80+
81+
/// <summary>
82+
/// Demonstrates how to use the MCP resource templates with the Semantic Kernel.
83+
/// The code in this method:
84+
/// 1. Creates an MCP client.
85+
/// 2. Retrieves the list of resource templates provided by the MCP server.
86+
/// 3. Reads relevant to the prompt records from the `vectorStore://records/{prompt}` MCP resource template.
87+
/// 4. Adds the records to the chat history and prompts the AI model to explain what SK is.
88+
/// </summary>
89+
private static async Task UseMCPResourceTemplatesAsync()
90+
{
91+
Console.WriteLine($"Running the {nameof(UseMCPResourceTemplatesAsync)} sample.");
92+
93+
// Create an MCP client
94+
await using IMcpClient mcpClient = await CreateMcpClientAsync();
95+
96+
// Retrieve list of resource templates provided by the MCP server and display them
97+
IList<ResourceTemplate> resourceTemplates = await mcpClient.ListResourceTemplatesAsync();
98+
DisplayResourceTemplates(resourceTemplates);
99+
100+
// Create a kernel
101+
Kernel kernel = CreateKernelWithChatCompletionService();
102+
103+
// Enable automatic function calling
104+
OpenAIPromptExecutionSettings executionSettings = new()
105+
{
106+
Temperature = 0,
107+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
108+
};
109+
110+
string prompt = "What is the Semantic Kernel?";
111+
112+
// Retrieve relevant to the prompt records via MCP resource template
113+
ReadResourceResult resource = await mcpClient.ReadResourceAsync($"vectorStore://records/{prompt}");
114+
115+
// Add the resource content/records to the chat history and prompt the AI model to explain what SK is
116+
ChatHistory chatHistory = [];
117+
chatHistory.AddUserMessage(resource.ToChatMessageContentItemCollection());
118+
chatHistory.AddUserMessage(prompt);
119+
120+
// Execute a prompt using the MCP resource and prompt added to the chat history
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 Semantic Kernel (SK) is a lightweight software development kit (SDK) designed for use in .NET applications.
129+
// It acts as an orchestrator that facilitates interaction between AI models and available plugins, enabling them to work together to produce desired outputs.
28130
}
29131

30132
/// <summary>
@@ -38,9 +140,9 @@ public static async Task Main(string[] args)
38140
/// 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.
39141
/// 7. Having received the weather information from the function call, the AI model returns the answer to the prompt.
40142
/// </summary>
41-
private static async Task UseMCPToolsWithSKAsync()
143+
private static async Task UseMCPToolsAsync()
42144
{
43-
Console.WriteLine($"Running the {nameof(UseMCPToolsWithSKAsync)} sample.");
145+
Console.WriteLine($"Running the {nameof(UseMCPToolsAsync)} sample.");
44146

45147
// Create an MCP client
46148
await using IMcpClient mcpClient = await CreateMcpClientAsync();
@@ -86,9 +188,9 @@ private static async Task UseMCPToolsWithSKAsync()
86188
/// 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.
87189
/// 10. Having received the weather information from the function call, the AI model returns the answer to the prompt.
88190
/// </summary>
89-
private static async Task UseMCPToolsAndPromptWithSKAsync()
191+
private static async Task UseMCPToolsAndPromptAsync()
90192
{
91-
Console.WriteLine($"Running the {nameof(UseMCPToolsAndPromptWithSKAsync)} sample.");
193+
Console.WriteLine($"Running the {nameof(UseMCPToolsAndPromptAsync)} sample.");
92194

93195
// Create an MCP client
94196
await using IMcpClient mcpClient = await CreateMcpClientAsync();
@@ -208,7 +310,7 @@ private static void DisplayTools(IList<McpClientTool> tools)
208310
Console.WriteLine("Available MCP tools:");
209311
foreach (var tool in tools)
210312
{
211-
Console.WriteLine($"- {tool.Name}: {tool.Description}");
313+
Console.WriteLine($"- Name: {tool.Name}, Description: {tool.Description}");
212314
}
213315
Console.WriteLine();
214316
}
@@ -222,7 +324,27 @@ private static void DisplayPrompts(IList<McpClientPrompt> prompts)
222324
Console.WriteLine("Available MCP prompts:");
223325
foreach (var prompt in prompts)
224326
{
225-
Console.WriteLine($"- {prompt.Name}: {prompt.Description}");
327+
Console.WriteLine($"- Name: {prompt.Name}, Description: {prompt.Description}");
328+
}
329+
Console.WriteLine();
330+
}
331+
332+
private static void DisplayResources(IList<Resource> resources)
333+
{
334+
Console.WriteLine("Available MCP resources:");
335+
foreach (var resource in resources)
336+
{
337+
Console.WriteLine($"- Name: {resource.Name}, Uri: {resource.Uri}, Description: {resource.Description}");
338+
}
339+
Console.WriteLine();
340+
}
341+
342+
private static void DisplayResourceTemplates(IList<ResourceTemplate> resourceTemplates)
343+
{
344+
Console.WriteLine("Available MCP resource templates:");
345+
foreach (var template in resourceTemplates)
346+
{
347+
Console.WriteLine($"- Name: {template.Name}, Description: {template.Description}");
226348
}
227349
Console.WriteLine();
228350
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Reflection;
4+
5+
namespace MCPServer;
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+
/// <summary>
15+
/// Read an embedded resource as a string.
16+
/// </summary>
17+
/// <param name="resourcePath">The path to the resource, relative to the assembly namespace.</param>
18+
/// <returns>A string containing the resource content.</returns>
19+
public static string ReadAsString(string resourcePath)
20+
{
21+
Stream stream = ReadAsStream(resourcePath);
22+
23+
using StreamReader reader = new(stream);
24+
return reader.ReadToEnd();
25+
}
26+
27+
/// <summary>
28+
/// Read an embedded resource as a byte array.
29+
/// </summary>
30+
/// <param name="resourcePath">The path to the resource, relative to the assembly namespace.</param>
31+
/// <returns>A byte array containing the resource content.</returns>
32+
public static byte[] ReadAsBytes(string resourcePath)
33+
{
34+
Stream stream = ReadAsStream(resourcePath);
35+
36+
using MemoryStream memoryStream = new();
37+
stream.CopyTo(memoryStream);
38+
return memoryStream.ToArray();
39+
}
40+
41+
/// <summary>
42+
/// Read an embedded resource as a stream.
43+
/// </summary>
44+
/// <param name="resourcePath">The path to the resource, relative to the assembly namespace.</param>
45+
/// <returns>A stream containing the resource content.</returns>
46+
public static Stream ReadAsStream(string resourcePath)
47+
{
48+
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
49+
Assembly assembly =
50+
typeof(EmbeddedResource).GetTypeInfo().Assembly ??
51+
throw new InvalidOperationException($"[{s_namespace}] {resourcePath} assembly not found");
52+
53+
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
54+
string resourceName = $"{s_namespace}.{resourcePath}";
55+
56+
return
57+
assembly.GetManifestResourceStream(resourceName) ??
58+
throw new InvalidOperationException($"{resourceName} resource not found");
59+
}
60+
}

0 commit comments

Comments
 (0)