Agent 工程里有一句很有吸引力的话:万物皆 CLI。
它的经验基础很扎实。Codex、Claude Code 可以直接调用 git、gh、rg、测试命令和项目脚本;一个团队已有的内部工具,只要放进 PATH,Agent 往往当天就能使用,不必先搭一套协议服务。
但把这句话当成普遍架构结论,会漏掉工具发现、Schema、跨宿主互操作、远程认证和治理问题。这篇不站队,而是做一个可运行比较:
同一个“查询 Agent 运行记录”的能力,先做成适合 Agent 调用的 CLI(Agent-Friendly CLI),再用 MCP Python SDK 2.0 暴露为工具。两边共用领域函数,只比较接口成本和能力边界。
| 项目 | 说明 |
|---|---|
| 内容类型 | Agent 工具接口设计与 CLI/MCP 对照实战 |
| 适合读者 | 正在给 Codex、Claude Code 或自建 Agent 接入内部工具的开发者 |
| 阅读 / 跟做时间 | 约 20 分钟 / 45-60 分钟 |
| 环境要求 | Python 3.10+;CLI 零依赖;MCP 示例使用官方 mcp==2.0.0 |
| 代码检查点 | 23d99e9 |
| 实验下载 | agent-cli-capability-lab-0.2.0.zip |
| SHA-256 | A04884139D17A89588971B7997AB48F6F85654D74DBEBB9CA85607B6A8BF6006 |
| 资料核对日期 | 2026-08-10 |
| 实验边界 | 本地确定性数据;不比较模型质量,不覆盖远程 OAuth 和生产部署 |
一分钟概览
- “Agent 是 CLI”和“Agent 使用 CLI”是两个不同方向。
- CLI 是进程接口:命令、参数、cwd、环境、stdin/stdout/stderr 和退出码。
- MCP 是应用层协议:发现工具、交换 Schema、调用能力,并承载资源、Prompt 和结构化内容。
- MCP 的 stdio 传输也会启动子进程,所以 CLI 和 MCP 不是互斥技术。
- MCP 2026-07-28 核心已改为无状态,不能再用“CLI 无状态、MCP 有状态”做区别。
- 本地、短时、已有工具链优先考虑 CLI;需要跨宿主发现、统一 Schema 和远程治理时再考虑 MCP。
- 最稳妥的实现不是写两套业务逻辑,而是“核心能力 -> CLI 适配器 / MCP 适配器”。
图 1:一个领域能力从核心函数分出 CLI 和 MCP 两个接口,再分别被 Codex、Claude Code 与其他宿主调用
图 1:CLI 与 MCP 不需要争夺业务逻辑所有权。它们更适合做薄适配层,让同一能力在不同接入环境中复用。
1. 先拆开两个方向
讨论“Agent 和 CLI”时,常常把两种关系混在一起。
1.1 Agent 以 CLI 形式运行
codex exec "review this repository"
claude -p "review this repository"
此时 CLI 是 Agent 的进程入口。调用者关心工作目录、权限、事件、退出码和超时。这是上一篇的主题。
1.2 Agent 把 CLI 当作工具
git diff --stat
gh pr view 42 --json title,statusCheckRollup
agent-lab runs get run-003 --format json
此时 Agent 是调用者,CLI 是能力接口。设计重点变成:模型能否从帮助信息理解它、参数是否容易填对、结果是否适合放进上下文、写操作是否有明确副作用。
图 2:Agent 作为 CLI 被调度,与 Agent 把 CLI 作为工具调用的方向区别
图 2:一个方向管理 Agent 生命周期,另一个方向设计 Agent 可用能力。两者都经过操作系统进程,却解决不同问题。
2. 为什么 CLI 对 Agent 特别有吸引力
OpenAI 的 Harness Engineering 实践提到,Agent 可以直接使用 gh、仓库脚本等标准开发工具;官方 Agent-Friendly CLI 用例则强调可预测 JSON、分页、按 ID 精确读取和大结果文件导出。这些做法之所以有效,来自 CLI 的几个朴素优势。
2.1 现成工具很多
git、rg、jq、gh、数据库客户端、云平台 CLI、测试和构建系统早已存在。Agent 不需要一套只为 LLM 重写的世界。
2.2 进程边界容易观察
命令行、参数、输出和退出码天然可以写入追踪记录(Trace),也容易在人类终端里复现。出问题时,开发者能运行同一条命令,而不是只能重放一段模型对话。
2.3 本地认证可以复用
成熟 CLI 往往已有登录、配置文件和权限体系。Agent 在受控环境中可以复用它们,但这也是风险:凭证复用不等于权限合理,外部执行框架仍要限制环境、命令和副作用。
2.4 组合成本低
Shell 可以把小工具串起来。不过“可组合”不意味着应该让模型随意生成长管道。自动化里更稳妥的方式仍是参数数组、固定命令模板和独立中间产物。
3. 一个适合 Agent 调用的 CLI 应该长什么样
配套实验实现了三个只读能力:
python -m agent_cli_lab.cli runs list --limit 2 --format json
python -m agent_cli_lab.cli runs get run-003 --format json
python -m agent_cli_lab.cli reports export run-003 --output reports/run-003.json
对应的设计判断如下。
3.1 用窄命令表达明确意图
runs get <id> 比 query --expression "..." 更容易发现,也更容易做权限审计。稳定 ID 比让模型按标题猜资源可靠。
3.2 人类输出和机器输出分开
默认文本适合终端扫描,--format json 提供稳定字段。不要通过去掉 ANSI 颜色和正则表达式把人类表格勉强解析成协议。
3.3 列表必须有上限
实验限制 1 <= limit <= 100。真实接口还应返回 total、分页游标或是否还有下一页,防止一次结果吞掉上下文。
3.4 大结果返回文件,不塞 stdout
reports export 返回文件路径和字节数。Agent 可以先看摘要,再决定是否读取产物,而不是把数十万字符塞回下一轮推理。
3.5 错误要稳定而可行动
未知 ID 返回 stderr JSON,并以退出码 2 结束:
{"error":"unknown run id: missing"}
这比 Python 堆栈更适合作为工具反馈。写操作还应增加 --dry-run、幂等键、预期版本和明确确认;本文示例刻意只读,因此没有伪造一个没有真实副作用的 --dry-run。
4. CLI 的合同到底是什么
一个 CLI 的“协议”来自多项约定,而不是单独的 JSON:
可执行文件 + 子命令 + 参数
+ cwd + env + stdin
+ stdout + stderr + exit code
+ help/version + 产物
它非常适合同机短生命周期任务,却有几个天然弱点:
- 工具发现通常依赖
PATH和--help,没有统一目录; - 不同 CLI 的 JSON、错误和分页风格不同;
- 输入 Schema 常藏在帮助文字里,宿主需要额外解析或人工编写说明;
- 远程调用、OAuth、租户隔离和跨语言客户端不是 Shell 自动解决的;
- 富文本、图片、资源引用等结果需要额外文件约定。
这并不说明 CLI 落后,只说明它是一种进程接口,而不是完整的 Agent 工具协议。
5. MCP 增加了什么
MCP 在工具场景中提供标准化的 tools/list 和 tools/call,工具携带名称、描述与 JSON Schema;协议还定义 Resources、Prompts、结构化内容和传输方式。
值得特别更新的是:2026-07-28 规范把核心协议改为无状态。 initialize/initialized 握手和 Mcp-Session-Id 已从这一版协议中移除,版本、客户端身份和能力改为随请求携带;需要预先了解服务能力时,可以选择调用 server/discover。因此下面这种对照已经不可靠:
错误:CLI 是无状态,MCP 是有状态
更准确的区别是:
CLI:操作系统进程与用户界面约定
MCP:Agent 宿主与能力提供方之间的应用层协议
MCP 的 stdio 传输同样会启动本地子进程并通过 stdin/stdout 交换消息。它不是“摆脱 CLI”,而是在进程之上增加统一消息和发现合同。
图 3:CLI 进程合同与 MCP 应用层协议在不同层解决问题
图 3:stdio MCP 仍依赖进程与标准流;Streamable HTTP 则把同一协议带到远程服务。状态性已经不是 2026-07-28 核心规范的分界。
6. 用同一个核心函数实现两种接口
实验目录的关键结构是:
agent_cli_lab/
├─ records.py # 领域能力
├─ cli.py # argparse 适配器
└─ mcp_server.py # MCPServer 2.0 适配器
records.py 不知道 CLI 或 MCP:
def get_run(run_id: str, path: Path = DEFAULT_DATASET) -> dict[str, Any]:
for run in _load(path):
if run.get("id") == run_id:
return run
raise RunNotFoundError(f"unknown run id: {run_id}")
CLI 只负责参数和退出码:
data = get_run(args.run_id)
_emit(data, args.format)
MCP 2.0 适配器只负责注册工具:
from mcp.server.mcpserver import MCPServer
server = MCPServer("agent-cli-lab", version="0.2.0")
@server.tool(structured_output=True)
def get_run(run_id: str) -> dict[str, Any]:
return get_run_record(run_id)
这种结构有三个好处:
- CLI 和 MCP 不会产生两套业务判断;
- 领域能力可以用普通单元测试快速覆盖;
- 更换传输或 Agent 宿主时,不必重写数据访问和错误语义。
7. 跑通 CLI 和 MCP 2.0
先验证零依赖 CLI:
cd agent-cli-capability-lab-0.2.0/phase-8-agent-cli-engineering
python -m unittest discover -s tests -v
python -m agent_cli_lab.cli runs list --limit 2 --format json
此时如果尚未安装 MCP SDK,test_mcp_adapter 会显示为 skipped,这是预期结果;其余 CLI 与领域逻辑测试应通过。
再在隔离环境安装官方 MCP Python SDK:
python -m venv .venv
.venv/Scripts/python -m pip install "mcp==2.0.0"
.venv/Scripts/python -m unittest discover -s tests -v
.venv/Scripts/python -m agent_cli_lab.mcp_server
macOS/Linux 把解释器路径换成 .venv/bin/python。
最后一条命令会启动 stdio MCP 服务并等待宿主请求,终端看起来会停在那里,这是服务的正常状态,可以用 Ctrl+C 退出。工具发现是否正确由测试验证,不要把“进程一直运行”误判为卡死。
本机验证结果为 8 tests passed,其中 MCP 测试检查:
tools/list可以发现list_runs、get_run、export_report;get_run的结构化结果与领域函数一致;- CLI 未知 ID 以稳定错误和退出码
2返回; - 导出产物的内容和字节数可以验证。
如果环境里已有 MCP 1.x,仅检查 import mcp 会产生误判。本文第一次测试正是这样失败的:包存在,但 mcp.server.mcpserver.MCPServer 不存在。迁移检查应验证确切版本和实际导入路径。
8. CLI、MCP、SDK/API 怎么选
| 场景 | 优先选择 | 原因 |
|---|---|---|
Agent 在本机调用 git、测试、构建工具 | CLI | 现成、低成本、可复现 |
| 团队已有稳定内部 CLI | 先 CLI | 先验证真实使用频率,再决定是否协议化 |
| 多个 Agent 宿主需要自动发现同一工具 | MCP | 统一发现和 Schema |
| 需要返回图片、资源引用或多种内容块 | MCP | 协议有结构化内容模型 |
| 远程多租户工具,需要统一认证与治理 | MCP 或正式服务 API | Shell 本身不解决远程身份与租户边界 |
| 单一应用内部的高频强类型调用 | SDK/API | 少一层进程和序列化开销 |
| 一次性迁移或运维脚本 | CLI | 易审查、易在终端重放 |
| 大批量低延迟数据路径 | SDK/API | 避免每次启动进程和 Agent 上下文开销 |
图 4:根据本地性、发现需求、远程治理和调用频率选择 CLI、MCP 或 SDK API
图 4:选择顺序不是从“简单”升级到“高级”,而是先看边界。很多内部能力长期停在高质量 CLI 就足够。
一个实用判断顺序是:
已有稳定 CLI,且调用发生在同一受控环境?
是 -> 先直接使用 CLI
否 -> 是否需要跨宿主发现、统一 Schema 或丰富内容?
是 -> 考虑 MCP
否 -> 是否是单应用、高频、强类型调用?
是 -> SDK/API
否 -> 先做最小 CLI 或固定工作流验证需求
9. “万物皆 CLI”在哪些地方成立
它作为工程启发很有价值:
- 先复用现有工具,不为 Agent 重建整个基础设施;
- 让关键操作能在终端里被人复现;
- 用文件、退出码和结构化输出留下证据;
- 把复杂 GUI 操作压缩为可自动化命令。
但作为完整架构结论只对了一半:
- CLI 没有统一工具发现和 Schema 协商;
PATH可见不等于权限合理;- 本地认证可复用不等于适合远程多租户;
- stdout JSON 不自动成为跨宿主协议;
- 进程可启动不代表超时、取消、幂等和产物已被治理。
我更愿意把它改写成:
能做成清晰 CLI 的能力,通常已经拥有不错的任务边界;需要被多个 Agent 宿主长期治理时,再把稳定核心适配成 MCP 或 SDK/API。
10. 适合 Agent 调用的 CLI 检查清单
发现与输入
-
--help和--version稳定可用。 - 子命令窄而明确,不要求模型发明查询语言。
- 使用稳定资源 ID,支持按 ID 精确读取。
- 参数有边界、默认值和明确错误。
输出与上下文
- 提供稳定
--format json,stdout 不混诊断信息。 - stderr 用于可行动错误,退出码有文档。
- 列表支持限制和分页。
- 大结果导出到显式文件并返回产物元数据。
副作用与安全
- 写操作有
dry-run或草稿阶段。 - 支持幂等键、预期版本或冲突检查。
- 不回显 Secret,环境变量最小继承。
- 高风险动作保留批准点和审计证据。
运行可靠性
- 调用者设置 cwd、超时和输出上限。
- 取消时处理子进程和结果未知状态。
- 版本更新有契约测试。
- CLI、MCP 和 SDK 适配器共用领域逻辑。
下一步练习
给你正在使用的一个内部 CLI 做一次 30 分钟体检:
- 让 Codex 只读运行
tool --help,写出它理解的三个核心能力。 - 用三个正常输入、两个边界输入和一个未知 ID 测试退出码与输出。
- 把最大结果导出为文件,比较前后上下文大小。
- 判断它是否真的需要 MCP:写出至少一个跨宿主发现、远程治理或丰富内容的明确需求。
- 若需要 MCP,只新增薄适配器,不复制业务逻辑。
完成后,你得到的不是“CLI 与 MCP 谁赢了”,而是一份能解释当前选择、未来迁移条件和验证证据的接口决策。