Semantic Kernel也能充当MCP Client
背景
笔者之前,分别写过两篇关于Semantic Kernel(下简称SK)相关的博客,最近模型上下文协议(下称MCP)大火,实际上了解过SK的小伙伴,一看到 MCP的一些具体呈现,会发现,Client 调用 Server的方式,和SK调用插件的过程很像,实际操作了一下,发现确实是可以的。
也就是说,如果我们之前的项目里用到SK做过Agent相关的模块,如今也可以丝滑的让其充当MCP Client的角色,去使用更多MCP生态的东西,而不需要做更多的改动。
虽然SK是为AI Agent的发展而诞生的,但好框架就是好框架,没想到它和MCP也这么契合。
本篇,建立在《再尝Semantic Kernel,Planning特性很香》的基础上,再次扩展一下MCP相关的介绍。
注意:本篇不会深入介绍MCP相关的概念,架构等等前置内容,主要还是通过案例说明SK和MCP
Server之间的联系,建议不熟悉MCP相关内容的小伙伴,先登录MCP官网进行了解。
创建一个MCP Server
在MCP的官方介绍文档里已经有C#的官方SDK了,这里我们参照它官方的例子,先来一个MCP Server。
Tips:官方的案例已经非常简洁和完善了,建议没搞过MCP的小伙伴上手试一下,虽然案例很简单,一看就能明白,但那真正跑通的获得感,还是得自己动手试一下才能体会到。
构建服务
这一步我觉得大家还是直接看官方文档更清楚,我这里不在赘述
传送门👉:https://modelcontextprotocol.io/quickstart/server#building-your-server-5
需要注意的是,我这里的Server是使用SSE的传输方式。
目前SSE的方式官方已经声明会逐步被Streamable Http的形式替代,但目前还是Built-in状态,本地调试的话还可以使用stdio的方式,这也是Claude Desktop,Cline之类客户端工具支持的方式,这点大家按需设定即可,这部分内容可以参考这里👉:https://mcp-framework.com/docs/Transports/transports-overview。
编写Tool
定义一个class,然后标记上MCPServer的特定属性,这部分官网也有介绍,我就直接上代码了
[McpServerToolType]
public static class WeatherTools
{[McpServerTool(Name = "GetWeather"), Description("获取当前城市的天气")]public static async Task<string> GetWeather(HttpClient client,[Description("中国的城市编码adcode")] string adcode){if (string.IsNullOrEmpty(adcode)){return "adcode不能为空";}string gdKey = ConfigHelper.GetAppSetting("GaoDeKey");var jsonElement = await client.GetFromJsonAsync<JsonElement>($"/v3/weather/weatherInfo?key={gdKey}&city={adcode}&extensions=base");var lives = jsonElement.GetProperty("lives").EnumerateArray();if (!lives.Any()){return "当前城市天气获取失败";}return string.Join("\n--\n",lives.Select(live =>{var city = $"{live.GetProperty("province").GetString()}--{live.GetProperty("city").GetString()}";var weather = live.GetProperty("weather").GetString();var temperature = live.GetProperty("temperature").GetString();var windPower = live.GetProperty("windpower").GetString();var humidity = live.GetProperty("humidity").GetString();return $"城市:{city}\n天气:{weather}\n温度:{temperature}°C\n风力:{windPower}级\n湿度:{humidity}%";}));}
}
我这里,没有使用官方案例里的天气接口,而是改成了高德的天气接口,因为一会儿还要演示一下SK调用MCP Server的能力,除了调用本地的Server,高德还有一个云端的MCP Server,非常好用,稍后一并介绍一下,正好就连天气接口也改成高德的。
编写完成后,启动我们的Server服务。
dotnet run
验证
Tool编写完成后,可以先使用一些软件或者工具类的MCP Client验证一下,开发阶段,这些工具还是非常有必要的,它的角色定位就像我们用到的数据库管理工具,比如SSMS,Pg Admin,DBeaver等。
我这里使用的是官方的MCP Inspector,本地只要有node和npx环境即可。
另外,因为要经常测试一些Server,建议把Python和Python包管理工具uv也安装一下。
然后,我们启动Inspector
npx @modelcontextprotocol/inspector node build/index.js
启动之后,控制台会监听一个端口,然后在浏览器打开,然后配置好我们的Server地址,如下图
获取到所有的Tool之后,测试验证一下我们刚完成的天气接口是否生效。
至此,验证工作完成,说明我们的MCP Server是可以正常工作的,接下来就是接入实际的业务系统,来调用这个Server提供的能力了。
在业务系统创建MCP Client
注入SK服务
这部分略过,在前面的系列文章里已经写过了,或者大家也可以直接查看微软的官方文档,这里不再赘述。
编写插件
这里呢,因为我在之前的项目里,已经开始使用SK框架了,并且完成了部分的Agent功能,都是以Plugin的方式注入到系统里的,这里的演示也就暂时以这种方式来接入,后续再根据实际情况调整。
插件的代码如下
[KernelFunction("call_weather_api")]
[Description("通过地理编码,获取天气信息")]
[return: Description("如果运行正常,返回编号所属地址的天气详情")]
public async Task<string> CallWeatherApi(string adcode)
{Logger.Debug("--------天气插件正确执行---------------");var defaultOptions = new McpClientOptions{ClientInfo = new() { Name = "SK", Version = "1.0.0" }};var defaultConfig = new SseClientTransportOptions{Endpoint = new Uri($"http://localhost:5001/sse"),Name = "Magic.Services.MCPServer",};await using var client = await McpClientFactory.CreateAsync(new SseClientTransport(defaultConfig),defaultOptions);var result = await client.CallToolAsync("GetWeather", new Dictionary<string, object?>{{ "adcode",adcode}});return JsonHelper.JsonSerialize(result);
}
调用
我这里是在Web系统里进行的演示,所以以接口形式来调用插件,代码如下
public async Task<IActionResult> CallLocalServer(string adcode)
{_kernel.Plugins.AddFromType<LocalServer>("LocalServer", _serviceProvider);// 获取聊天完成服务var chatCompletionService = _kernel.GetRequiredService<IChatCompletionService>();// 启用自动函数调用OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new(){ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,//FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()};PromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };ChatHistory chatHistory = [];chatHistory.AddSystemMessage($"你是一个天气预报员,本函数的入参会给你一个中国地理位置编码,你要调用合适的MCP Server来完成天气预报。");chatHistory.AddUserMessage($"查询地理编码为【{adcode}】的地点天气");var chatResult = await chatCompletionService.GetChatMessageContentAsync(chatHistory,openAIPromptExecutionSettings,_kernel);Console.Write($"\nAssistant : {chatResult}\n");return Json(chatResult);
}
验证
为了方便验证,可以先把接口的访问等级降低,直接通过URL地址访问,效果如下
控制台打印的信息如下
至此,我们已经在本地创建了一个MCPServer,并通过MCP Inspector进行了验证,同时又在原有使用SK的系统里,通过SK成功调用了这个Server提供的tool,接入复杂度可以接受,效果也非常不错。
接下来,再试试SK能不能成功调用高德地图的MCP Server
调用高德MCP Server
前置工作
注意,如果前面的天气接口你是使用的高德的服务,那么相信你已经注册了高德的key,如果没有,这里需要去注册一下。
编写服务
由于调用的是第三方的MCP,我们这里可以直接在接口或者服务类里编写调用代码
public async Task<IActionResult> CallGaodeServer(string msg)
{// 第一步:创建 mcp 客户端var defaultOptions = new McpClientOptions{ClientInfo = new() { Name = "地图规划", Version = "1.0.0" }};var defaultConfig = new SseClientTransportOptions{Endpoint = new Uri(ConfigurationHelper.GetSectionValue("GaodeMCP")),Name = "Magic.Services.MCPServer",};await using var client = await McpClientFactory.CreateAsync(new SseClientTransport(defaultConfig),defaultOptions);var tools = await client.ListToolsAsync();foreach (var tool in tools){Logger.Debug($"秀一下高德的能力之--- {tool.Name}");}#pragma warning disable SKEXP0001_kernel.Plugins.AddFromFunctions("gaodemap", tools.Select(aiFunction => aiFunction.AsKernelFunction()));#pragma warning restore SKEXP0001// 获取聊天完成服务var chatCompletionService = _kernel.GetRequiredService<IChatCompletionService>();// 启用自动函数调用OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new(){ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,};PromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };ChatHistory chatHistory = [];chatHistory.AddUserMessage(msg);var chatResult = await chatCompletionService.GetChatMessageContentAsync(chatHistory,openAIPromptExecutionSettings,_kernel);Console.Write($"\nAssistant : {chatResult}\n");return Json(chatResult);
}
验证
验证工作,还是和前面一样,暂时在浏览器里直接访问即可。
控制台打印的信息更友好一点
结束语
好了,至此,我们成功在原有的系统上,使用SK框架充当MCP Client的角色,完成了本地MCP Server的调用和第三方MCP Server的调用目标。
最后,再推荐几个介绍MCP的参考站点
- 中文官网:https://mcp-docs.cn/
- MCP.so:https://mcp.so/zh
- 痴者工良的博客:https://www.cnblogs.com/whuanle/p/18837493
- Semantic Kernel的学习文档:https://learn.microsoft.com/zh-cn/semantic-kernel/
- 本人 之前写的两篇介绍SK的博客,本文的代码部分缺失很多初始化的代码,均在这两篇当中https://juejin.cn/post/7460393309552164902,https://juejin.cn/post/7463301527991762955