如何把 ASP-NET Core WebApi 打造成 Mcp Server

前言

    MCP (Model Context Protocol) 即模型上下文協議目前不要太火爆了,關於它是什麼相信大家已經很熟悉了。目前主流的 AI 開發框架和 AI 工具都支持集成MCP,這也正是它的意義所在。畢竟作爲一個標準的協議,當然是更多的生態接入進來纔會有意義。使用 MCP 我們可以把Tools調用標準化,這意味着我們可以忽略語言、框架快速把工具融合到不同的模型中去。現在,如何把現有的業務邏輯快速的接入到模型中,成爲模型落地很關鍵的一步,今天我們就藉助微軟的Semantic KernelMicrosoft.Extensions.AI框架,通過簡單的示例展示,如何把現有的ASP NET Core WebApi轉換成MCP Server

概念相關

接下來我們大致介紹一下本文設計到的相關的概念以及涉及到的相關類庫

MCP

MCP 是一個開放協議,它爲應用程序向 LLM 提供上下文的方式進行了標準化。它的重點是標準化,而不是取代誰。它涉及到幾個核心的概念

簡單來說就是你寫的 AI 應用就是MCP Hosts,因爲MCP是一個協議,所以你需要通過MCP Clients訪問MCP ServersMCP Servers提供的就是工具或者一些其他能力。需要說明的是,如果想在 AI 應用中使用MCP,模型需要支持Function Calling,當然如果你能通過提示詞的方式調試出來也是可以的,但是效果肯定不如本身就支持Function Calling

因爲 MCP 是一個開放協議,所以我們可以把原來固定在 AI 應用裏的工具代碼單獨抽離出來,形成獨立的應用,這樣這個 Tools 應用就可以和 AI 應用隔離,他們可以不是同一種語言,甚至可以在不同的機器上。所以現在很多開源的組件和平臺都可以提供自己的MCP Server了。就和沒有微服務概念之前我們代碼都寫到一個項目裏,有了微服務之後我們可以把不同的模塊形成單獨的項目,甚至可以使用不同的開發語言。可以通過 HTTP、RPC 等多種方式調用他麼。

框架

簡單介紹一下本文涉及到的相關框架及地址:

實現

整體來說實現的思路也很簡單,因爲Semantic Kernel支持加載OpenAPI格式的數據加載成它的Plugins,我們可以把Plugins轉換成Microsoft.Extensions.AI提供的標準的AIFunction類型,通過mcpdotnet可以把AIFunction標準類型轉換成mcpdotnetTools

WebApi

我們需要新建一個ASP.NET Core WebAPI項目,用來完成查詢天氣的功能。首先,添加Swagger支持。當然你使用別的庫也可以,這裏的重點就是可以得到該項目接口的OpenAPI數據信息。

<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />

其次,添加根據 IP 查詢地址信息的功能

<PackageReference Include="IPTools.China" Version="1.6.0" />

因爲IPTools使用的是sqlite數據庫,所以需要把 db 加載到項目裏。具體使用細節可以查看該庫的具體地址 https://github.com/stulzq/IPTools

<ItemGroup>
 <None Update="ip2region.db">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

接下來實現具體功能的Controller代碼

 /// <summary>
/// 獲取城市天氣
/// </summary>
 [ApiController]
 [Route("api/[controller]/[action]")]
public class WeatherController(IHttpClientFactory _httpClientFactory) : ControllerBase
 {
     /// <summary>
     /// 獲取當前時間
     /// </summary>
     /// <returns>當前時間</returns>
     [HttpGet]
     public string GetCurrentDate()
     {
         return DateTime.Now.ToString("MM/dd");
     }

     /// <summary>
     /// 獲取當前城市信息
     /// </summary>
     /// <returns>當前城市信息</returns>
     [HttpGet]
     public async Task<IpInfo> GetLocation()
     {
         var httpClient = _httpClientFactory.CreateClient();
         IpData ipInfo = await httpClient.GetFromJsonAsync<IpData>("https://ipinfo.io/json");
         var ipinfo = IpTool.Search(ipInfo!.ip);
         return ipinfo;
     }

     /// <summary>
     /// 獲取天氣信息
     /// </summary>
     /// <param >省份</param>
     /// <param >城市</param>
     /// <param >日期(格式:月份/日期)</param>
     /// <returns>天氣信息</returns>
     [HttpGet]
     public async Task<string> GetCurrentWeather(string region, string city, string currentDate)
     {
         var httpClient = _httpClientFactory.CreateClient();
         WeatherRoot weatherRoot = await httpClient.GetFromJsonAsync<WeatherRoot>($"https://cn.apihz.cn/api/tianqi/tqybmoji15.php?id=88888888&key=88888888&sheng={region!}&place={city!}")!;
         DataItem today = weatherRoot!.data!.FirstOrDefault(i => i.week2 == currentDate)!;
         return$"{today!.week2} {today.week1},天氣{today.wea1}轉{today.wea2}。最高氣溫{today.wendu1}攝氏度,最低氣溫{today.wendu2}攝氏度。";
     }
 }

publicclassIpData
{
    publicstring ip { get; set; }
    publicstring city { get; set; }
    publicstring region { get; set; }
    publicstring country { get; set; }
    publicstring loc { get; set; }
    publicstring org { get; set; }
    publicstring postal { get; set; }
    publicstring timezone { get; set; }
    publicstring readme { get; set; }
}

publicclassDataItem
{
    publicstring week1 { get; set; }
    publicstring week2 { get; set; }
    publicstring wea1 { get; set; }
    publicstring wea2 { get; set; }
    publicstring wendu1 { get; set; }
    publicstring wendu2 { get; set; }
    publicstring img1 { get; set; }
    publicstring img2 { get; set; }
}

publicclassWeatherRoot
{
    public List<DataItem> data { get; set; }
    publicint code { get; set; }
    publicstring place { get; set; }
}

代碼裏實現了三個 action,分別是獲取城市天氣、獲取當前城市信息、獲取天氣信息接口。接下來添加項目配置

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "",
        Description = "",
    });

    var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});
builder.Services.AddHttpClient();

var app = builder.Build();

//使用OpenApi的版本信息
app.UseSwagger(options =>
{
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;
});
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/swagger/v1/swagger.json""v1");
});

app.UseAuthorization();

app.MapControllers();

app.Run();

完成上面的代碼之後,可以運行起來該項目。通過http://項目地址:端口/swagger/v1/swagger.json獲取WebApi接口的OpenAPI的數據格式。

MCP Server

接下來搭建MCP Server項目,來把上面的WebApi項目轉換成MCP Server。首先添加MCPSemanticKernel OpenApi涉及到的類庫,因爲我們需要使用SemanticKernel來把swagger.json加載成Plugins

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  <PackageReference Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.47.0" />
  <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.11" />
</ItemGroup>

接下來我們來編寫具體的代碼實現

IKernelBuilder kernelBuilder = Kernel.CreateBuilder();;
Kernel kernel = kernelBuilder.Build();

#pragma warning disable SKEXP0040

//把swagger.json加載成Plugin
//這裏也可以是本地路徑或者是文件流
await kernel.ImportPluginFromOpenApiAsync(
   pluginName: "city_date_weather",
   uri: new Uri("http://localhost:5021/swagger/v1/swagger.json"),
   executionParameters: new OpenApiFunctionExecutionParameters 
   { 
       EnablePayloadNamespacing = true
   }
 );

#pragma warning restore SKEXP0040

var builder = Host.CreateEmptyApplicationBuilder(settings: null);
builder.Services
    //添加MCP Server
    .AddMcpServer()
    //使用Stdio模式
    .WithStdioServerTransport()
    //把Plugins轉換成McpServerTool
    .WithTools(kernel.Plugins);

await builder.Build().RunAsync();


publicstaticclassMcpServerBuilderExtensions
{
    /// <summary>
    /// 把Plugins轉換成McpServerTool
    /// </summary>
    public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, KernelPluginCollection plugins)
    {
        foreach (var plugin in plugins)
        {
            foreach (var function in plugin)
            {
                builder.Services.AddSingleton(services => McpServerTool.Create(function.AsAIFunction()));
            }
        }

        return builder;
    }
}

MCP 的傳輸層協議可以使用stdio(既標準輸入輸出)sse或者是streamable,甚至是自定義的方式進行通信。其中stdio可以本機進程間通信,sse或者是streamable進行遠程通信。它的消息格式,或者理解爲數據傳輸的格式是JSON-RPC 2.0

其中ImportPluginFromOpenApiAsync方法是其中比較關鍵的點,它是把OpenApi接口信息轉換成Kernel Plugins。它通過讀取swagger.json裏的接口信息的元數據構建成KernelFunction實例,而具體的觸發操作則轉換成 Http 調用。具體的實現方式可以通過閱讀 CreateRestApiFunction[1] 方法源碼的實現。

再次AsAIFunction方法則是把KernelFunctionFromMethod轉換成KernelAIFunction,因爲KernelFunctionFromMethod是繼承了KernelFunction類,KernelAIFunction則是繼承了AIFunction類,所以這個操作是把KernelFunction轉換成AIFunction。可以把KernelAIFunction理解成KernelFunction的外觀類,它只是包裝了KernelFunction的操作,所以觸發的時候還是KernelFunctionFromMethod裏的操作。具體的實現可以查看 KernelAIFunction[2] 類的實現。

幾句簡單的代碼既可以實現一個Mcp Server,雖然上面我們使用的是Uri的方式加載的OpenAPI文檔地址,但是它也支持本地文件地址或者文件流的方式。不得不說微軟體系下的框架在具體的落地方面做得確實夠實用,因爲具體的邏輯都是WebApi實現的,Mcp Server只是一個媒介。

MCP Client

最後實現的是MCP Client是爲了驗證Mcp Server效果用的,這裏可以使用任何框架來實現,需要引入ModelContextProtocol和具體的 AI 框架,AI 框架可以是Microsoft.Extensions.AI,也可以是Semantic Kernel。這裏我們使用Microsoft.Extensions.AI,因爲它足夠簡單也足夠簡潔,引入相關的類庫

<ItemGroup>
    <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.4.3-preview.1.25230.7" />
    <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.12" />
</ItemGroup>

其中ModelContextProtocol提供了McpClient功能,Microsoft.Extensions.AI提供具體的 AI 功能集成。具體實現如下所示

//加載McpServer,以爲我們構建的是使用Stdio的方式,所以這裏直接使用McpServer路徑即可
awaitusing IMcpClient mcpClient = await McpClientFactory.CreateAsync(new StdioClientTransport(new()
{
    Name = "city_date_weather",
    Command = "..\\..\\..\\..\\McpServerDemo\\bin\\Debug\\net9.0\\McpServerDemo.exe"
}));

//加載MCP Tools
var tools = await mcpClient.ListToolsAsync();
foreach (AIFunction tool in tools)
{
    Console.WriteLine($"Tool Name: {tool.Name}");
    Console.WriteLine($"Tool Description: {tool.Description}");
    Console.WriteLine();
}

//中文的function calling,國內使用qwen-max系列效果最好
string apiKey = "sk-****";
var chatClient = new ChatClient("qwen-max-2025-01-25", new ApiKeyCredential(apiKey), new OpenAIClientOptions
{
    Endpoint = new Uri("https://dashscope.aliyuncs.com/compatible-mode/v1")
}).AsIChatClient();

IChatClient client = new ChatClientBuilder(chatClient)
    //開啓function calling支持
    .UseFunctionInvocation()
    .Build();

//構建Tools
ChatOptions chatOptions = new()
{
    Tools = [.. tools],
};

//創建對話代碼
List<Microsoft.Extensions.AI.ChatMessage> chatList = [];

string question = "";
do
{
    Console.Write($"User:");
    question = Console.ReadLine();

    if (string.IsNullOrWhiteSpace(question) || question == "exists")
    {
        break;
    }

    chatList.Add(new Microsoft.Extensions.AI.ChatMessage(ChatRole.User, question));

    Console.Write($"Assistant:");
    StringBuilder sb = new StringBuilder();
    awaitforeach (var update in client.GetStreamingResponseAsync(chatList, chatOptions))
    {
        if (string.IsNullOrWhiteSpace(update.Text))
        {
            continue;
        }
        sb.Append(update.Text);

        Console.Write(update.Text);
    }

    chatList.Add(new Microsoft.Extensions.AI.ChatMessage(ChatRole.Assistant, sb.ToString()));

    Console.WriteLine();

} while (true);

Console.ReadLine();

上面的代碼實現了McpClient接入 AI 應用

其中mcpClient.ListToolsAsync()獲取到的是McpClientTool集合,而McpClientTool繼承自AIFunction類,具體可查看 McpClientTool[3] 實現源碼。由此可以看出微軟封裝Microsoft.Extensions.AI基座的重要性,以後更多的框架都可以圍繞Microsoft.Extensions.AI進行封裝統一操作,這樣大大提升了擴展的便捷性。

當然,你也可以使用Semantic Kernel框架進行上面的操作,這裏就不過多贅述了,直接上代碼

//加載McpServer,以爲我們構建的是使用Stdio的方式,所以這裏直接使用McpServer路徑即可
awaitusing IMcpClient mcpClient = await McpClientFactory.CreateAsync(new StdioClientTransport(new()
{
    Name = "city_date_weather",
    Command = "..\\..\\..\\..\\McpServerDemo\\bin\\Debug\\net9.0\\McpServerDemo.exe"
}));

//加載MCP Tools
var tools = await mcpClient.ListToolsAsync();

using HttpClientHandler handler = new HttpClientHandler
{
    ClientCertificateOptions = ClientCertificateOption.Automatic
};

using HttpClient httpClient = new(handler)
{
    BaseAddress = new Uri("https://dashscope.aliyuncs.com/compatible-mode/v1")
};

#pragma warning disable SKEXP0070
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion("qwen-max-2025-01-25""sk-***", httpClient: httpClient);
//把Tools加載成sk的Plugins
kernelBuilder.Plugins.AddFromFunctions("weather", tools.Select(aiFunction => aiFunction.AsKernelFunction()));

Kernel kernel = kernelBuilder.Build();
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();

PromptExecutionSettings promptExecutionSettings = new()
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

var history = new ChatHistory();

while (true)
{
    Console.Write($"User:");
    string input = Console.ReadLine();

    if (string.IsNullOrWhiteSpace(input) || input == "exists")
    {
        break;
    }

    history.AddUserMessage(input);
    var chatMessage = await chatCompletionService.GetChatMessageContentAsync(
    history,
    executionSettings: promptExecutionSettings,
    kernel: kernel);

    Console.WriteLine("Assistant:" + chatMessage.Content);

    history.AddAssistantMessage(chatMessage.Content);
}

Console.ReadLine();

因爲MCP是一個協議標準,所以MCP Server可以做到一次構建,到處使用。

運行效果

運行的時候需要先運行起來WebApi項目,然後把McpServer編譯成exe文件,然後運行McpClient項目,我們打印出來了可用的Tools列表。在 Client 項目進行對話,詢問當前天氣效果如下

感興趣的如果想運行具體的代碼示例,可以查看我上傳的代碼示例 McpDemo[4]

總結

    本文演示瞭如何把 ASP.NET Core WebApi 打造成 Mcp Server,通過講解基本概念,介紹使用的框架,以及簡單的示例展示了這一過程,整體來說是比較簡單的。MCP的重點是標準化,而不是取代。如果想在 AI 應用中使用MCP,模型需要支持Function Calling. 我們可以把原來固定在 AI 應用裏的工具代碼單獨抽離出來,使用不同的開發語言形成獨立的應用,這樣這個 Tools 應用就可以和 AI 應用隔離,形成獨立可複用的工具。

    現在 AI 大部分時候確實很好用,但是它也不是銀彈。至於它的邊界在哪裏,只有不斷地使用實踐。你身邊的事情都可以先用 AI 嘗試去做,不斷地試探它的能力。AI 幫你做完的事情,如果能達到你的預期,你可以看它的實現方式方法,讓自己學習到更好的思路。如果是完全依賴 AI,而自己不去思考,那真的可能會被 AI 取代掉。只有你自己不斷的進步,才能進一步的探索 AI,讓它成爲你的好工具。

引用鏈接

[1] CreateRestApiFunction: https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs#L251
[2] KernelAIFunction: https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs#L529
[3] McpClientTool: https://github.com/modelcontextprotocol/csharp-sdk/blob/main/src/ModelContextProtocol/Client/McpClientTool.cs#L28
[4] McpDemo: https://github.com/softlgl/McpDemo

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0fygcmuxZKWBIuAS2oSK4g