Skip to content

Commit

Permalink
.Net: Demo showing how to integrate MCP tools with Semantic Kernel (m…
Browse files Browse the repository at this point in the history
…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
markwallace-microsoft authored Mar 4, 2025
1 parent 7c8dccc commit f8ee3ac
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 2 deletions.
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
159 changes: 159 additions & 0 deletions dotnet/samples/Demos/ModelContextProtocol/McpDotNetExtensions.cs
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
}
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>
55 changes: 55 additions & 0 deletions dotnet/samples/Demos/ModelContextProtocol/Program.cs
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}");
44 changes: 44 additions & 0 deletions dotnet/samples/Demos/ModelContextProtocol/README.md
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 dotnet/samples/Demos/ModelContextProtocol/SimpleToolsConsole.json
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"
}
}
}

0 comments on commit f8ee3ac

Please sign in to comment.