Skip to content

.Net: Demo showing how to integrate MCP tools with Semantic Kernel #10779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageVersion Include="Dapr.Actors.AspNetCore" Version="1.14.0" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.14.0" />
<PackageVersion Include="FastBertTokenizer" Version="1.0.28" />
<PackageVersion Include="mcpdotnet" Version="1.0.1.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.13" />
<PackageVersion Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="1.0.1" />
Expand Down Expand Up @@ -87,8 +88,7 @@
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables"
Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
Expand Down
9 changes: 9 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessFramework.Aspire.Tra
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.Bedrock", "src\Agents\Bedrock\Agents.Bedrock.csproj", "{8C658E1E-83C8-4127-B8BF-27A638A45DDD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol", "samples\Demos\ModelContextProtocol\ModelContextProtocol.csproj", "{B16AC373-3DA8-4505-9510-110347CD635D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1257,6 +1259,12 @@ Global
{8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Publish|Any CPU.Build.0 = Publish|Any CPU
{8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C658E1E-83C8-4127-B8BF-27A638A45DDD}.Release|Any CPU.Build.0 = Release|Any CPU
{B16AC373-3DA8-4505-9510-110347CD635D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B16AC373-3DA8-4505-9510-110347CD635D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B16AC373-3DA8-4505-9510-110347CD635D}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{B16AC373-3DA8-4505-9510-110347CD635D}.Publish|Any CPU.Build.0 = Debug|Any CPU
{B16AC373-3DA8-4505-9510-110347CD635D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B16AC373-3DA8-4505-9510-110347CD635D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1429,6 +1437,7 @@ Global
{37381352-4F10-427F-AB8A-51FEAB265201} = {3F260A77-B6C9-97FD-1304-4B34DA936CF4}
{DAD5FC6A-8CA0-43AC-87E1-032DFBD6B02A} = {3F260A77-B6C9-97FD-1304-4B34DA936CF4}
{8C658E1E-83C8-4127-B8BF-27A638A45DDD} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9}
{B16AC373-3DA8-4505-9510-110347CD635D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
Expand Down
147 changes: 147 additions & 0 deletions dotnet/samples/Demos/ModelContextProtocol/McpClientUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// 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;
internal static class McpClientUtils
{
internal static async Task<IMcpClient> GetSimpleToolsAsync()
{
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);
}

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();
}

internal 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()
);
}

internal 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;
}

internal static IEnumerable<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();
}

internal 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;
}
}
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>
50 changes: 50 additions & 0 deletions dotnet/samples/Demos/ModelContextProtocol/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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"]!);
}

Kernel kernel = builder.Build();

// Add the MCP simple tools as Kernel functions
var mcpClient = await McpClientUtils.GetSimpleToolsAsync().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 result = await kernel.InvokePromptAsync("Summarize the last four commits to the microsoft/semantic-kernel repository?", new(executionSettings)).ConfigureAwait(false);
Console.WriteLine("\n\nSummary of the last four commits to the microsoft/semantic-kernel repository:");
Console.WriteLine(result);
40 changes: 40 additions & 0 deletions dotnet/samples/Demos/ModelContextProtocol/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Model Context Protocol Sample

This example demonstrates how to use Model Context Protocol tools with Semantic Kernel.

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
```
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"
}
}
}
Loading