如何把 ASP-NET Core WebApi 打造成 Mcp Server
前言
MCP (Model Context Protocol) 即模型上下文協議目前不要太火爆了,關於它是什麼相信大家已經很熟悉了。目前主流的 AI 開發框架和 AI 工具都支持集成MCP
,這也正是它的意義所在。畢竟作爲一個標準的協議,當然是更多的生態接入進來纔會有意義。使用 MCP 我們可以把Tools調用標準化
,這意味着我們可以忽略語言、框架快速把工具融合到不同的模型中去。現在,如何把現有的業務邏輯快速的接入到模型中,成爲模型落地很關鍵的一步,今天我們就藉助微軟的Semantic Kernel
和Microsoft.Extensions.AI
框架,通過簡單的示例展示,如何把現有的ASP NET Core WebApi
轉換成MCP Server
。
概念相關
接下來我們大致介紹一下本文設計到的相關的概念以及涉及到的相關類庫
MCP
MCP 是一個開放協議,它爲應用程序向 LLM 提供上下文的方式進行了標準化。它的重點是標準化,而不是取代誰。它涉及到幾個核心的概念
-
• MCP Hosts: 如
Claude Desktop
、IDE
、AI
工具、或者是你開發的 AI 程序等 -
• MCP Clients: 維護與
MCP Servers
一對一連接的協議客戶端 -
• MCP Servers: 輕量級程序,通過標準的
Model Context Protocol
提供特定能力
簡單來說就是你寫的 AI 應用就是MCP Hosts
,因爲MCP
是一個協議,所以你需要通過MCP Clients
訪問MCP Servers
,MCP Servers
提供的就是工具或者一些其他能力。需要說明的是,如果想在 AI 應用中使用MCP
,模型需要支持Function Calling
,當然如果你能通過提示詞
的方式調試出來也是可以的,但是效果肯定不如本身就支持Function Calling
。
因爲 MCP 是一個開放協議,所以我們可以把原來固定在 AI 應用裏的工具代碼單獨抽離出來,形成獨立的應用,這樣這個 Tools 應用就可以和 AI 應用隔離,他們可以不是同一種語言,甚至可以在不同的機器上。所以現在很多開源的組件和平臺都可以提供自己的MCP Server
了。就和沒有微服務概念之前我們代碼都寫到一個項目裏,有了微服務之後我們可以把不同的模塊形成單獨的項目,甚至可以使用不同的開發語言。可以通過 HTTP、RPC 等多種方式調用他麼。
框架
簡單介紹一下本文涉及到的相關框架及地址:
-
• Microsoft.Extensions.AI:微軟提供的通過. NET 實現 AIGC 操作的開發基礎框架,提供了基礎
對話
和Function Calling
等基礎操作,使用簡單擴展性強,支持 OTEL 要測協議監控模型調用情況。目前已適配 Ollama、OpenAI、Azure OpenAI 等。項目地址 https://github.com/dotnet/extensions/tree/main/src/Libraries/Microsoft.Extensions.AI -
• Semantic Kernel:以
Microsoft.Extensions.AI
爲基礎 (低版本的不是) 打造的更強大的 AI 開發框架,提供了基礎對話
和Function Calling
功能的同時,還提供了多模態、RAG、智能體、流程處理等強大的應用級功能, 有. NET、Python、Java 三種語言版本。項目地址 https://github.com/microsoft/semantic-kernel -
• mcpdotnet(modelcontextprotocol/csharp-sdk):原名爲 mcpdotnet,現在是. NET 構建 MCP 的官方項目,可以使的 Microsoft.Extensions.AI 和 Semantic Kernel 快速的適配到 MCP。項目地址 https://github.com/modelcontextprotocol/csharp-sdk
實現
整體來說實現的思路也很簡單,因爲Semantic Kernel
支持加載OpenAPI
格式的數據加載成它的Plugins
,我們可以把Plugins
轉換成Microsoft.Extensions.AI
提供的標準的AIFunction
類型,通過mcpdotnet
可以把AIFunction
標準類型轉換成mcpdotnet
的Tools
。
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
。首先添加MCP
和SemanticKernel 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
加載McpServer
裏的工具 -
• 其次,把
MCP Tools
加載到Microsoft.Extensions.AI
裏 -
• 最後,在和 AI 模型對話的時候把 Tools 轉換成
function calling
。中文的function calling
,個人體驗下來國內使用qwen-max
系列效果最好
其中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