Skip to content

.Net: Use Mcp tools by SK agents #11582

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
merged 9 commits into from
Apr 16, 2025
2 changes: 1 addition & 1 deletion dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<PackageVersion Include="Grpc.AspNetCore.Server.Reflection" Version="2.70.0" />
<PackageVersion Include="Grpc.AspNetCore.Web" Version="2.70.0" />
<PackageVersion Include="Grpc.Tools" Version="2.70.0" />
<PackageVersion Include="ModelContextProtocol" Version="0.1.0-preview.8" />
<PackageVersion Include="ModelContextProtocol" Version="0.1.0-preview.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.13" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.14" />
<PackageVersion Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="1.0.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CA2249;CS0612;SKEXP0001;VSTHRD111;CA2007</NoWarn>
<NoWarn>$(NoWarn);CA2249;CS0612;SKEXP0001;SKEXP0110;VSTHRD111;CA2007</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand All @@ -18,6 +18,8 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Agents\AzureAI\Agents.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Agents\Core\Agents.Core.csproj" />
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\..\..\src\SemanticKernel.Core\SemanticKernel.Core.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

namespace MCPClient;

internal sealed class Program
internal sealed partial class Program
{
public static async Task Main(string[] args)
{
Expand All @@ -30,6 +30,10 @@ public static async Task Main(string[] args)
await UseMCPResourceTemplatesAsync();

await UseMCPSamplingAsync();

await UseChatCompletionAgentWithMCPToolsAsync();

await UseAzureAIAgentWithMCPToolsAsync();
}

/// <summary>
Expand Down Expand Up @@ -356,7 +360,7 @@ private static Task<IMcpClient> CreateMcpClientAsync(
} : null
);

async Task<CreateMessageResult> InvokeHandlerAsync(CreateMessageRequestParams? request, IProgress<ProgressNotificationValue> progress, CancellationToken cancellationToken)
async ValueTask<CreateMessageResult> InvokeHandlerAsync(CreateMessageRequestParams? request, IProgress<ProgressNotificationValue> progress, CancellationToken cancellationToken)
{
if (request is null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.AzureAI;
using ModelContextProtocol.Client;

namespace MCPClient;

internal sealed partial class Program
{
/// <summary>
/// Demonstrates how to use <see cref="AzureAIAgent"/> with MCP tools represented as Kernel functions.
/// The code in this method:
/// 1. Creates an MCP client.
/// 2. Retrieves the list of tools provided by the MCP server.
/// 3. Creates a kernel and registers the MCP tools as Kernel functions.
/// 4. Defines Azure AI agent with instructions, name, kernel, and arguments.
/// 5. Invokes the agent with a prompt.
/// 6. The agent sends the prompt to the AI model, together with the MCP tools represented as Kernel functions.
/// 7. The AI model calls DateTimeUtils-GetCurrentDateTimeInUtc function to get the current date time in UTC required as an argument for the next function.
/// 8. 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.
/// 9. Having received the weather information from the function call, the AI model returns the answer to the agent and the agent returns the answer to the user.
/// </summary>
private static async Task UseAzureAIAgentWithMCPToolsAsync()
{
Console.WriteLine($"Running the {nameof(UseAzureAIAgentWithMCPToolsAsync)} sample.");

// Create an MCP client
await using IMcpClient mcpClient = await CreateMcpClientAsync();

// Retrieve and display the list provided by the MCP server
IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
DisplayTools(tools);

// Create a kernel and register the MCP tools as Kernel functions
Kernel kernel = new();
kernel.Plugins.AddFromFunctions("Tools", tools.Select(aiFunction => aiFunction.AsKernelFunction()));

// Define the agent using the kernel with registered MCP tools
AzureAIAgent agent = await CreateAzureAIAgentAsync(
name: "WeatherAgent",
instructions: "Answer questions about the weather.",
kernel: kernel
);

// Invokes agent with a prompt
string prompt = "What is the likely color of the sky in Boston today?";
Console.WriteLine(prompt);

AgentResponseItem<ChatMessageContent> response = await agent.InvokeAsync(message: prompt).FirstAsync();
Console.WriteLine(response.Message);
Console.WriteLine();

// The expected output is: Today in Boston, the weather is 61°F and rainy. Due to the rain, the likely color of the sky will be gray.

// Delete the agent thread after use
await response!.Thread.DeleteAsync();

// Delete the agent after use
await agent.Client.DeleteAgentAsync(agent.Id);
}

private static async Task<AzureAIAgent> CreateAzureAIAgentAsync(Kernel kernel, string name, string instructions)
{
// Load and validate configuration
IConfigurationRoot config = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.AddEnvironmentVariables()
.Build();

if (config["AzureAI:ConnectionString"] is not { } connectionString)
{
const string Message = "Please provide a valid `AzureAI:ConnectionString` secret to run this sample. See the associated README.md for more details.";
Console.Error.WriteLine(Message);
throw new InvalidOperationException(Message);
}

string modelId = config["AzureAI:ChatModelId"] ?? "gpt-4o-mini";

// Create the Azure AI Agent
AIProjectClient projectClient = AzureAIAgent.CreateAzureAIClient(connectionString, new AzureCliCredential());

AgentsClient agentsClient = projectClient.GetAgentsClient();

Azure.AI.Projects.Agent agent = await agentsClient.CreateAgentAsync(modelId, name, null, instructions);

return new AzureAIAgent(agent, agentsClient)
{
Kernel = kernel
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using ModelContextProtocol.Client;

namespace MCPClient;

internal sealed partial class Program
{
/// <summary>
/// Demonstrates how to use <see cref="ChatCompletionAgent"/> with MCP tools represented as Kernel functions.
/// The code in this method:
/// 1. Creates an MCP client.
/// 2. Retrieves the list of tools provided by the MCP server.
/// 3. Creates a kernel and registers the MCP tools as Kernel functions.
/// 4. Defines chat completion agent with instructions, name, kernel, and arguments.
/// 5. Invokes the agent with a prompt.
/// 6. The agent sends the prompt to the AI model, together with the MCP tools represented as Kernel functions.
/// 7. The AI model calls DateTimeUtils-GetCurrentDateTimeInUtc function to get the current date time in UTC required as an argument for the next function.
/// 8. 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.
/// 9. Having received the weather information from the function call, the AI model returns the answer to the agent and the agent returns the answer to the user.
/// </summary>
private static async Task UseChatCompletionAgentWithMCPToolsAsync()
{
Console.WriteLine($"Running the {nameof(UseChatCompletionAgentWithMCPToolsAsync)} sample.");

// Create an MCP client
await using IMcpClient mcpClient = await CreateMcpClientAsync();

// Retrieve and display the list provided by the MCP server
IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
DisplayTools(tools);

// Create a kernel and register the MCP tools as kernel functions
Kernel kernel = CreateKernelWithChatCompletionService();
kernel.Plugins.AddFromFunctions("Tools", tools.Select(aiFunction => aiFunction.AsKernelFunction()));

// Enable automatic function calling
OpenAIPromptExecutionSettings executionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
};

string prompt = "What is the likely color of the sky in Boston today?";
Console.WriteLine(prompt);

// Define the agent
ChatCompletionAgent agent = new()
{
Instructions = "Answer questions about the weather.",
Name = "WeatherAgent",
Kernel = kernel,
Arguments = new KernelArguments(executionSettings),
};

// Invokes agent with a prompt
ChatMessageContent response = await agent.InvokeAsync(prompt).FirstAsync();

Console.WriteLine(response);
Console.WriteLine();

// The expected output is: The sky in Boston today is likely gray due to rainy weather.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,18 @@ public static IMcpServerBuilder WithResourceHandlers(this IMcpServerBuilder buil
return builder;
}

private static Task<ListPromptsResult> HandleListPromptRequestsAsync(RequestContext<ListPromptsRequestParams> context, CancellationToken cancellationToken)
private static ValueTask<ListPromptsResult> HandleListPromptRequestsAsync(RequestContext<ListPromptsRequestParams> context, CancellationToken cancellationToken)
{
// Get and return all prompt definitions registered in the DI container
IEnumerable<PromptDefinition> promptDefinitions = context.Server.Services!.GetServices<PromptDefinition>();

return Task.FromResult(new ListPromptsResult
return ValueTask.FromResult(new ListPromptsResult
{
Prompts = [.. promptDefinitions.Select(d => d.Prompt)]
});
}

private static async Task<GetPromptResult> HandleGetPromptRequestsAsync(RequestContext<GetPromptRequestParams> context, CancellationToken cancellationToken)
private static async ValueTask<GetPromptResult> HandleGetPromptRequestsAsync(RequestContext<GetPromptRequestParams> context, CancellationToken cancellationToken)
{
// Make sure the prompt name is provided
if (context.Params?.Name is not string { } promptName || string.IsNullOrEmpty(promptName))
Expand All @@ -217,7 +217,7 @@ private static async Task<GetPromptResult> HandleGetPromptRequestsAsync(RequestC
return await definition.Handler(context, cancellationToken);
}

private static Task<ReadResourceResult> HandleReadResourceRequestAsync(RequestContext<ReadResourceRequestParams> context, CancellationToken cancellationToken)
private static ValueTask<ReadResourceResult> HandleReadResourceRequestAsync(RequestContext<ReadResourceRequestParams> context, CancellationToken cancellationToken)
{
// Make sure the uri of the resource or resource template is provided
if (context.Params?.Uri is not string { } resourceUri || string.IsNullOrEmpty(resourceUri))
Expand Down Expand Up @@ -248,23 +248,23 @@ private static Task<ReadResourceResult> HandleReadResourceRequestAsync(RequestCo
throw new ArgumentException($"No handler found for the resource uri '{resourceUri}'.");
}

private static Task<ListResourceTemplatesResult> HandleListResourceTemplatesRequestAsync(RequestContext<ListResourceTemplatesRequestParams> context, CancellationToken cancellationToken)
private static ValueTask<ListResourceTemplatesResult> HandleListResourceTemplatesRequestAsync(RequestContext<ListResourceTemplatesRequestParams> context, CancellationToken cancellationToken)
{
// Get and return all resource template definitions registered in the DI container
IEnumerable<ResourceTemplateDefinition> definitions = context.Server.Services!.GetServices<ResourceTemplateDefinition>();

return Task.FromResult(new ListResourceTemplatesResult
return ValueTask.FromResult(new ListResourceTemplatesResult
{
ResourceTemplates = [.. definitions.Select(d => d.ResourceTemplate)]
});
}

private static Task<ListResourcesResult> HandleListResourcesRequestAsync(RequestContext<ListResourcesRequestParams> context, CancellationToken cancellationToken)
private static ValueTask<ListResourcesResult> HandleListResourcesRequestAsync(RequestContext<ListResourcesRequestParams> context, CancellationToken cancellationToken)
{
// Get and return all resource template definitions registered in the DI container
IEnumerable<ResourceDefinition> definitions = context.Server.Services!.GetServices<ResourceDefinition>();

return Task.FromResult(new ListResourcesResult
return ValueTask.FromResult(new ListResourcesResult
{
Resources = [.. definitions.Select(d => d.Resource)]
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public static ResourceDefinition CreateBlobResource(string uri, string name, byt
/// <param name="context">The MCP server context.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The result of the invocation.</returns>
public async Task<ReadResourceResult> InvokeHandlerAsync(RequestContext<ReadResourceRequestParams> context, CancellationToken cancellationToken)
public async ValueTask<ReadResourceResult> InvokeHandlerAsync(RequestContext<ReadResourceRequestParams> context, CancellationToken cancellationToken)
{
if (this._kernelFunction == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public bool IsMatch(string uri)
/// <param name="context">The MCP server context.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The result of the invocation.</returns>
public async Task<ReadResourceResult> InvokeHandlerAsync(RequestContext<ReadResourceRequestParams> context, CancellationToken cancellationToken)
public async ValueTask<ReadResourceResult> InvokeHandlerAsync(RequestContext<ReadResourceRequestParams> context, CancellationToken cancellationToken)
{
this._kernelFunction ??= KernelFunctionFactory.CreateFromMethod(this.Handler);

Expand Down
Loading
Loading