genai-function-calling/semantic-kernel-dotnet/Program.cs (138 lines of code) (raw):
using System.ClientModel; // for ApiKeyCredential
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Server;
using OpenAI;
sealed class Release
{
[JsonPropertyName("version")]
public required string Version { get; set; }
}
sealed class ReleasesResponse
{
[JsonPropertyName("releases")]
public required List<Release> Releases { get; set; }
}
[McpServerToolType]
sealed class ElasticsearchPlugin
{
[KernelFunction("get_latest_elasticsearch_version")]
[McpServerTool(Name = "get_latest_elasticsearch_version")]
[Description("Returns the latest GA version of Elasticsearch in \"X.Y.Z\" format.")]
public string GetLatestVersion(
[Description("Major version to filter by (e.g. 7, 8). Defaults to latest")] int? majorVersion = null)
{
using var httpClient = new HttpClient();
var response = httpClient.GetAsync("https://artifacts.elastic.co/releases/stack.json").Result;
var json = response.Content.ReadAsStringAsync().Result;
var releaseData = JsonSerializer.Deserialize<ReleasesResponse>(json);
var query = releaseData?.Releases
// Filter out non-release versions (e.g. -rc1) and remove " GA" suffix
.Select(r => r.Version.Replace(" GA", ""))
.Where(v => !string.IsNullOrEmpty(v) && !v.Contains('-') && Version.TryParse(v, out _))
.Select(v => Version.Parse(v));
if (majorVersion.HasValue)
{
query = query?.Where(v => v.Major == majorVersion.Value) ?? [];
}
return query?.Max()?.ToString() ?? throw new Exception("No valid versions found.");
}
}
class Program
{
[Experimental("SKEXP0001")]
static async Task Main()
{
var source = new ActivitySource("ElasticsearchVersionAgent");
if (Environment.GetCommandLineArgs().Contains("--mcp-server"))
{
using var activity = source.StartActivity(name: "mcp-server");
await Mcp.ServerMain<ElasticsearchPlugin>();
} else if (Environment.GetCommandLineArgs().Any(arg => arg.StartsWith("--mcp")))
{
using var activity = source.StartActivity(name: "agent-mcp");
await Mcp.ClientMain<ElasticsearchPlugin>(RunAgent);
}
else
{
using var activity = source.StartActivity(name: "agent");
await RunAgent(KernelPluginFactory.CreateFromType<ElasticsearchPlugin>());
}
}
static async Task RunAgent(KernelPlugin plugin)
{
var kernelBuilder = Kernel.CreateBuilder();
var azureApiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
if (azureApiKey != null) {
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
var deployment = Environment.GetEnvironmentVariable("CHAT_MODEL")!;
kernelBuilder.AddAzureOpenAIChatCompletion(deployment, endpoint, azureApiKey);
} else {
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? "your-api-key";
var model = Environment.GetEnvironmentVariable("CHAT_MODEL") ?? "gpt-4o-mini";
var baseUrl = Environment.GetEnvironmentVariable("OPENAI_BASE_URL");
OpenAIClientOptions? options = null;
if (baseUrl != null) {
options = new OpenAIClientOptions { Endpoint = new Uri(baseUrl) };
}
var openAIClient = new OpenAIClient(new ApiKeyCredential(apiKey), options);
kernelBuilder.AddOpenAIChatCompletion(model, openAIClient);
}
var kernel = kernelBuilder.Build();
kernel.Plugins.Add(plugin);
ChatCompletionAgent agent = new()
{
Name = "version-agent",
Kernel = kernel,
Arguments = new KernelArguments(new OpenAIPromptExecutionSettings()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
Temperature = 0
}),
};
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("What is the latest version of Elasticsearch 8?");
AgentThread? thread = null;
await foreach (ChatMessageContent response in agent.InvokeAsync(chatHistory, thread))
{
Console.WriteLine(response.Content);
}
}
}
internal static class Mcp
{
internal static async Task ServerMain<TKPlugin>() where TKPlugin : class, new()
{
var builder = Host.CreateApplicationBuilder();
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithTools<TKPlugin>();
await builder.Build().RunAsync();
}
[Experimental("SKEXP0001")]
internal static async Task ClientMain<TKPlugin>(Func<KernelPlugin, Task> pluginCallback) where TKPlugin : class, new()
{
var realPlugin = KernelPluginFactory.CreateFromType<TKPlugin>();
var options = new StdioClientTransportOptions()
{
Name = realPlugin.Name,
Command = Process.GetCurrentProcess().MainModule?.FileName ?? "dotnet",
WorkingDirectory = Environment.CurrentDirectory,
Arguments = Environment.GetCommandLineArgs().Concat(["--mcp-server"]).ToArray(),
};
await using IMcpClient mcpClient = await McpClientFactory.CreateAsync(new StdioClientTransport(options));
var tools = await mcpClient.ListToolsAsync();
var plugin = KernelPluginFactory.CreateFromFunctions(realPlugin.Name, tools.Select(aiFunction => aiFunction.AsKernelFunction()));
await pluginCallback(plugin);
}
}