MCP协议开发实战:从零搭建AI Agent工具链

文章目录

    • [1. 引言](#1. 引言)
    • [2. 什么是 MCP 协议](#2. 什么是 MCP 协议)
      • [2.1 一句话理解](#2.1 一句话理解)
      • [2.2 核心概念](#2.2 核心概念)
      • [2.3 三种关键能力](#2.3 三种关键能力)
    • [3. MCP 架构与传输方式](#3. MCP 架构与传输方式)
      • [3.1 Client-Server 架构](#3.1 Client-Server 架构)
      • [3.2 传输方式](#3.2 传输方式)
    • [4. 环境准备](#4. 环境准备)
      • [4.1 创建虚拟环境](#4.1 创建虚拟环境)
      • [4.2 安装依赖](#4.2 安装依赖)
    • [5. 从零构建第一个 MCP Server](#5. 从零构建第一个 MCP Server)
      • [5.1 最小示例](#5.1 最小示例)
      • [5.2 添加更多工具](#5.2 添加更多工具)
    • [6. MCP Client 开发:连接与调用](#6. MCP Client 开发:连接与调用)
    • [7. 实战:搭建 AI Agent 工具链](#7. 实战:搭建 AI Agent 工具链)
      • [7.1 Agent 主循环](#7.1 Agent 主循环)
      • [7.2 关键设计点](#7.2 关键设计点)
    • [8. 使用 MCP Inspector 调试](#8. 使用 MCP Inspector 调试)
    • [9. 进阶:拆分为模块化工具链](#9. 进阶:拆分为模块化工具链)
    • [10. 最佳实践与注意事项](#10. 最佳实践与注意事项)
      • [10.1 工具设计原则](#10.1 工具设计原则)
      • [10.2 安全注意事项](#10.2 安全注意事项)
      • [10.3 常见陷阱](#10.3 常见陷阱)
    • [11. 总结](#11. 总结)

1. 引言

在 AI Agent 快速发展的今天,一个核心痛点越来越突出:大语言模型(LLM)能力再强,也无法直接访问本地文件、数据库、浏览器或企业内部 API。传统做法是为每个工具写一套定制的调用逻辑,导致代码高度耦合、难以复用,每接入一个新模型或新工具都要重写胶水代码。

MCP(Model Context Protocol,模型上下文协议) 正是为解决这一问题而生。它由 Anthropic 在 2024 年底开源,提供了一套标准化的「模型---工具」通信协议。只要工具方按 MCP 规范暴露能力,任何支持 MCP 的 Agent 都能即插即用地调用这些工具,彻底解耦模型与工具。

本文将带你从零开始,理解 MCP 的架构原理,并用 Python 亲手实现一个 MCP Server 与 Client,最终搭建出一条可运行的 AI Agent 工具链。

2. 什么是 MCP 协议

2.1 一句话理解

MCP 之于 AI Agent,类似于 LSP(Language Server Protocol)之于代码编辑器:编辑器通过 LSP 获得任意语言的提示能力,Agent 通过 MCP 获得任意工具的执行能力。一次开发,处处可用。

2.2 核心概念

MCP 定义了三个核心角色:

角色 职责 典型示例
Host 承载 Agent 的宿主应用 Claude Desktop、自研 Chat 应用
Client 协议客户端,负责与 Server 建立连接 Agent 内部的连接器
Server 工具提供方,暴露「能力」 文件系统服务、数据库服务、Git 服务

一次典型的调用流程:
#mermaid-svg-X391LRoBNr4EJt87{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X391LRoBNr4EJt87 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X391LRoBNr4EJt87 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X391LRoBNr4EJt87 .error-icon{fill:#552222;}#mermaid-svg-X391LRoBNr4EJt87 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X391LRoBNr4EJt87 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X391LRoBNr4EJt87 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X391LRoBNr4EJt87 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X391LRoBNr4EJt87 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X391LRoBNr4EJt87 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X391LRoBNr4EJt87 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X391LRoBNr4EJt87 .marker.cross{stroke:#333333;}#mermaid-svg-X391LRoBNr4EJt87 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X391LRoBNr4EJt87 p{margin:0;}#mermaid-svg-X391LRoBNr4EJt87 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster-label text{fill:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster-label span{color:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster-label span p{background-color:transparent;}#mermaid-svg-X391LRoBNr4EJt87 .label text,#mermaid-svg-X391LRoBNr4EJt87 span{fill:#333;color:#333;}#mermaid-svg-X391LRoBNr4EJt87 .node rect,#mermaid-svg-X391LRoBNr4EJt87 .node circle,#mermaid-svg-X391LRoBNr4EJt87 .node ellipse,#mermaid-svg-X391LRoBNr4EJt87 .node polygon,#mermaid-svg-X391LRoBNr4EJt87 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .rough-node .label text,#mermaid-svg-X391LRoBNr4EJt87 .node .label text,#mermaid-svg-X391LRoBNr4EJt87 .image-shape .label,#mermaid-svg-X391LRoBNr4EJt87 .icon-shape .label{text-anchor:middle;}#mermaid-svg-X391LRoBNr4EJt87 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .rough-node .label,#mermaid-svg-X391LRoBNr4EJt87 .node .label,#mermaid-svg-X391LRoBNr4EJt87 .image-shape .label,#mermaid-svg-X391LRoBNr4EJt87 .icon-shape .label{text-align:center;}#mermaid-svg-X391LRoBNr4EJt87 .node.clickable{cursor:pointer;}#mermaid-svg-X391LRoBNr4EJt87 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X391LRoBNr4EJt87 .arrowheadPath{fill:#333333;}#mermaid-svg-X391LRoBNr4EJt87 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X391LRoBNr4EJt87 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X391LRoBNr4EJt87 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X391LRoBNr4EJt87 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X391LRoBNr4EJt87 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X391LRoBNr4EJt87 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X391LRoBNr4EJt87 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .cluster text{fill:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster span{color:#333;}#mermaid-svg-X391LRoBNr4EJt87 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X391LRoBNr4EJt87 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X391LRoBNr4EJt87 rect.text{fill:none;stroke-width:0;}#mermaid-svg-X391LRoBNr4EJt87 .icon-shape,#mermaid-svg-X391LRoBNr4EJt87 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X391LRoBNr4EJt87 .icon-shape p,#mermaid-svg-X391LRoBNr4EJt87 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X391LRoBNr4EJt87 .icon-shape .label rect,#mermaid-svg-X391LRoBNr4EJt87 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X391LRoBNr4EJt87 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X391LRoBNr4EJt87 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X391LRoBNr4EJt87 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AI Agent

(Host)
MCP Client
MCP Server
文件系统 / 数据库 / API

2.3 三种关键能力

MCP Server 可以对外提供三类能力:

  • Tools(工具):可被模型调用的具体操作,比如「查询用户表」「读取文件」。模型根据工具的 schema 决定何时调用、传什么参数。
  • Resources(资源):可被读取的数据,比如「数据库表结构」。资源与工具的区别在于:工具是「做动作」,资源是「读数据」。
  • Prompts(提示模板):预设好的提示词片段,便于复用。

其中 Tools 是 Agent 工具链中最常用、最先要掌握的部分,本文将重点围绕它展开。

3. MCP 架构与传输方式

3.1 Client-Server 架构

MCP 采用严格的 C/S 架构,Client 与 Server 之间通过 JSON-RPC 2.0 交换消息。一次工具调用在协议层表现为几个标准方法:

  • tools/list:Client 向 Server 请求可用工具列表及参数 schema;
  • tools/call:Client 发起一次工具调用,携带参数;
  • initialize:建立连接时的握手;
  • notifications:取消、进度通知等。

3.2 传输方式

MCP 官方支持两种传输:

  1. stdio(标准输入输出):最常见、最适合本地工具。Server 作为子进程启动,Client 通过 stdin/stdout 与其通信。
  2. HTTP + SSE(Server-Sent Events):适合远程服务,Server 以 HTTP 服务形式运行,Client 通过网络连接。

本地开发优先推荐 stdio,部署简单、调试方便;需要多用户共享或跨机器调用时再切换到 HTTP 模式。

4. 环境准备

本文使用 Python 3.10+ 作为开发语言。MCP 官方提供了 Python SDK,README 后续示例均基于 Python 3.10 验证。

4.1 创建虚拟环境

bash 复制代码
mkdir mcp-demo && cd mcp-demo
python3 -m venv .venv
source .venv/bin/activate   # Windows 使用 .venv\Scripts\activate

4.2 安装依赖

bash 复制代码
pip install "mcp[cli]"
pip install httpx python-dotenv

安装完成后验证版本:

bash 复制代码
mcp version

输出 MCP 的 CLI 版本号即表示安装成功。mcp[cli] 同时提供了 mcp devmcp run 等调试命令,后续会用到。

5. 从零构建第一个 MCP Server

5.1 最小示例

新建 server.py

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

# 创建一个 MCP Server 实例
mcp = FastMCP("Demo Server")


@mcp.tool()
def add(a: int, b: int) -> int:
    """计算两个整数之和"""
    return a + b


if __name__ == "__main__":
    mcp.run()

这里有几个关键点:

  • FastMCP 是官方提供的高层封装,可以大幅减少样板代码;
  • @mcp.tool() 装饰器把一个普通 Python 函数注册为 MCP Tool;
  • 函数的 docstring 会自动成为工具的 description,类型注解则自动生成参数 schema,供模型理解何时调用、如何传参。

运行 Server:

bash 复制代码
python server.py

提示:stdout 是 MCP 协议的通信通道,不要在 Server 代码里用 print 输出日志,否则会污染协议数据流。需要日志时请写入 stderr 或使用 logging 模块。

5.2 添加更多工具

下面扩展一个简单的「待办事项」服务,让 Server 具备真正的业务能力:

python 复制代码
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("Todo Server")

_todos: dict[int, str] = {}
_next_id = 1


class TodoResult(BaseModel):
    """待办操作结果"""
    id: int = Field(description="待办 ID")
    content: str = Field(description="待办内容")


@mcp.tool()
def add_todo(content: str) -> TodoResult:
    """添加一条待办事项

    Args:
        content: 待办内容,比如「下午 3 点开会」
    """
    global _next_id
    todo_id = _next_id
    _next_id += 1
    _todos[todo_id] = content
    return TodoResult(id=todo_id, content=content)


@mcp.tool()
def list_todos() -> list[TodoResult]:
    """列出所有待办事项"""
    return [TodoResult(id=k, content=v) for k, v in _todos.items()]


@mcp.tool()
def remove_todo(todo_id: int) -> str:
    """删除指定待办事项

    Args:
        todo_id: 要删除的待办 ID
    """
    if todo_id not in _todos:
        return f"待办 {todo_id} 不存在"
    content = _todos.pop(todo_id)
    return f"已删除待办:{content}"

当函数返回 Pydantic 模型时,MCP 会自动将其序列化为结构化输出,并附带字段说明,方便模型理解返回内容。

6. MCP Client 开发:连接与调用

Server 就绪后,我们编写 Client 来连接它。编写 client.py

python 复制代码
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def main():
    server_params = StdioServerParameters(
        command="python",
        args=["server.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 初始化连接
            await session.initialize()

            # 获取工具列表
            tools = await session.list_tools()
            print("可用工具:", [t.name for t in tools.tools])

            # 调用 add_todo 工具
            result = await session.call_tool(
                "add_todo",
                arguments={"content": "阅读 MCP 官方文档"},
            )
            print("添加结果:", result.content)

            # 调用 list_todos 工具
            result = await session.call_tool("list_todos", arguments={})
            print("待办列表:", result.content)


asyncio.run(main())

运行结果示例:

text 复制代码
可用工具: ['add_todo', 'list_todos', 'remove_todo']
添加结果: [TextContent(text='{"id": 1, "content": "阅读 MCP 官方文档"}', type='text')]
待办列表: [TextContent(text='[{"id": 1, "content": "阅读 MCP 官方文档"}]', type='text')]

注意 call_tool 返回的 content 是一个列表,其中每个元素是 TextContent 对象,实际文本在 .text 属性里。实践中通常将其拼接后交给大模型继续处理。

7. 实战:搭建 AI Agent 工具链

有了 Server 与 Client,最后一步是把它们接入大模型,形成一个完整的 Agent 闭环:模型理解用户意图 → 决定调用工具 → 执行工具 → 根据结果生成回答。

7.1 Agent 主循环

我们使用 OpenAI 兼容接口作为模型层(可根据需要替换为其他兼容服务)。先安装依赖:

bash 复制代码
pip install openai

编写 agent.py

python 复制代码
import json
import asyncio
from openai import AsyncOpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


SYSTEM_PROMPT = """你是一个待办管理助手。
当用户需要添加、查询或删除待办时,调用对应工具完成操作。
不要在未经调用工具的情况下编造待办数据。"""


async def run_agent(user_input: str):
    # 1. 启动 MCP 连接
    server_params = StdioServerParameters(command="python", args=["server.py"])

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 2. 把 MCP 工具转换成 OpenAI function 格式
            mcp_tools = await session.list_tools()
            functions = [
                {
                    "type": "function",
                    "function": {
                        "name": t.name,
                        "description": t.description or "",
                        "parameters": t.inputSchema,
                    },
                }
                for t in mcp_tools.tools
            ]

            # 创建 DeepSeek 兼容客户端(可按需更换)。
            client = AsyncOpenAI(
                api_key="your-api-key",
                base_url="https://api.deepseek.com",
            )

            messages = [
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": user_input},
            ]

            # 3. 模型决策:是否需要调用工具
            response = await client.chat.completions.create(
                model="deepseek-chat",
                messages=messages,
                tools=functions,
            )
            msg = response.choices[0].message

            # 4. 如果模型请求调用工具,则执行并回填结果
            while msg.tool_calls:
                messages.append(msg)  # 把 assistant 的工具调用请求加入上下文

                for tool_call in msg.tool_calls:
                    tool_name = tool_call.function.name
                    tool_args = json.loads(tool_call.function.arguments)

                    result = await session.call_tool(tool_name, arguments=tool_args)
                    result_text = result.content[0].text if result.content else ""

                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": result_text,
                    })
                    print(f"[工具执行] {tool_name}({tool_args}) -> {result_text}")

                # 带着工具结果再次请求模型
                response = await client.chat.completions.create(
                    model="deepseek-chat",
                    messages=messages,
                    tools=functions,
                )
                msg = response.choices[0].message

            # 5. 输出最终回答
            print(f"[Agent 回答] {msg.content}")


if __name__ == "__main__":
    asyncio.run(run_agent("帮我添加一条待办:明天上午 10 点参加评审"))

运行后可以看到完整链路:

text 复制代码
[工具执行] add_todo({'content': '明天上午 10 点参加评审'}) -> {"id": 3, "content": "明天上午 10 点参加评审"}
[Agent 回答] 已为你添加待办:明天上午 10 点参加评审。

7.2 关键设计点

  • 工具描述质量决定调用准确率:模型依靠 description 和参数 schema 判断是否调用工具,描述应包含「何时用、怎么用、参数含义、返回什么」。
  • 工具执行结果必须回填 :很多 Agent 失败的根因是遗漏了 role: tool 的回填步骤,导致模型收不到工具结果。
  • 循环上限 :生产环境应给 while 循环加上最大迭代次数(如 5 次),防止模型反复调用工具造成死循环或高额成本。
  • 错误处理:工具内部应捕获异常并返回可读的错误信息,而不是把堆栈抛给模型。

8. 使用 MCP Inspector 调试

MCP 官方提供了图形化调试工具 MCP Inspector,通过 mcp CLI 启动:

bash 复制代码
mcp dev server.py

启动后终端会显示一个本地调试地址,浏览器打开后即可:

  • 查看 Server 暴露的所有 Tools / Resources / Prompts;
  • 手动填写参数并调用工具,实时查看请求与响应;
  • 不需要编写 Client 代码即可验证 Server 逻辑。

这是开发阶段排查「工具调用失败」「返回格式异常」问题最有效的手段。

9. 进阶:拆分为模块化工具链

当工具越来越多时,建议采用模块化组织:

text 复制代码
mcp-agent/
├── agent.py          # Agent 主循环
├── client.py         # MCP Client 封装
└── servers/          # 多个 MCP Server
    ├── todo/
    │   └── server.py
    ├── filesystem/
    │   └── server.py
    └── database/
        └── server.py

每个 Server 独立实现、独立运行,Agent 侧同时连接多个 Server 并汇总所有工具。这样的好处是:

  • 工具按职责拆分,单个 Server 保持轻量;
  • 不同 Server 可由不同团队维护,互不影响;
  • Agent 可以按需启用或禁用某个工具集。

同时连接多个 Server 的典型写法:

python 复制代码
async def connect_servers():
    servers = [
        StdioServerParameters(command="python", args=["servers/todo/server.py"]),
        StdioServerParameters(command="python", args=["servers/filesystem/server.py"]),
    ]
    sessions = []
    for params in servers:
        read, write = await stdio_client(params).__aenter__()
        session = await ClientSession(read, write).__aenter__()
        await session.initialize()
        sessions.append(session)
    return sessions

10. 最佳实践与注意事项

10.1 工具设计原则

  • 单一职责:一个工具只做一件事,参数清晰、返回明确;
  • 描述先行:把 description 当作给模型的「使用说明书」来写;
  • 幂等优先:能做成幂等的操作尽量幂等,方便模型重试;
  • 返回结构化数据:优先使用 Pydantic 模型定义返回类型,方便后续程序化处理。

10.2 安全注意事项

  • 输入校验:对工具参数做严格校验,防止模型幻觉参数导致越权或异常;
  • 权限隔离:敏感操作(删除、写入、执行命令)需要额外确认或白名单机制;
  • 不暴露密钥:工具内部使用的 API key 等敏感信息要放在环境变量中,不写入 schema 和返回结果;
  • 协议日志外移:stdio 模式下不要向 stdout 写日志。

10.3 常见陷阱

  • 在 stdio Server 中使用 print 会破坏协议通信,导致 Client 报「协议解析失败」;
  • Windows 下 stdio 子进程的编码问题可能导致中文乱码,建议显式设置 UTF-8;
  • call_tool 返回的是 CallToolResult,需要从 .content 列表里取文本,直接打印对象会得到不可读的内容;
  • 模型可能返回不完整的 JSON 参数,需要在解析 tool_call.function.arguments 时做异常兜底。

11. 总结

本文从 MCP 的概念出发,完整走通了「Server 开发 → Client 连接 → Agent 集成 → 调试调试 → 模块化拆分」的全过程。通过 MCP,工具与模型彻底解耦,你的 AI Agent 可以像搭积木一样组合任意工具能力,而不必为每次集成重写胶水代码。

下一步可以尝试:

  • 接入官方提供的 filesystem、git、sqlite 等参考 Server;
  • 将自研 Server 部署为远程 HTTP 服务,支持多客户端共享;
  • 在 Agent 中加入多轮工具调用与审批流,构建更复杂的业务 Agent。

掌握 MCP,就等于掌握了 AI Agent 工具链的通用语言。现在就动手,把你手头的第一个内部工具接入 MCP 吧。

相关推荐
Bruce_Liuxiaowei13 分钟前
2026年8月第4周网络安全形势周报
人工智能·安全·web安全·智能体
2601_9622035114 分钟前
Nginx 安装配置
运维·nginx
航飞光电市场经理15 分钟前
2026年工业UWB定位降本实战:基于航飞光电UWB信标+蓝牙信标+LoRa融合方案的深度解析
大数据·人工智能
IT_陈寒17 分钟前
SpringBoot自动配置失效?这个隐式依赖坑了我三天
前端·人工智能·后端
Helen_Liang_七仔的AI工具箱17 分钟前
该缺的缺、该压的压不住?库存管理的3个盲区(附ai-inventory-procurement-forecast-skill安装地址)
人工智能·库存预测
luj_176820 分钟前
断舍离中的项目管理智慧
c语言·开发语言·c++·经验分享·算法
码视野23 分钟前
基于 Spring Boot + Vue3 的【城市雨污水管网液位淤积溯源与立交桥下穿隧洞防汛排涝智控中台】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端
跨境数据猎手23 分钟前
外贸独立站开发:从选型到上线全流程梳理
大数据·架构·自动化