AI 工程化实战:从零手搓代码,这一次彻底搞懂MCP!

本文深入剖析MCP(Model Context Protocol,模型上下文协议)的底层运作机制、核心价值与完整实现方案,帮助你彻底理解这一AI时代的"Type-C 接口",掌握从零构建MCP客户端和服务端的能力。

一、为什么需要 MCP?

在深入 MCP 之前,我们需要先回顾其技术前置------Function Calling(工具调用)。如果你对 Function Calling 的底层原理感兴趣,可以参考本系列的前一篇文章。

我们知道,LLM 本质上是一个运行在受限计算环境中的『概率预测引擎』 ,核心工作机制是『文字接龙』,这决定了它具备极强的意图识别能力,却无法直接操作现实世界的工具。

为了打破这个限制,Function Calling 应运而生,它的基本逻辑为:

  • 大模型负责识别用户意图,基于现有的工具列表,决定是否使用工具,使用哪个工具及参数,最终输出一段符合规范的 JSON Schema 指令。
  • 应用程序负责向模型声明具备哪些工具及参数(JSON Schema);在收到大模型的工具调用指令后,去执行真实的业务逻辑(如 SQL 查询、 HTTP 请求等),并将执行结果反馈给模型。

这一流程解决了基本的工具调用需求,但随着 AI 应用复杂度的提升,逐渐暴露出以下工程痛点:

  • 接入成本高 :工具的具体使用方法被耦合在应用程序中,导致:
    • 每接入一个新工具或增加新参数,开发者都需要在业务代码中手动编写详尽的 JSON Schema 描述。当工具库日益庞大时,维护这些描述的质量将耗费大量精力。
    • 由于缺乏统一的行业标准,最熟悉工具用法的提供方无法直接提供一份「自描述」规范,接入方必须反复查阅文档并进行二次封装,造成了极大的心智负担。
  • 生态系统割裂:每一个应用开发者在接入第三方工具时,都必须从头编写一套属于自己的适配代码,形成了严重的工程重复。

为了解决上述痛点,Model Context Protocol(MCP) 应运而生。

MCP 的出现,就像在混乱的充电接口世界中引入了 USB-C,或者在计算机网络中引入了 TCP/IP 协议。
它定义了一套通用的、与模型无关的通信协议,让应用程序和底层工具实现了彻底解耦。

引入 MCP 之后,维护工具 Schema 的『脏活累活』被交还给了最熟悉它的工具提供方。AI 应用程序只需要进行简单配置,就能以标准化的方式获取工具描述并发起调用。这样存在以下优势:

  • 一次开发,到处可用:工具提供方只需实现一次 MCP 协议,所有支持该协议的 AI 应用都能直接接入。工具的『使用说明书』由提供方编写和维护,AI 应用只需按照协议进行订阅,极大地降低了集成门槛。
  • 架构解耦合:应用程序与第三方工具不再硬编码绑定。业务逻辑被封装在独立的 Server 进程中,开发者可以像拼乐高积木一样,自由组合来自不同供应商的 MCP 服务,各组件独立演进,互不干扰。
  • 动态发现与按需加载 :客户端通过标准接口实时获取 Server 端的工具能力。开发者只需修改配置文件指向不同的 MCP Server 地址,即可实现工具的动态拔插,无需修改核心业务代码。

💡 经典误区纠正

很多初学者会认为 MCP 是更高级的 Function Calling,甚至已经取代了后者。实际上并非如此,MCP 只是标准化了 Function Calling 的工程接入路径!这两者有着明确的边界分工:

  • Function Calling 解决的是 ++AI应用程序++ 与++大模型++的交互逻辑。大模型通过训练,学会了如何返回标准指令,告知应用程序该调用谁。
  • MCP 解决的是 ++AI应用程序++ 与++第三方工具++的交互逻辑。有哪些工具、该怎么调用,统一由第三方通过 MCP Server 声明,应用程序只负责按协议路由。

二、MCP 是什么?

前一节我们探讨了为什么需要 MCP,本节将深入剖析 MCP 的具体细节。

1. 核心架构

一个完整的 MCP 架构由三个核心角色组成:

  1. MCP Host(宿主) :发起请求的 AI 应用程序(如 Claude Desktop、Cursor 或企业自研的 AI 平台)。它不直接处理业务逻辑,而是负责与大模型对话,并将意图转化为标准指令下发给 Client。
  2. MCP Client(客户端): 运行在 Host 内部的通信组件,专门负责与外部 Server 建立标准连接。它将 Host 的指令翻译成 MCP 标准 JSON 报文,维持连接并处理数据格式转换。
  3. MCP Server(服务端) :真正触达数据与逻辑的底层程序。它可以是连接企业数据库的 Java 服务,也可以是操作本地文件系统的 Python 脚本。它向外声明工具集并执行具体动作(SQL 查询、API 调用等)。

2. 传输通道

为了适配不同的工程场景,MCP 设计了两套精妙的传输通道(Transport):

++🚄 通道 A:Stdio++

在本地 IDE(如 Cursor 或 Trae)中使用 MCP 时,通常采用 Stdio (标准输入输出) 通道。

  • 运行机制 : Host 会以子进程的形式启动 Server,通过操作系统的标准输入 sys.stdin 向 Server 发送 JSON 报文;Server 处理完后,将结果写入标准输出 sys.stdout供 Host 读取。

  • 核心优势:极度轻量,高性能,无需占用网络端口,且 Server 进程的生命周期与 Host 完全绑定,随用随起,用完即毁。

++🚀通道 A:Streamable HTTP++

当需要将 MCP Server 部署在云端(如企业内部的知识库服务)时,本地的 Stdio 就无能为力了,此时需要使用 Streamable HTTP。

早期 MCP 使用了双端点的 SSE(Server-Sent Events)方案,但由于其状态维持复杂,目前已被标记为废弃。取而代之的是更加优雅的 Streamable HTTP 规范。

  • 单端点统一: 所有的交互(建立流、发送请求、关闭会话)全部通过同一个 URL(例如 /mcp)完成,极大地简化了后端的路由配置和网关运维。

  • 动态协议协商: Client 在发起 POST 请求时,可以通过 Accept 头声明自己支持 application/json 还是 text/event-stream。Server 会根据请求特性智能降级:如果是普通短任务,直接返回 JSON;如果是耗时任务,则平滑升级为 SSE 流,利用分块传输推送进度。

  • Serverless 原生友好: 它采用了无状态设计,无需长连接死守,完美适配按次计费的云原生平台,支持水平扩展和自动伸缩。

3. 通信协议

在 RESTful 盛行的今天,MCP 选择了看似『复古』的 JSON-RPC 2.0 作为应用层协议。这并非随兴而为,而是基于 AI 交互本质的深度考量:

  • 契合 Function Calling 本质: 大模型指挥工具做事,本质上就是远程过程调用(RPC)。不需要 REST 复杂的资源路径(/weather/beijing)和 HTTP 动词,只需要最纯粹的:函数名 (method) + 参数 (params)
  • 天然支持异步通知 :AI 任务往往耗时较长,REST 往往需要长轮询或 Webhook 来获取进度。而 JSON-RPC 支持 Notification (通知)机制,Server 可以随时向 Client 推送进度,完美适配大模型的异步流式交互。
  • 极简的报文解析 : JSON-RPC 抛弃了复杂的 HTTP Header 和状态码体系。请求仅需 method(你要干嘛)、params(参数)id(请求唯一标识),响应只有 result (结果)和 id。这种轻量级设计确保了在低带宽、高延迟环境下的高效解析。

4. 核心交互流程

我们通过标准动作来看看一次完整的 MCP 调用是如何发生的:

  1. initialize(初始化):首先 MCP Client 与 MCP Server 建立连接,类似于 TCP 的三次握手,双方交换协议版本与各自的能力(例如是否支持进度条、是否支持资源监听等)。
  2. tools/list(获取工具列表):Client 获取 Server 提供的所有工具列表。Server 此时会返回包含 name、description 和严谨的 inputSchema(参数规范)的列表。
  3. tools/call(调用工具):Host 将大模型生成的参数通过 Client 发给 Server,执行指定的工具。Server 执行真实的底层逻辑,并将结果返回,供大模型进行最终的自然语言总结。

三、MCP 实战

理论储备完毕,下面开始上实战。为了让大家彻底吃透 MCP,我们将通过三个层次的实现,层层递进地剥开它的外衣:

  1. 使用 IDE 平台,无代码体验 MCP 的强大接入能力。
  2. 手写 MCP Server(适配 stdio 和 streamable http 两种协议)。
  3. 手写 MCP Host & Client,完整理解 MCP 全貌。

1. IDE 平台的无代码接入

目前,绝大多数 AI IDE(Trae, Cursor,Claude Desktop 等 )已原生支持 MCP,可以非常简单地接入第三方能力,这里以 Trae 为例。

首先我们创建一个『傻瓜』智能体,不勾选任何平台内置工具(比如使用终端和联网搜索等)。

当我们询问『傻瓜』智能体:"当前的详细时间是什么"。它只能回答出日期(因为 Trae 默认在系统提示词中加了日期信息),但无法知道具体时间。

接下来,我们配置一个获取时间的 MCP,依次点击设置->MCP->添加,Trae 提供了两种添加模式:

  • 市场添加: Trae 提供了社区中热门的 MCP Server。
  • 手动添加:如果在市场中无法找到想要的 MCP Server,或者想使用自己开发的 MCP Server,则需要手动添加。

我们从选择从市场添加,找到Time这一个 MCP Server,它提供了查询时间的能力。

点击右侧的 + 号,会弹窗展示如下配置信息,点击确认即可添加成功。

json 复制代码
{
  "mcpServers": {
    "Time": {
      "command": "uvx",
      "args": [
        "mcp-server-time",
        "--local-timezone=Asia/Shanghai"
      ],
      "env": {}
    }
  }
}

原理解析 :这里的 uvx 是 Python 的包运行器。当你保存配置时,IDE 会在后台拉起一个 mcp-server-time 的子进程(子进程实际执行了 uvx mcp-server-time --local-timezone=Asia/Shanghai),随后 IDE 的 Client 组件通过 Stdio(标准输入输出) 与该进程进行 JSON-RPC 通信。

添加完毕后,进入智能体配置页面,勾选刚刚引入的 Time 服务,赋予智能体调用该工具的权限。

我们再次询问『傻瓜』智能体:"当前的详细时间是什么",它检测到有 MCP 工具可以获取到时间,主动向我们发起工具调用请求。允许运行后,『傻瓜』智能体成功获取了当前时间。

2. 自定义 MCP Server

当现成工具无法满足业务需求时(例如查询公司私有数据库),我们就需要动手写一个 Server。

业务场景假设:当用户问起某个员工的所在部门/薪资时,能自动去公司数据库里查出来并回答。

我们将实现两个独立的 MCP Server,借此演示 MCP 支持的两种核心传输协议:

  1. 员工部门查询助手 ------ 使用 stdio 协议(适合本地同机进程级通信)。
  2. 员工薪资查询助手 ------ 使用 Streamable HTTP 协议(适合跨网络、微服务架构)。

(1)stdio 传输

编写 mcp_server_stdio.py,这里使用官方提供的 MCP SDK,能利用 Python 的类型提示自动将函数签名转化为大模型能精准解析的 JSON Schema。

python 复制代码
from mcp.server.fastmcp import FastMCP

# 初始化一个名为 "员工部门查询助手" 的 MCP Server
mcp = FastMCP("员工部门查询助手")

# 模拟一个后端私有数据库
MOCK_DB = {
    "张三": "财务部",
    "李四": "技术部"
}
"""
核心:使用@mcp.tool装饰器暴露能力,sdk 帮我们封装了 tools/list、tools/call 等底层 JSON-RPC 交互。
    1. 通过反射获取函数签名和类型提示
    2. 解析 Docstring 生成描述,供 LLM 理解。
    3. 自动生成符合 JSON Schema 规范的工具定义
    4. 处理 JSON-RPC 请求并将参数传递给函数
    5. 将函数返回值封装成 JSON-RPC 响应
"""
@mcp.tool()
def get_employee_department(name: str) -> str:
    """
    查询企业内部员工的部门信息。
    Args:
        name (str): 员工姓名。
    Returns:
       员工部门信息。
    """
    return MOCK_DB.get(name, "无此员工")

if __name__ == "__main__":
    # 使用stdio传输方式
    print("MCP Server (stdio模式) 已启动,等待JSON RPC请求...")
    mcp.run(transport="stdio") 

接着在 Trae 中手动添加这个 mcp,添加的 JSON 内容为:

json 复制代码
{
  "mcpServers": {
    "get_employee_department": {
      "command": "uv",
      "args": [
       	"run",
        "{mcp_server_stdio.py所在的绝对路径}"
      ]
    }
  }
}

配置完毕后,将其应用到『傻瓜』智能体,并询问:"张三的部门是什么",得到回复:"张三的部门是财务部"。

(2)Streamable HTTP 传输

编写 mcp_server_streamable_http.py。这里需要使用 uvicorn 将 MCP 服务暴露为 Web 端口。

python 复制代码
from mcp.server.fastmcp import FastMCP
import uvicorn

# 初始化一个名为"员工薪资查询助手"的MCP Server
mcp = FastMCP("员工薪资查询助手")

# 模拟一个后端私有数据库
MOCK_DB = {
    "张三": "5000元",
    "李四": "6000元"
}

@mcp.tool()
def get_employee_salary(name: str) -> str:
    """
    查询企业内部员工的薪资信息。
    Args:
        name (str): 员工姓名。
    Returns:
       员工薪资信息。
    """
    return MOCK_DB.get(name, "无此员工")


if __name__ == "__main__":
    # 使用uvicorn将MCP挂载为标准的ASGI Web服务,对外暴露单端点,支持现代HTTP路由
    print("MCP Server (streamable-http模式) 已启动,等待JSON RPC请求...")
    uvicorn.run(mcp.streamable_http_app, host="0.0.0.0", port=8000)

接着需要启动服务: uv run mcp_server_streamable_http.py

之后在 Trae 中手动添加这个 mcp,添加的 JSON 内容为:

json 复制代码
{
  "mcpServers": {
    "get_employee_salary": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}

配置完毕后,将其应用到『傻瓜』智能体,并询问:"张三的薪资是多少",得到回复:"张三的薪资是5000元"。

3. 自定义 MCP Host & Client

如果要自研 AI 平台,不仅要写 Server,还要写 Host(宿主)和 Client(客户端)来驱动整个流程。

以下是一个完整的全链路逻辑演示:从启动读取配置、建立握手、工具发现,一直到大模型决策拦截与真正的动态调用执行。

首先,定义一个配置文件:mcp_config.json,用于添加 MCP Server。

python 复制代码
{
  "mcpServers": {
    "get_employee_department": {
      "command": "uv",
      "args": [
       	"run",
        "{mcp_server_stdio.py所在的绝对路径}"
      ]
    },
    "get_employee_salary": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}

编写 mcp_client.py,负责与 mcp server 交互:

python 复制代码
from typing import Dict, Any
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamable_http_client

class MCPClient:
    def __init__(self, name: str, config: Dict[str, Any]):
        self.name = name
        self.config = config
        self.session = None
        self._client_ctx = None 

    async def connect(self) -> None:
        """连接到 MCP Server"""
        if "url" in self.config:   # streamable http模式
            self._client_ctx = streamable_http_client(url=self.config["url"])
            read, write, _ = await self._client_ctx.__aenter__()
        elif "command" in self.config : # stdio模式
            params = StdioServerParameters(
                command=self.config["command"],
                args=self.config.get("args", [])
            )
            self._client_ctx = stdio_client(params)
            read, write = await self._client_ctx.__aenter__()
        else:
            raise ValueError(f"[{self.name}] 配置错误:必须指定 url 或 command")

        self.session = ClientSession(read, write)
        await self.session.__aenter__()
        await self.session.initialize()

    async def list_tools(self) -> Dict[str, Any]:
        """获取工具清单"""
        response = await self.session.list_tools()
        return {tool.name: tool for tool in response.tools}

    async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> str:
        """执行工具调用"""
        result = await self.session.call_tool(tool_name, arguments)
        return result.content[0].text if result.content else ""
   
    async def close(self) -> None:
        """关闭连接 - 必须在程序退出前调用"""
        if self.session:
            await self.session.__aexit__(None, None, None)
        if self._client_ctx:
             await self._client_ctx.__aexit__(None, None, None)

编写 mcp_host.py ,串联整个流程。

python 复制代码
import asyncio
import json
from typing import Dict, Any, List
from volcenginesdkarkruntime import Ark
from mcp_client import MCPClient

class MCPHost:
    """MCP Host - 批量管理 Client"""
    def __init__(self, config_file: str):
        self.clients = {}
        # 读取配置并初始化 Client
        with open(config_file, 'r') as f:
            config = json.load(f)
        for name, server_config in config["mcpServers"].items():
            self.clients[name] = MCPClient(name, server_config)

    async def start(self):
        """启动所有 Client"""
        tasks = [client.connect() for client in self.clients.values()]
        await asyncio.gather(*tasks)
        
    async def close(self):
        """关闭所有 Client"""
        tasks = [client.close() for client in self.clients.values()]
        await asyncio.gather(*tasks)

    async def get_all_tools(self):
        """获取所有工具"""
        all_tools = {}
        for name, client in self.clients.items():
            all_tools[name] = await client.list_tools()
        return all_tools

    async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]):
        """调用指定 Server 的工具"""
        return await self.clients[server_name].call_tool(tool_name, arguments)

    def get_tools_for_llm(self, all_tools: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
        """转换为 LLM 格式"""
        return [{
            "type": "function",
            "function": {
                "name": tool_name,
                "description": tool.description,
                "parameters": tool.inputSchema
            }
        } for tools in all_tools.values() for tool_name, tool in tools.items()]


async def run_llm_with_mcp(user_query: str):
    """运行 LLM + MCP - 支持多轮工具调用"""
    # 初始化 LLM,这里使用方舟的豆包模型,使用其他模型基本逻辑一致
    client = Ark(
        base_url="https://ark.cn-beijing.volces.com/api/v3",
        api_key="你的 api key",
    )
    MODEL_NAME = "doubao-seed-1-8-251228"
    # 初始化 Host
    host = MCPHost("mcp_config.json")
    await host.start()
    # 获取工具
    all_tools = await host.get_all_tools()
    tools_for_llm = host.get_tools_for_llm(all_tools)
    # 初始化消息历史
    messages = [{"role": "user", "content": user_query}]
    # 工具调用循环
    tool_call_count = 0
    while tool_call_count < 5:
        # 调用 LLM
        response = client.chat.completions.create(
            model=MODEL_NAME,
            messages=messages,
            tools=tools_for_llm
        )
        response_message = response.choices[0].message
        # 如果没有工具调用,退出循环
        if not response_message.tool_calls:
            print(f"\n💡 最终回答: {response_message.content}")
            break
        # 添加 LLM 响应到消息历史
        messages.append(response_message.model_dump())
        # 处理工具调用
        for tool_call in response_message.tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)
            # 查找 Server
            server_name = next(
                (s for s, tools in all_tools.items() if function_name in tools),
                None
            )
            if server_name is None:
                result = f"错误: 未找到工具 {function_name}"
            else:
                # 调用工具
                tool_result = await host.call_tool(server_name, function_name, function_args)
                result = tool_result
            # 添加工具结果到消息历史
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "content": result
            })

        tool_call_count += 1
    
    # 如果达到最大调用次数,获取最终回答
    if tool_call_count >= 5:
        final_response = client.chat.completions.create(
            model=MODEL_NAME,
            messages=messages
        )
        print(f"\n💡 最终回答: {final_response.choices[0].message.content}")
    # 关闭所有连接
    await host.close()


if __name__ == "__main__":
    asyncio.run(run_llm_with_mcp("帮我查一下张三的部门和薪资"))

当我们运行 mcp_host.py 后,大模型返回:张三所在的部门是财务部,薪资为5000元。


四、总结

MCP 不仅仅是一个简单的 JSON 格式约定,它是 AI 应用迈向标准化生态互联的基石。

通过统一的上下文协议,我们彻底改变了过去大模型应用中的工具调用硬编码逻辑。

  • 工具提供方可以专注于打磨数据与执行逻辑。
  • AI 平台开发者则可以将精力聚焦于 Prompt 编排和 RAG 等核心工程链路。

前几篇文章我们介绍了大模型(LLM)提示词工程 (PE)检索增强生成(RAG)工具调用(Function Calling) ,今天则正式解锁了重要的模型上下文协议(MCP)

但这仅仅是开始。在接下来的实战系列中,我将继续带大家深度解锁更多 AI 核心知识点:工作流(Workflow)、智能体(Agent)、LangChain、Coze、Skill 等等。

如果觉得文章还不错,别忘了关注、点赞、收藏三连支持!

让我们一起持续进化,成为真正能够驾驭"赛车"的 AI 工程师。