200字
Agent学习-Phase4:MCP 与 Skill 集成
2026-05-03
2026-05-03

前置说明:这是一个我自己学习Agent Harness开发的一个笔记,上传到这里纯属方便,这个项目叫Odd, 是通过分多个Phase来完成一个能够支持Tools MCP Skill 子agent 任务清单和上下文压缩的Agent框架。我会分多个Phase让AI完成代码,并阅读代码学习

仓库在:https://github.com/GC-SHIRO/Odd.git

一句话总结

让 Agent 长出"外接器官"的能力。MCP(Model Context Protocol)让 Agent 可以调用外部进程提供的工具——就像给 Agent 插上一块"外接硬盘";Skill 让 Agent 可以通过 SKILL.md 文件加载领域知识和行为指令——就像给 Agent 装上"可热插拔的记忆芯片"。


为什么需要 MCP 和 Skill?

前三个阶段,Agent 已经会"聊"(模型层)、会"做事"(工具系统)、会"反复思考做事"(Agent 循环)。但有一个问题:

  • 工具是写死的:所有工具都是 Python 代码,想加新工具就得改代码、重新部署

  • 知识是写死的:Agent 只知道 system prompt 里告诉它的那点东西

MCP 和 Skill 就是要解决这两个问题:

问题

解决方案

类比

工具不好扩展

MCP:让 Agent 通过标准化协议调用外部进程的工具

USB 接口——插上就能用

知识不好扩展

Skill:用 Markdown 文件描述领域知识,动态加载

记忆卡——插上就知道新知识


第一部分:MCP——让工具"外挂化"

MCP 是什么?

MCP(Model Context Protocol)是 Anthropic 提出的一个开放协议,让 AI 模型和外部工具之间通过标准化的方式通信。核心思想是:

  • MCP Server 是一个独立进程,提供工具

  • MCP Client 连接到 Server,发现工具、调用工具

  • 通信协议是 JSON-RPC 2.0,通过 stdio(标准输入/输出)传递消息

一句话:你的 Agent 不用自己实现工具了,去连接别人写的 MCP Server 就行。

标准定义:什么是模型上下文协议 (MCP)? - Model Context Protocol - MCP 模型上下文协议

架构图

┌─────────────┐     JSON-RPC over stdio     ┌──────────────────┐
│   Odd Agent │ ←─────────────────────────→ │   MCP Server     │
│             │   initialize                 │  (独立进程)       │
│  MCPClient  │   tools/list                 │                  │
│      +      │   tools/call                 │  echo tool       │
│  Adapter    │                              │  add tool        │
└─────────────┘                              └──────────────────┘

MCPClient(odd/mcp/client.py)——通信管道

MCPClient 是 Agent 和外部 MCP Server 之间的"翻译官",负责三件事:

  1. 启动子进程:用 subprocess.Popen 启动 MCP Server

  2. 初始化握手:发送 initialize 请求,交换协议版本和能力信息

  3. 发现和调用工具:通过 tools/list 获取工具列表,通过 tools/call 执行工具

class MCPClient:
    def __init__(self, command: List[str]):
        # command 就是启动 MCP Server 的命令
        # 比如 ["npx", "@modelcontextprotocol/server-filesystem", "/path"]
        self.command = command
        self._request_id = 0  # JSON-RPC 请求 ID,每次递增

    def connect(self):
        # 1. 启动子进程
        self.process = subprocess.Popen(
            self.command,
            stdin=subprocess.PIPE,   # 往这写 JSON-RPC 请求
            stdout=subprocess.PIPE,  # 从这读 JSON-RPC 响应
            text=True,
        )
        # 2. 初始化握手:告诉 Server 我是谁,我支持什么
        self._send_request("initialize", {
            "protocolVersion": "2024-11-05",
            "capabilities": {},
            "clientInfo": {"name": "odd", "version": "0.1.0"},
        })
        # 3. 通知 Server 初始化完成
        self._send_notification("initialized", {})

    def list_tools(self):
        # 问 Server:你有什么工具?
        response = self._send_request("tools/list", {})
        return response.get("tools", [])
        # 返回格式:[{"name": "echo", "description": "...", "inputSchema": {...}}, ...]

    def call_tool(self, name, arguments):
        # 让 Server 执行工具
        response = self._send_request("tools/call", {
            "name": name,
            "arguments": arguments,
        })
        # MCP 结果在 content 数组里,取 text 类型的内容
        content = response.get("content", [])
        text_parts = [item["text"] for item in content if item["type"] == "text"]
        return "\n".join(text_parts)

JSON-RPC 通信细节

MCP 用 请求-响应 模式通信,每条消息一行 JSON:

// 请求(有 id,期待响应)
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}

// 响应
{"jsonrpc": "2.0", "id": 1, "result": {"tools": [...]}}

// 通知(无 id,不期待响应)
{"jsonrpc": "2.0", "method": "initialized", "params": {}}

MCPToolAdapter(odd/mcp/adapter.py)——外接器官的"神经接口"

MCP Server 返回的工具定义和 Agent 内部用的 Tool 接口不一样。Adapter 就是做这个适配的——它把 MCP 工具"伪装"成一个普通 Tool,Agent 完全不用知道这个工具是本地实现还是远程 MCP Server 的。

class MCPToolAdapter(Tool):
    def __init__(self, client: MCPClient, tool_def: dict):
        self._client = client
        self._spec = ToolSpec(
            name=tool_def["name"],
            description=tool_def.get("description", ""),
            parameters=tool_def.get("inputSchema", {...}),
        )

    @property
    def spec(self):
        return self._spec

    def execute(self, arguments):
        # 转发给 MCP Client,由它去调用远程 Server
        return self._client.call_tool(self.spec.name, arguments)

关键点:inputSchemaparameters 的字段名映射。MCP 协议用 inputSchema,Agent 内部用 parameters(OpenAI 风格),Adapter 在构造 ToolSpec 时做了这个转换。

模拟 MCP Server(scripts/sandbox/mock_mcp_server.py

为了测试,项目里有一个极简的 MCP Server 模拟器。它就是个死循环,从 stdin 读 JSON,处理后写到 stdout:

# 每行一个独立的 JSON-RPC 请求
for line in sys.stdin:
    request = json.loads(line)
    method = request.get("method")

    if method == "initialize":
        # 返回 Server 信息
        response = {"result": {"protocolVersion": "2024-11-05", ...}}
    elif method == "tools/list":
        # 返回工具列表
        response = {"result": {"tools": [echo, add]}}
    elif method == "tools/call":
        # 执行工具
        if tool_name == "echo":
            result_text = f"echo: {arguments['message']}"
        elif tool_name == "add":
            result_text = f"add: {a} + {b} = {a + b}"
        response = {"result": {"content": [{"type": "text", "text": result_text}]}}
    
    sys.stdout.write(json.dumps(response) + "\n")

这个模拟 Server 提供了两个工具:echo(回显消息)和 add(计算两数之和)。


第二部分:Skill——让知识"可热插拔"

Skill 是什么?

Skill 是一种声明式的知识注入机制。用 Markdown 文件描述 Agent 应该具备的领域知识、行为规范、工作流程。Agent 启动时自动加载,注入到 system prompt 中。

标准定义:Agent Skills Overview - Agent Skills

Skill 文件格式(SKILL.md)

每个 Skill 一个文件夹,里面放一个 SKILL.md,格式是 YAML frontmatter + Markdown 正文

---
name: hello-skill
description: 一个简单的示例 Skill,教会 Agent 用中文打招呼
---

# Hello Skill

当用户请求打招呼时,使用以下句式回复:
"你好,我是 Odd。很高兴为您服务!"

请始终保持友好、热情的语气。
  • --- 之间是 YAML 元数据:name(Skill 名称)、description(描述)

  • --- 之后是 Markdown 正文:要注入 system prompt 的实际指令

SkillLoader(odd/skills/loader.py)——记忆卡读卡器

SkillLoader 扫描 skills/ 目录,解析所有 SKILL.md,并提供给 Agent。

class SkillLoader:
    def __init__(self, skills_dir: str):
        self.skills_dir = skills_dir

    def list_skills(self):
        # 扫描目录,找所有包含 SKILL.md 的子文件夹

    def load(self, name):
        # 读取指定 Skill 的 SKILL.md → 解析 frontmatter → 返回 Skill 对象

    def load_all(self):
        # 加载全部 Skill

    def build_prompt(self):
        # 把所有 Skill 拼成一段可注入 system prompt 的文本
        # 格式:
        #   ## 已加载的 Skill
        #   ### hello-skill
        #   *一个简单的示例 Skill*
        #   
        #   当用户请求打招呼时...

极简 YAML 解析

SkillLoader 没有引入 PyYAML 依赖,而是自己写了一个最小化的解析器,只支持 key: value 格式:

@staticmethod
def _parse_simple_yaml(text):
    result = {}
    for line in text.strip().split("\n"):
        if ":" in line:
            key, _, value = line.partition(":")
            result[key.strip()] = value.strip().strip("\"'")
    return result

这也是项目"最小实现"原则的体现:不是非用不可的依赖就不加


第三部分:Agent → MCP 集成

如何在 Agent 中使用 MCP 工具?

整体流程:

# 1. 连接 MCP Server
client = MCPClient(["python", "mock_mcp_server.py"])
client.connect()

# 2. 发现工具,包装成 Adapter
tools = client.list_tools()
adapters = [MCPToolAdapter(client, t_def) for t_def in tools]

# 3. 把 Adapter 传给 Agent——Agent 完全不知道它们是远程的!
agent = Agent(model=provider, tools=adapters, max_rounds=5)

# 4. 正常使用
result = agent.run("请调用 add 工具计算 42 + 58")

Agent 看到的 adapters 就是普通的 Tool 列表。当 Agent 调用时:

Agent: 我要调用 add(a=42, b=58)
  → MCPToolAdapter.execute({"a": 42, "b": 58})
    → MCPClient.call_tool("add", {"a": 42, "b": 58})
      → 写 JSON-RPC 到子进程 stdin
      ← 读 JSON-RPC 从子进程 stdout
    ← "add: 42 + 58 = 100"
  ← "add: 42 + 58 = 100"
Agent: 工具结果已收到

MCP 与 Skill 的关系

二者是互补的,解决不同维度的问题:

维度

MCP

Skill

目的

扩展 Agent 的能力(能做什么)

扩展 Agent 的知识(知道什么)

形式

独立进程 + JSON-RPC 协议

静态 Markdown 文件

注入点

通过 Tool 接口注入

通过 System Prompt 注入

运行时行为

Agent 主动调用工具

模型在生成时参考指令


验证脚本

运行 python scripts/verify_stage_4.py

  1. MCP 客户端连接与发现:连接模拟 Server,确认发现 echoadd 两个工具

  2. MCP 工具调用(echo):调用 echo 工具,验证返回内容正确

  3. MCP 工具调用(add):调用 add 工具计算 3+7=10

  4. MCPToolAdapter 包装:验证 Adapter 正确转换了工具规格,并能正常执行

  5. Skill 列表:SkillLoader 成功列出 hello Skill

  6. Skill 加载:成功解析 frontmatter,提取 name、description、content

  7. build_prompt 生成:验证生成的 prompt 包含 Skill 名称和内容

  8. 空目录处理:不存在的目录不报错,返回空

  9. Agent + MCP 集成(模拟模型):用 FakeModel 模拟模型调用 echo 工具,验证 Agent 循环正确传递 MCP 工具结果

  10. 真实模型 + MCP(需 API Key):用 OpenAI 真实模型调用 add 工具


学到的要点

  1. 协议比代码重要:MCP 的核心不是它的实现,而是 JSON-RPC over stdio 这套简单的协议。理解了协议,你可以用任何语言实现 Server 或 Client。

  2. 适配器模式是系统集成的瑞士军刀MCPToolAdapter 让外部工具对 Agent 透明——Agent 不需要知道工具来自哪里。以后加其他协议(如 HTTP-based 的工具),只需要写一个新的 Adapter,Agent 代码一行都不用改。

  3. 请求-响应的配对:JSON-RPC 用 id 字段关联请求和响应。Server 必须原样返回请求中的 id,Client 靠它判断"这条响应是对应刚才哪个请求的"。这也意味着 Client 不能并发发多个请求(除非维护一个待处理请求表)。

  4. 子进程通信是个好东西subprocess.Popen + stdin/stdout 通信比 HTTP 更简单可靠——不需要端口管理、不需要处理网络中断。MCP 选择 stdio 是有道理的。

  5. 依赖最小化:SkillLoader 没有引入 PyYAML,而是自己写了个 10 行的极简解析器。功能只实现了项目需要的部分——这就是"你不需要它"(YAGNI)原则。

  6. 声明式 vs 命令式:Skill 是声明式的知识注入,MCP 工具是命令式的能力扩展。两者结合,Agent 既"懂得多"又"能做得多"。

  7. MCP 的真实世界应用:有了 MCP 支持,Agent 可以使用任何 MCP Server。比如:

  • @modelcontextprotocol/server-filesystem → 安全的文件系统访问

  • @modelcontextprotocol/server-github → GitHub 操作

  • @modelcontextprotocol/server-postgres → 数据库查询

  • 任何社区或自己写的 MCP Server

评论