forked from microsoft/semantic-kernel
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
.Net: Demo showing how to integrate MCP tools with Semantic Kernel (m…
…icrosoft#10779) ### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
- Loading branch information
1 parent
7c8dccc
commit f8ee3ac
Showing
7 changed files
with
319 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
dotnet/samples/Demos/ModelContextProtocol/McpDotNetExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using McpDotNet.Client; | ||
using McpDotNet.Configuration; | ||
using McpDotNet.Protocol.Types; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
using Microsoft.SemanticKernel; | ||
|
||
namespace ModelContextProtocol; | ||
|
||
/// <summary> | ||
/// Extension methods for McpDotNet | ||
/// </summary> | ||
internal static class McpDotNetExtensions | ||
{ | ||
/// <summary> | ||
/// Retrieve an <see cref="IMcpClient"/> instance configured to connect to a GitHub server running on stdio. | ||
/// </summary> | ||
internal static async Task<IMcpClient> GetGitHubToolsAsync() | ||
{ | ||
McpClientOptions options = new() | ||
{ | ||
ClientInfo = new() { Name = "GitHub", Version = "1.0.0" } | ||
}; | ||
|
||
var config = new McpServerConfig | ||
{ | ||
Id = "github", | ||
Name = "GitHub", | ||
TransportType = "stdio", | ||
TransportOptions = new Dictionary<string, string> | ||
{ | ||
["command"] = "npx", | ||
["arguments"] = "-y @modelcontextprotocol/server-github", | ||
} | ||
}; | ||
|
||
var factory = new McpClientFactory( | ||
[config], | ||
options, | ||
NullLoggerFactory.Instance | ||
); | ||
|
||
return await factory.GetClientAsync(config.Id).ConfigureAwait(false); | ||
} | ||
|
||
/// <summary> | ||
/// Map the tools exposed on this <see cref="IMcpClient"/> to a collection of <see cref="KernelFunction"/> instances for use with the Semantic Kernel. | ||
/// </summary> | ||
internal static async Task<IEnumerable<KernelFunction>> MapToFunctionsAsync(this IMcpClient mcpClient) | ||
{ | ||
var tools = await mcpClient.ListToolsAsync().ConfigureAwait(false); | ||
return tools.Tools.Select(t => t.ToKernelFunction(mcpClient)).ToList(); | ||
} | ||
|
||
#region private | ||
private static KernelFunction ToKernelFunction(this Tool tool, IMcpClient mcpClient) | ||
{ | ||
async Task<string> InvokeToolAsync(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
// Convert arguments to dictionary format expected by mcpdotnet | ||
Dictionary<string, object> mcpArguments = []; | ||
foreach (var arg in arguments) | ||
{ | ||
if (arg.Value is not null) | ||
{ | ||
mcpArguments[arg.Key] = function.ToArgumentValue(arg.Key, arg.Value); | ||
} | ||
} | ||
|
||
// Call the tool through mcpdotnet | ||
var result = await mcpClient.CallToolAsync( | ||
tool.Name, | ||
mcpArguments, | ||
cancellationToken: cancellationToken | ||
).ConfigureAwait(false); | ||
|
||
// Extract the text content from the result | ||
return string.Join("\n", result.Content | ||
.Where(c => c.Type == "text") | ||
.Select(c => c.Text)); | ||
} | ||
catch (Exception ex) | ||
{ | ||
Console.Error.WriteLine($"Error invoking tool '{tool.Name}': {ex.Message}"); | ||
|
||
// Rethrowing to allow the kernel to handle the exception | ||
throw; | ||
} | ||
} | ||
|
||
return KernelFunctionFactory.CreateFromMethod( | ||
method: InvokeToolAsync, | ||
functionName: tool.Name, | ||
description: tool.Description, | ||
parameters: tool.ToParameters(), | ||
returnParameter: ToReturnParameter() | ||
); | ||
} | ||
|
||
private static object ToArgumentValue(this KernelFunction function, string name, object value) | ||
{ | ||
var parameter = function.Metadata.Parameters.FirstOrDefault(p => p.Name == name); | ||
return parameter?.ParameterType switch | ||
{ | ||
Type t when Nullable.GetUnderlyingType(t) == typeof(int) => Convert.ToInt32(value), | ||
Type t when Nullable.GetUnderlyingType(t) == typeof(double) => Convert.ToDouble(value), | ||
Type t when Nullable.GetUnderlyingType(t) == typeof(bool) => Convert.ToBoolean(value), | ||
Type t when t == typeof(List<string>) => (value as IEnumerable<object>)?.ToList(), | ||
Type t when t == typeof(Dictionary<string, object>) => (value as Dictionary<string, object>)?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), | ||
_ => value, | ||
} ?? value; | ||
} | ||
|
||
private static List<KernelParameterMetadata>? ToParameters(this Tool tool) | ||
{ | ||
var inputSchema = tool.InputSchema; | ||
var properties = inputSchema?.Properties; | ||
if (properties == null) | ||
{ | ||
return null; | ||
} | ||
|
||
HashSet<string> requiredProperties = new(inputSchema!.Required ?? []); | ||
return properties.Select(kvp => | ||
new KernelParameterMetadata(kvp.Key) | ||
{ | ||
Description = kvp.Value.Description, | ||
ParameterType = ConvertParameterDataType(kvp.Value, requiredProperties.Contains(kvp.Key)), | ||
IsRequired = requiredProperties.Contains(kvp.Key) | ||
}).ToList(); | ||
} | ||
|
||
private static KernelReturnParameterMetadata? ToReturnParameter() | ||
{ | ||
return new KernelReturnParameterMetadata() | ||
{ | ||
ParameterType = typeof(string), | ||
}; | ||
} | ||
private static Type ConvertParameterDataType(JsonSchemaProperty property, bool required) | ||
{ | ||
var type = property.Type switch | ||
{ | ||
"string" => typeof(string), | ||
"integer" => typeof(int), | ||
"number" => typeof(double), | ||
"boolean" => typeof(bool), | ||
"array" => typeof(List<string>), | ||
"object" => typeof(Dictionary<string, object>), | ||
_ => typeof(object) | ||
}; | ||
|
||
return !required && type.IsValueType ? typeof(Nullable<>).MakeGenericType(type) : type; | ||
} | ||
#endregion | ||
} |
33 changes: 33 additions & 0 deletions
33
dotnet/samples/Demos/ModelContextProtocol/ModelContextProtocol.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId> | ||
<NoWarn>$(NoWarn);CA2249;CS0612</NoWarn> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="mcpdotnet" /> | ||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" /> | ||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging.Console" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<None Update="SimpleToolsConsole.json"> | ||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> | ||
</None> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\..\src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj" /> | ||
<ProjectReference Include="..\..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" /> | ||
<ProjectReference Include="..\..\..\src\SemanticKernel.Core\SemanticKernel.Core.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.Connectors.OpenAI; | ||
using ModelContextProtocol; | ||
|
||
var config = new ConfigurationBuilder() | ||
.AddUserSecrets<Program>() | ||
.AddEnvironmentVariables() | ||
.Build(); | ||
|
||
// Prepare and build kernel | ||
var builder = Kernel.CreateBuilder(); | ||
builder.Services.AddLogging(c => c.AddDebug().SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace)); | ||
|
||
if (config["OpenAI:ApiKey"] is not null) | ||
{ | ||
builder.Services.AddOpenAIChatCompletion( | ||
serviceId: "openai", | ||
modelId: config["OpenAI:ChatModelId"] ?? "gpt-4o", | ||
apiKey: config["OpenAI:ApiKey"]!); | ||
} | ||
else | ||
{ | ||
Console.Error.WriteLine("Please provide a valid OpenAI:ApiKey to run this sample. See the associated README.md for more details."); | ||
return; | ||
} | ||
|
||
Kernel kernel = builder.Build(); | ||
|
||
// Add the MCP simple tools as Kernel functions | ||
var mcpClient = await McpDotNetExtensions.GetGitHubToolsAsync().ConfigureAwait(false); | ||
var functions = await mcpClient.MapToFunctionsAsync().ConfigureAwait(false); | ||
|
||
foreach (var function in functions) | ||
{ | ||
Console.WriteLine($"{function.Name}: {function.Description}"); | ||
} | ||
|
||
kernel.Plugins.AddFromFunctions("GitHub", functions); | ||
|
||
// Enable automatic function calling | ||
var executionSettings = new OpenAIPromptExecutionSettings | ||
{ | ||
Temperature = 0, | ||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() | ||
}; | ||
|
||
// Test using GitHub tools | ||
var prompt = "Summarize the last four commits to the microsoft/semantic-kernel repository?"; | ||
var result = await kernel.InvokePromptAsync(prompt, new(executionSettings)).ConfigureAwait(false); | ||
Console.WriteLine($"\n\n{prompt}\n{result}"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# Model Context Protocol Sample | ||
|
||
This example demonstrates how to use Model Context Protocol tools with Semantic Kernel. | ||
|
||
MCP is an open protocol that standardizes how applications provide context to LLMs. | ||
|
||
For for information on Model Context Protocol (MCP) please refer to the [documentation](https://modelcontextprotocol.io/introduction). | ||
|
||
This sample uses [mcpdotnet](https://www.nuget.org/packages/mcpdotnet) was heavily influenced by the [samples](https://github.com/PederHP/mcpdotnet/tree/main/samples) from that repository. | ||
|
||
The sample shows: | ||
|
||
1. How to connect to an MCP Server using [mcpdotnet](https://www.nuget.org/packages/mcpdotnet) | ||
2. Retrieve the list of tools the MCP Server makes available | ||
3. Convert the MCP tools to Semantic Kernel functions so they can be added to a Kernel instance | ||
4. Invoke the tools from Semantic Kernel using function calling | ||
|
||
## Configuring Secrets | ||
|
||
The example require credentials to access OpenAI. | ||
|
||
If you have set up those credentials as secrets within Secret Manager or through environment variables for other samples from the solution in which this project is found, they will be re-used. | ||
|
||
### To set your secrets with Secret Manager: | ||
|
||
```text | ||
cd dotnet/samples/Demos/ModelContextProtocol | ||
dotnet user-secrets init | ||
dotnet user-secrets set "OpenAI:ChatModelId" "..." | ||
dotnet user-secrets set "OpenAI:ApiKey" "..." | ||
"..." | ||
``` | ||
|
||
### To set your secrets with environment variables | ||
|
||
Use these names: | ||
|
||
```text | ||
# OpenAI | ||
OpenAI__ChatModelId | ||
OpenAI__ApiKey | ||
``` |
17 changes: 17 additions & 0 deletions
17
dotnet/samples/Demos/ModelContextProtocol/SimpleToolsConsole.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"Options": { | ||
"ClientInfo": { | ||
"Name": "SimpleToolsConsole", | ||
"Version": "1.0.0" | ||
} | ||
}, | ||
"Config": { | ||
"Id": "everything", | ||
"Name": "Everything", | ||
"TransportType": "stdio", | ||
"TransportOptions": { | ||
"command": "npx", | ||
"arguments": "-y @modelcontextprotocol/server-everything" | ||
} | ||
} | ||
} |