前置说明:这是一个我自己学习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——让工具"外挂化"
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 之间的"翻译官",负责三件事:
启动子进程:用
subprocess.Popen启动 MCP Server初始化握手:发送
initialize请求,交换协议版本和能力信息发现和调用工具:通过
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)关键点:inputSchema → parameters 的字段名映射。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 的关系
二者是互补的,解决不同维度的问题:
验证脚本
运行 python scripts/verify_stage_4.py
MCP 客户端连接与发现:连接模拟 Server,确认发现
echo和add两个工具MCP 工具调用(echo):调用 echo 工具,验证返回内容正确
MCP 工具调用(add):调用 add 工具计算 3+7=10
MCPToolAdapter 包装:验证 Adapter 正确转换了工具规格,并能正常执行
Skill 列表:SkillLoader 成功列出
helloSkillSkill 加载:成功解析 frontmatter,提取 name、description、content
build_prompt 生成:验证生成的 prompt 包含 Skill 名称和内容
空目录处理:不存在的目录不报错,返回空
Agent + MCP 集成(模拟模型):用 FakeModel 模拟模型调用 echo 工具,验证 Agent 循环正确传递 MCP 工具结果
真实模型 + MCP(需 API Key):用 OpenAI 真实模型调用 add 工具
学到的要点
协议比代码重要:MCP 的核心不是它的实现,而是 JSON-RPC over stdio 这套简单的协议。理解了协议,你可以用任何语言实现 Server 或 Client。
适配器模式是系统集成的瑞士军刀:
MCPToolAdapter让外部工具对 Agent 透明——Agent 不需要知道工具来自哪里。以后加其他协议(如 HTTP-based 的工具),只需要写一个新的 Adapter,Agent 代码一行都不用改。请求-响应的配对:JSON-RPC 用
id字段关联请求和响应。Server 必须原样返回请求中的id,Client 靠它判断"这条响应是对应刚才哪个请求的"。这也意味着 Client 不能并发发多个请求(除非维护一个待处理请求表)。子进程通信是个好东西:
subprocess.Popen+ stdin/stdout 通信比 HTTP 更简单可靠——不需要端口管理、不需要处理网络中断。MCP 选择 stdio 是有道理的。依赖最小化:SkillLoader 没有引入 PyYAML,而是自己写了个 10 行的极简解析器。功能只实现了项目需要的部分——这就是"你不需要它"(YAGNI)原则。
声明式 vs 命令式:Skill 是声明式的知识注入,MCP 工具是命令式的能力扩展。两者结合,Agent 既"懂得多"又"能做得多"。
MCP 的真实世界应用:有了 MCP 支持,Agent 可以使用任何 MCP Server。比如:
@modelcontextprotocol/server-filesystem→ 安全的文件系统访问@modelcontextprotocol/server-github→ GitHub 操作@modelcontextprotocol/server-postgres→ 数据库查询任何社区或自己写的 MCP Server