セマンティックカーネル：MCPの開始

MCPがますます注目を浴びており、主要なモデル会社も続々とMCPをサポートしています。OpenAIも最近MCPのサポートを発表し、コミュニティのSDKも登場しました。今日はModelContextProtocolを使用してサーバーとクライアントを作成し、それらを接続してみましょう。同時に、SKを忘れずに、SKがどのようにMCPを使用するかを見てみましょう。

まずはサーバー側を見てみましょう：

プロジェクトファイルは以下の通りです：

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.2" />
  </ItemGroup>
</Project>
```

Program.cs：

```csharp
using ModelContextProtocol;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapMcpSse();
app.Run();
```

ツール定義：

```csharp
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace MCPOrderTool.Tools;

[McpServerToolType]
public static class OrderTool
{
    [McpServerTool("queryOrder"), Description("開始日と終了日に基づいて注文を照会する")]
    public static List<Order> QueryList(DateTime? beginTime, DateTime? endTime)
    {
        Console.WriteLine("-------------パラメータ-------------");
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"BeginTime:{beginTime}，EndTime：{endTime}");
        Console.ResetColor();
        Console.WriteLine("-------------パラメータ-------------");
        if (beginTime is null || endTime is null)
        {
            beginTime = DateTime.Now;
            endTime = DateTime.Now;
        }
        return new List<Order>
        {
            new Order("NO000001", "Order1", DateTime.Now.ToString("yyyy-MM-dd"), "Pending"),
            new Order("NO000002", "Order2", DateTime.Now.ToString("yyyy-MM-dd"), "Completed"),
            new Order("NO000003", "Order3", DateTime.Now.ToString("yyyy-MM-dd"), "Pending"),
            new Order("NO000004", "Order4", DateTime.Now.ToString("yyyy-MM-dd"), "Completed"),
            new Order("NO000005", "Order5", DateTime.Now.ToString("yyyy-MM-dd"), "Pending"),
            new Order("NO000006", "Order6", DateTime.Now.ToString("yyyy-MM-dd"), "Completed"),
            new Order("NO000007", "Order7", DateTime.Now.ToString("yyyy-MM-dd"), "Pending"),
            new Order("NO000008", "Order8", DateTime.Now.ToString("yyyy-MM-dd"), "Completed"),
            new Order("NO000009", "Order9", DateTime.Now.ToString("yyyy-MM-dd"), "Pending"),
            new Order("NO000010", "Order10", DateTime.Now.ToString("yyyy-MM-dd"), "Completed"),
        };
    }
}

public record Order(string OrderId, string OrderName, string OrderTime, string OrderStatus);
```

MCPサーバー側のマップ定義：

```csharp
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Protocol.Transport;

public static class McpEndpointRouteBuilderExtensions
{
    public static IEndpointConventionBuilder MapMcpSse(this IEndpointRouteBuilder endpoints)
    {
        IMcpServer? server = null;
        SseResponseStreamTransport? transport = null;
        var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
        var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();

        var routeGroup = endpoints.MapGroup("");

        routeGroup.MapGet("/sse", async (HttpResponse response, CancellationToken requestAborted) =>
        {
            await using var localTransport = transport = new SseResponseStreamTransport(response.Body);
            await using var localServer = server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);

            await localServer.StartAsync(requestAborted);
            response.Headers.ContentType = "text/event-stream";
            response.Headers.CacheControl = "no-cache";
            try
            {
                await transport.RunAsync(requestAborted);
            }
            catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
            {
                Console.WriteLine("closed");
            }
        });

        routeGroup.MapPost("/message", async context =>
        {
            if (transport is null)
            {
                await Results.BadRequest("Connect to the /sse endpoint before sending messages.").ExecuteAsync(context);
                return;
            }
            var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(McpJsonUtilities.DefaultOptions, context.RequestAborted);
            if (message is null)
            {
                await Results.BadRequest("No message in request body.").ExecuteAsync(context);
                return;
            }
            await transport.OnMessageReceivedAsync(message, context.RequestAborted);
            context.Response.StatusCode = StatusCodes.Status202Accepted;
            await context.Response.WriteAsync("Accepted");
        });

        return routeGroup;
    }
}
```

次はクライアントの使用です：

プロジェクトファイル：

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25161.3" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0-preview.2.25163.2" />
    <PackageReference Include="Microsoft.SemanticKernel" Version="1.44.0" />
    <PackageReference Include="Microsoft.SemanticKernel.Plugins.Core" Version="1.44.0-alpha" />
    <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.2" />
    <PackageReference Include="ModelContextProtocol-SemanticKernel" Version="0.0.1-preview-03" />
  </ItemGroup>
</Project>
```

Program.csファイル：

```csharp
using Azure.Core;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using ModelContextProtocol.Client;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.SemanticKernel.Extensions;
using ModelContextProtocol.SemanticKernel.Options;
using ModelContextProtocol.Server;
using OpenAI;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;

var key = File.ReadAllText("c:/gpt/key.txt");

while (true)
{
    Console.WriteLine("===========================================================");
    Console.WriteLine("1、ツールリストの取得  2、クライアントでツールを呼び出す  3、SKでツールを呼び出す  0、終了");
    Console.WriteLine("===========================================================");
    var no = Console.ReadLine();
    switch (no)
    {
        case "1":
            await MCPClientToolsListAsync();
            break;
        case "2":
            await MCPClientAsync();
            break;
        case "3":
            await SKClientAsync();
            break;
        case "0":
            return;
    }
}

async Task MCPClientToolsListAsync()
{
    var serverConfig = new McpServerConfig
    {
        Id = "QueryOrder",
        Name = "QueryOrder",
        TransportType = TransportTypes.Sse,
        Location = "http://localhost:3001/sse"
    };
    var clientOptions = new McpClientOptions
    {
        ClientInfo = new()
        {
            Name = "QueryOrderClient",
            Version = "0.0.1",
        }
    };
    var mcpClient = await McpClientFactory.CreateAsync(serverConfig, clientOptions);
    Console.WriteLine("ツールを取得:");
    var tools = await mcpClient.ListToolsAsync();

    foreach (var tool in tools)
    {
        Console.WriteLine($"{tool.Name},{tool.Description}");
    }
    Console.WriteLine();
}

async Task MCPClientAsync()
{
    var serverConfig = new McpServerConfig
    {
        Id = "QueryOrder",
        Name = "MCPOrderTool",
        TransportType = TransportTypes.Sse,
        Location = "http://localhost:3001/sse"
    };
    var clientOptions = new McpClientOptions
    {
        ClientInfo = new()
        {
            Name = "QueryOrderClient",
            Version = "0.0.1",
        }
    };
    var mcpClient = await McpClientFactory.CreateAsync(serverConfig, clientOptions);
    var functions = await mcpClient.ListToolsAsync();
    IChatClient chatClient = new OpenAIClient(key).AsChatClient("gpt-4o-mini")
    .AsBuilder().UseFunctionInvocation().Build();
    var response = chatClient.GetStreamingResponseAsync(
        "今週の注文を照会",
        new()
        {
            Tools = [.. functions],
        });
    await foreach (var item in response)
    {
        Console.Write(item.Text);
    }
    Console.WriteLine();
}

async Task SKClientAsync()
{
    var builder = Kernel.CreateBuilder();
    builder.Services.AddLogging(c => c.AddDebug().SetMinimumLevel(LogLevel.Trace));

    builder.Services.AddOpenAIChatCompletion(
        serviceId: "openai",
        modelId: "gpt-4o-mini",
        apiKey: key);

    var kernel = builder.Build();
    kernel.Plugins.AddFromType<TimeInformationPlugin>();
    await kernel.Plugins.AddMcpFunctionsFromSseServerAsync("MCPOrderTool", "http://localhost:3001/sse");
    var executionSettings = new OpenAIPromptExecutionSettings
    {
        Temperature = 0,
        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
    };
    var prompt = "今月の注文を照会する";
    var result = kernel.InvokePromptStreamingAsync(prompt, new(executionSettings));
    await foreach (var item in result)
    {
        Console.Write(item.ToString());
    }
    Console.WriteLine();
}

public class TimeInformationPlugin
{
    [KernelFunction, Description("現在のUTC時間を取得する。")]
    public string GetCurrentUtcTime()
        => DateTime.UtcNow.ToString("R");
}
```

以下は実行結果です：1つ目はサーバー側のツール情報を列挙し、2つ目は通常の方法でMCPサーバーを呼び出します。この時、右の赤い枠に注意してください。時間は2023年で、モデルから取得した時間です。3つ目はSKでツールを呼び出し、SKに現在時間のプラグインを追加しています。青い枠を見てください。正しい時間が取得されています。

![画像](https://mmbiz.qpic.cn/sz_mmbiz_png/4CAA63nvHRBe3ciae7kOc7KwkMYyc1lpDpwwmfmqOCfrMLibd5oJOLDCujnXnUXWXVS8wWAiaAUDKLwr3sXlveQag/640?wx_fmt=png&from=appmsg)

(Translated by GPT)
元のリンク：https://mp.weixin.qq.com/s/8GTJTx3Jwe8ELBv9IeWlVA?token=1135395277&lang=zh_CN&wt.mc_id=MVP_325642