文章摘要

时光飞逝,转眼间已步入阳春三月,可我却迟迟未曾动笔写下 2025 年的第一篇 AI 博客。不知大家心中作何感想,从年初 DeepSeek 的爆火出圈,到近期 Manus 的刷屏热议,AI 领域的发展可谓是日新月异。例如,DeepSeek R1 的出现,让人们开始接受慢思考,可我们同样注意到,OpenAI 的 Deep Research 选择了一条和 R1 截然不同的路线,模型与智能体之间的界限开始变得模糊。对于这一点,使用过 Cursor Composer 或者 Deep Research 的朋友,相信你们会有更深刻的感悟。有人说,Agent 会成为 2025 年的 AI 主旋律。我不知道大家是否清楚 AutoGPT 与 Manus 的差别,对我个人而言,最重要的事情是在喧嚣过后找到 “值得亲手去做的事情”。所以,今天这篇博客,我想分享一个 “熟悉而陌生” 的东西:MCP,即:模型上下文协议,并尝试将这个协议和 Semantic Kernel 连接起来。

MCP 介绍

[TL;DR] MCP 是由 Anthropic 设计的开放协议,其定位类似于 AI 领域的 USB 接口,旨在通过统一接口解决大模型连接不同数据源和工具的问题。该协议通过 JSON-RPC 规范定义了 Prompt 管理资源访问工具调用三大核心能力,使得任何支持 Function Calling 的模型都能无缝对接外部系统,从而帮助大语言模型实现 “万物互联”。

什么是 MCP?

MCP(Model Context Protocol)是由 Anthropic 设计的一种开放协议,旨在标准化应用程序向大语言模型(LLMs)提供上下文的方式,使大模型能够以统一的方法连接各种数据源和工具。你可以将其理解为 AI 应用的 USB 接口,为 AI 模型连接到不同的数据源和工具提供了标准化的方法。架构设计上,MCP 采用了经典的 C/S 架构,客户端可以使用该协议灵活地连接多个 MCP Server,从而获取丰富的数据和功能支持,如下图所示:

MCP 基本架构 MCP 基本架构

具体而言,MCP 架构中包括四个核心角色:

  • MCP Host:承载用户交互的终端,如 Claude DesktopCusrorVSCode 等,负责发起请求
  • MCP Client:协议客户端,负责建立、维护与服务器端的一对一连接,通常需要集成 SDK 到 MCP Host
  • MCP Server: 协议服务器端,对外暴露三种核心能力:Prompts、Resources 和 Tools
  • Data Source:数据源,是本地资源(如 SQLite、文件系统)与远程服务(如 Github API)的集合

为什么选择 MCP?

在过去的这一年里,AI 智能体的技术生态逐渐呈现出两种典型的演进方向。首先,是以 LangChainSemantic Kernel 等为代表的 AI 框架;其次,是以 DifyCoze 等为代表的智能体编排平台。这实际上揭示了当前智能体技术发展的双重路径,即:人们正试图从框架层和平台层两个维度去攻克 Agent 技术的高峰

为什么选择 MCP? 为什么选择 MCP?

然而,当你真正地深入实践这一切的时候,你会在这些框架和平台中发现许多痛点。例如:

  • 语言框架的割裂性:不同技术栈中对 Agent 基础元素的定义存在着根本性差异。例如,LangChain 中采用 Python 的 @tool 装饰器来标注工具方法,而 C# 系列的 Semantic Kernel 则通过 [KernelFunction] 特性来实现功能注册。这种语法层面的分歧,无形中增加了跨平台协作的成本。
  • 平台生态的封闭性:以 Coze 和 Dify 的插件系统为例,虽然二者均支持集成 Jina AI 插件,但是其工作流编排和配置的规范完全不同。这种生态壁垒加剧了不同技术体系间的 “数字鸿沟”,造成应用迁移成本过高,最终导致智能体平台沦为信息孤岛。
  • 开发资源的重复消耗:目前,无论是服务供应商还是开发者,均需要参与智能体平台的适配工作,容易造成重复性工作,这对于 AI 时代而言是一种注意力的浪费。更重要是,这不利于 AI 技术的进一步发展,真正具有突破性的技术创新难以获得足够关注。

如你所见,有了 MCP 以后,开发人员只需要和 MCP 打交道,这是真正意义上的 “Attention Is All You Need”。

MCP 如何工作?

现在,当我们将目光聚焦在 MCP 上面时,我们会发现情况开始有所好转,因为 Anthropic 使用 JSON-RPC 规范定义了一套与语言、平台无关的协议。在该协议中,定义了 Requests、Responses 和 Notifications 三种消息类型:

TypeDescriptionRequirements
RequestsMessages sent to initiate an operationMust include unique ID and method name
ResponsesMessages sent in reply to requestsMust include same ID as request
NotificationsOne-way messages with no replyMust not include an ID

在上文中我们提到,MCP 支持 Prompts、Resources 和 Tools 三大核心能力。以 Tools 这个最常见的能力为例,MCP 支持工具的发现、调用和更新,其交互过程通常如下图所示:

MCP-Tools 能力示意图 MCP-Tools 能力示意图

此时,我们会注意到,MCP 针对工具调用主要提供了三个 API:tools/listtools/call 以及 notifications/tools/list_changed。其中,notifications/tools/list_changed 是可选的,属于 Notification 的一部分。顾名思义,当服务器端提供的工具列表发生变化时,它能够以通知的形式告知客户端这一变化。如果你熟悉 JSON-RPC 规范,相信你已经在脑海中推测出具体的消息结构。首先,客户端通过 tools/list 方法向服务器端发起请求:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "cursor": "optional-cursor-value"
  }
}

接下来,服务器端会返回它目前支持的工具列表。这里,我们以经典的 get_weather 方法为例:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [{
    "name": "get_weather",
    "description": "get current weather information for a location",
    "inputSchema": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "city name or zip code"
        }
      },
      "required": ["location"]
    }
  }],
  "nextCursor": "next-page-cursor"
  }
}

如果你接触过 ReAct、Tool Use、Function Calling 这些概念,你会发现这一切是如此地熟悉和亲切。当我们将这些工具提供给 LLM 以后,由 LLM 决定是否要调用指定的工具。此时,我们可以通过 tools/call 方法来调用指定的工具。这里,同样以经典的 get_weather 方法为例:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "New York"
    }
  }
}

此时,我们会收到服务器端的响应消息,如下所示:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
      }
    ],
    "isError": false
  }
}

读到这里,诸位看官心里一定在吐槽:有没有搞错,就这?这好像和 Function Calling 没什么区别嘛!我的理解是,MCP、Function Calling 和 Agent 本质上是一个层层递进的关系,MCP 提供一种与模型、语言、框架无关的工具抽象,任何支持 Function Calling 的模型都可以调用这些工具,而 Agent 框架则在此基础上对工具进行规划与编排。

MCP、Function Calling 和 Agent 三者间的联系 MCP、Function Calling 和 Agent 三者间的联系

例如,去年年底的时候,Anthropic 和智谱相继发布了 Cumputer Use 功能,可这些功能大多都仅限于在厂商自家的产品中使用。如果你想在国内的 DeekSeek 或者 Kimi 上面尝试,基本上是痴心妄想。可有了 MCP 以后,情况就大不相同。你只需要使用 Playwright MCP Server 或者 Browser-Use MCP Server 便可以轻松 “尝鲜”。这次 Manus 爆火后,社区在几个小时内迅速复刻出了OpenManus,这与该团队直接使用第三方库 browser-use 息息相关。由此可见,一个健康、开放的 AI 生态会极大地促进 AI 应用的繁荣。事实上,自去年 MCP 发布以来,社区里涌现出了大量的第三方 MCP 服务器,这些服务器极大地扩展了 AI 的能力边界。现在,AI 可以连接到 Notion、Slack、Github、Elasticsearch 等众多平台,如下图所示,mcpservers.orgmcp.so 等网站收录了许多 MCP Server:

Awesome MCP Servers Awesome MCP Servers

所以,我们为什么要了解 MCP 呢?因为只要接入了 MCP, 便可以拥抱 MCP 背后的整个生态,这意味着 AI 领域的 “万物互联” 时刻已悄然到来。唯一的问题在于,国内外的 AI 厂商是否有意愿一起将 MCP 发展为行业标准。我想,届时无论是服务供应商还是个人开发者,都能从 MCP 这个协议中受益。除了 Tools,MCP 还支持 ResourcesPrompts 相关的功能,它们负责对提示词、文件等进行管理。当然,这些并不是本文关注的重点,这里不再赘述。我们只需要知道一件事情,对一个 MCP Server 而言,最重要的是实现 tools/listtools/call 这两个方法。目前,官方 SDK 支持 Python、TypeScript、Java 和 Kotlin 这四种语言,我们可以使用这些 SDK 来集成或者开发一个 MCP Server。下面是一个 Python 版本的 SQLite Explorer 示例:

from mcp.server.fastmcp import FastMCP
import sqlite3

mcp = FastMCP("SQLite Explorer")

@mcp.resource("schema://main")
def get_schema() -> str:
    """Provide the database schema as a resource"""
    conn = sqlite3.connect("database.db")
    schema = conn.execute(
        "SELECT sql FROM sqlite_master WHERE type='table'"
    ).fetchall()
    return "\n".join(sql[0] for sql in schema if sql[0])

@mcp.tool()
def query_data(sql: str) -> str:
    """Execute SQL queries safely"""
    conn = sqlite3.connect("database.db")
    try:
        result = conn.execute(sql).fetchall()
        return "\n".join(str(row) for row in result)
    except Exception as e:
        return f"Error: {str(e)}"

如你所见,在该示例中,MCP Server 提供了一个 resource、一个 tool,前者负责返回当前数据库中的 DDL,后者提供一个查询数据的方法。恭喜你,现在你可以开始着手设计一个针对 Text2SQL 的 Agent 了。

MCP 的局限性

当然,我们需要学会辩证地看待事物,MCP 并非完美无瑕。首先,我们不清楚国内外厂商适配这一协议的热情到底有多少;其次,类似于大多数 AI 框架,MCP 正处在迅速发展阶段,该协议的最新版本是 2024-11-05。截止目前,官方在 2025 年上半年的 Roadmap 主要集中在:认证/授权、服务发现、无状态操作。所以,未来走向到底如何,着实充满了变数。例如,按照官方的设计,MCP 在传输层(Transports)支持 stdioHTTP with Server-Sent Events (SSE),可目前大多数的 MCP Server 都是运行在本地的 stdio。对于终端用户而言,使用 MCP 依然需要了解 Python、Node.js 甚至 Docker,不得不说,这其实是一种隐形的成本。

Semantic Kernel x MCP

Semantic Kernel 集成 MCP 流程示意图 Semantic Kernel 集成 MCP 流程示意图

在 Semantic Kernel 中,我们使用插件(Plugin)这个概念来描述一组工具,而每个工具则是一个 KernelFunction。因此,如果希望在 Semantic Kernel 中集成 MCP,本质上就是将 MCP 中的 Tools 转换为 Semantic Kernel 中的 KernelFunction。如上图所示,我们将在 Semantic Kernel 中集成一个 MCP 客户端,然后利用 tools/listtools/call 这两个 API 分别实现工具获取、工具调用这两个流程。

工具获取

截止目前,MCP 官方还没有提供对 .NET 的支持,不过社区里还是出现了第三方实现。例如:

博主这里选择的是 mcpdotnet,假设我们希望在 Semantic Kernel 中集成 Playwright MCP Server。此时,我们可以编写下面的代码来连接到对应的 MCP Server:

var clientOptions = new McpClientOptions()
{
  ClientInfo = new McpDotNet.Protocol.Types.Implementation() 
  { 
    Name = name, Version = "1.0.0" 
  },
};

var serverConfig = new McpServerConfig()
{
  Id = "playwright",
  Name = "playwright",
  TransportType = "stdio",
  TransportOptions = new Dictionary<string, string>
  {
    ["command"] = "npx",
    ["arguments"] = "-y @executeautomation/playwright-mcp-server",
  }
};

var loggerFactory = kernel.Services.GetRequiredService<ILoggerFactory>();
var clientFactory = new McpClientFactory(
  [serverConfig], 
  clientOptions, 
  NullLoggerFactory.Instance
);

var client = await clientFactory.GetClientAsync(serverConfig.Id).ConfigureAwait(false);

clientFactory 获取 IMcpClient 实例时,客户端会先调用 initialize() 方法。服务器端初始化完成后,会给客户端发送 notifications/initialized 通知,表明服务器端已完成初始化。此时,可调用 ListToolsAsync() 方法,获取 MCP 服务器端提供的工具列表:

var listToolsResult = await client.ListToolsAsync().ConfigureAwait(false);
var tools = listToolsResult.Tools;

协议转换

从前文中可知,MCP 使用 JSONSchema 来描述工具的输入参数,返回值则被定义为一个数组,如下图所示:

MCP 工具调用输入 &amp; 输出 MCP 工具调用输入 &amp; 输出

因此,我们需要写一个中间层,将 MCP 的工具转换为 KernelFunction,这部分内容非常简单,不再赘述:

// 将 MCP 中的 Tool 转换为 KernelFunction
private static KernelFunction ToKernelFunction(this Tool tool, IMcpClient client) {
  async Task<string> InvokeToolAsync(
    Kernel kernel, 
    KernelFunction function, 
    KernelArguments arguments, 
    CancellationToken cancellationToken
  ) {
    try {
      var mcpArguments = new Dictionary<string, object>();
      foreach (var arg in arguments) {
        if (arg.Value is not null) 
          mcpArguments[arg.Key] = function.ToArgumentValue(arg.Key, arg.Value);
      }

      var result = await client.CallToolAsync(
        tool.Name,
        mcpArguments,
        cancellationToken: cancellationToken
      ).ConfigureAwait(false);

      return string.Join("\n", result.Content
        .Where(c => c.Type == "text")
        .Select(c => c.Text));
      } catch {
        throw;
      }
  }

  return KernelFunctionFactory.CreateFromMethod(
    method: InvokeToolAsync,
    functionName: tool.Name,
    description: tool.Description,
    parameters: ToKernelParameters(tool),
    returnParameter: ToKernelReturnParameter()
  );
}

// 将 MCP 中工具的输入转换为 KernelFunction 输入
private static List<KernelParameterMetadata> ToKernelParameters(Tool tool) {
  var inputSchema = tool.InputSchema;
  var properties = inputSchema?.Properties;
  if (properties == null) return [];

  HashSet<string> requiredProperties = new(inputSchema!.Required ?? []);
  return properties.Select(kvp => new KernelParameterMetadata(kvp.Key) {
    Description = kvp.Value.Description,
    ParameterType = ConvertParameterDataType(kvp.Value, requiredProperties.Contains(kvp.Key)),
    IsRequired = requiredProperties.Contains(kvp.Key)
  })
  .ToList();
}

// 将 JSONSchema 中的数据类型转换为 C# 的数据类型
private static Type ConvertParameterDataType(JsonSchemaProperty property, bool required) {
  var type = property.Type switch {
    "string" => typeof(string),
    "integer" => typeof(int),
    "number" => typeof(double),
    "boolean" => typeof(bool),
    "array" => typeof(List<string>),
    "object" => typeof(Dictionary<string, object>),
    _ => typeof(object)
  };

  return !required && type.IsValueType ? typeof(Nullable<>).MakeGenericType(type) : type;
}

// 转换返回值,简化处理,直接返回字符串类型
private static KernelReturnParameterMetadata? ToKernelReturnParameter() {
  return new KernelReturnParameterMetadata() {
    ParameterType = typeof(string),
  };
}

// 将 KernelFunction 参数转换为 object
private static object ToArgumentValue(this KernelFunction function, string name, object value) {
  var parameter = function.Metadata.Parameters.FirstOrDefault(p => p.Name == name);
  return parameter?.ParameterType switch
  {
    Type t when Nullable.GetUnderlyingType(t) == typeof(int) => Convert.ToInt32(value),
    Type t when Nullable.GetUnderlyingType(t) == typeof(double) => Convert.ToDouble(value),
    Type t when Nullable.GetUnderlyingType(t) == typeof(bool) => Convert.ToBoolean(value),
    Type t when t == typeof(List<string>) => (value as IEnumerable<object>)?.ToList(),
    Type t when t == typeof(Dictionary<string, object>) => (value as Dictionary<string, object>)?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
    _ => value,
  } ?? value;
}

现在,一切就变得简单了,我们可以封装一个如下的扩展方法:

public static async Task<IEnumerable<KernelFunction>> GetKernelFunctionsAsync(this IMcpClient client) {
  var listToolsResult = await client.ListToolsAsync().ConfigureAwait(false);
  return listToolsResult.Tools.Select(tool => ToKernelFunction(tool, client)).ToList();
}

工具调用

在 MCP 中,客户端调用服务器端提供的工具,可以直接使用 CallToolAsync() 方法:

var result = await client.CallToolAsync(
  tool.Name,
  mcpArguments,
  cancellationToken: cancellationToken
).ConfigureAwait(false);

当我们转换为 KernelFunction 以后,只需要调用 InvokeAsync() 方法即可调用对应的插件函数。考虑到,在 Agent 中,插件函数通常是由 LLM 来调用的,我们将编写下面的扩展方法来实现工具的注册:

// 注册 MCP Server
public static async Task AddMCPServer(
  this Kernel kernel, string name, string command, 
  string version = "1.0.0", 
  string[] args = null, 
  Dictionary<string, string> env = null
  ) {
    var clientOptions = new McpClientOptions() {
        ClientInfo = new McpDotNet.Protocol.Types.Implementation() { 
          Name = name, Version = "1.0.0" 
        },
    };

    var serverConfig = new McpServerConfig() {
        Id = name,
        Name = name,
        TransportType = "stdio",
        TransportOptions = new Dictionary<string, string> {
            ["command"] = command,
            ["arguments"] = string.Join(' ', args ?? []),
        }
    };

    var loggerFactory = kernel.Services.GetRequiredService<ILoggerFactory>();
    var clientFactory = new McpClientFactory([serverConfig], clientOptions, loggerFactory);

    var client = await clientFactory.GetClientAsync(serverConfig.Id).ConfigureAwait(false);
    var kernelFunctions = await client.GetKernelFunctionsAsync();
    kernel.Plugins.AddFromFunctions(name, kernelFunctions);
}

至此,我们便完成了 MCP 在 Semantic Kernel 中的集成。现在,你只需要使用下面的代码片段即可:

// 添加 playwright-mcp-server
await kernel.AddMCPServer(
  name: "playwright",
  command: "npx",
  version: "1.0.0",
  args: ["-y", "@executeautomation/playwright-mcp-server"],
  env: null
);

场景化效果展示

好的,当我们给 Semantic Kernel 集成 MCP 以后,现在我们来一起看看它具体能帮我们做什么事情?

操作浏览器

如图所示,用户请求:打开 Bing 主页,搜索 “Model Context Protocol

MCP-操作浏览器-A MCP-操作浏览器-A

MCP-操作浏览器-B MCP-操作浏览器-B

访问文件系统

如图所示,用户请求:我在 D:\Projects\2024 这个 Git 仓库中都提交过那些代码,最近的一次的更新是什么?

MCP-访问文件、读取 Git 提交记录 MCP-访问文件、读取 Git 提交记录

读取 Github 仓库

如图所示,用户请求:阅读 OpenManus 仓库的代码,帮我分析其架构、设计相关的细节

MCP-读取 Github 仓库 MCP-读取 Github 仓库

博主先后体验了四个 MCP Server。其中,Knowledge Graph Memory Server 可以在本地构建知识图谱,从对话中提取并持久化三元组,实现长期记忆。当然,这里的关键在于,确定三元组的读写时机。今年,我准备将现有 RAG 和 Agent 融合,升级为 Agentic RAG。目前,考虑的是将 ReAct 模式应用于 RAG,显然,这里同样会遇到一个问题,即: LLM 如何判定上下文足以生成最终答案。因篇幅限制,此处不再展示更多截图,一切都需要大家去亲自体验。

本文小结

MCP 是 Anthropic 设计的开放协议,其定位类似于 AI 领域的 USB 接口,希望通过统一接口解决大模型连接不同数据源和工具的问题。Semantic Kernel 是微软开源的 Agent 框架,两者的结合可以让 .NET 开发者快速、高效地接入社区中的 MCP 服务器,减少重复性的平台对接工作。除工具调用外,还可以考虑将项目中的提示词模板统一放置到 MCP Server 上管理。博主撰写此文时,网络正在传播着被破解的 Manus 源代码。虽然经常有人说提示工程已不存在了,可在实际的项目中提示词依旧不可或缺。从这个角度来看,尽管提示词技术含量不高,但若能得到妥善管理,至少会比项目被破解、提示词被泄露更显体面。关于更多 MCP 的细节,请参考官方文档:Introduction - Model Context Protocol ,无论你是开发者还是普通用户,相信都能在那里找到答案。

参考链接

赞赏博主
相关推荐 随便逛逛
Semantic Kernel 视角下的 Text2SQL 实践与思考 本文深入探讨了人工智能领域的最新进展,特别是大型语言模型(LLMs)的应用及其与检索增强生成(RAG)技术的结合。文章首先引用《诗经》中的名句,比喻技术的快速发展,随后讨论了 Agent 的概念,强调了其规划、记忆和工具使用的能力。作者分享了对市场上主流 Agent 产品的体验,并提出了对大模型动态规划任务的信念。文中还介绍了 Text2SQL 技术,展示了如何将自然语言转化为 SQL 语句,以及这一技术如何帮助大模型与关系型数据库连接。最后,作者反思了技术进步对人类社会的影响,特别是在效率提升与人类幸福感之间的关系。
基于 K-Means 聚类分析实现人脸照片的快速分类 本文介绍了使用 K-Means 聚类算法对人脸照片进行自动分类的方法,解决了 “脸盲症” 问题。通过 Dlib 提取人脸特征向量,并利用 Scikit-Learn 的 K-Means 聚类分析,能够快速将大量人脸照片按人物进行分类。文章详细讲解了 K-Means 算法的原理、K 值确定方法,以及如何使用 PCA 降维和 Matplotlib 可视化聚类结果。该技术方案帮助用户高效地整理和管理照片,避免了人工分类的繁琐过程。尽管 K-Means 算法简单高效,但其对簇数和初始中心的选择较为敏感,可能不适用于噪声数据和非凸形簇,建议在实际应用中结合 DBSCAN 等算法以提高聚类效果。
评论 隐私政策