同一个需求两种做法,到底差在哪?这篇把 MCP 和 CLI Tool 两种方案从原理到代码到踩坑讲透,全程代码可复制。看完记得点赞收藏,你的支持是我更新最大的动力 🌟
前言:Agent 的"手脚"从哪来?
做过 LangChain Agent 的朋友都知道,Agent 要真正能干活,得有"工具"。查数据库、发邮件、读文件、调 API------这些都得靠外挂工具来完成。
问题是:工具怎么接进来?
目前摆在面前有两条主流的路:
路线 A:CLI Tool(传统做法)
把 git、curl、gh 这些命令行工具用 subprocess 包一下,套个 @tool 装饰器,就能给 Agent 用。简单直接,LangChain 从 0.0.x 版本就这么玩。
路线 B:MCP(Model Context Protocol)
Anthropic 在 2024 年底推出的开放协议,目标是做"Agent 界的 USB-C"------标准化 LLM 接入外部工具的方式。2025 年 LangChain 官方推出 langchain-mcp-adapters,现在 MCP Server 生态已经长出几百个。
两种路都能达到目的,但背后的取舍完全不同。这篇文章就是帮你搞清楚:
- 两者的本质区别
- 同一个需求两种做法的代码对比
- 什么场景选什么
- 生产环境踩过的坑
准备好了吗?我们开始。
一、先把两个概念说清楚
1.1 CLI Tool 方式:把命令行"翻译"给 Agent
核心思路一句话:任何能在命令行跑的东西,包一层都能给 Agent 用。
举个例子。我想让 Agent 能查 GitHub 某个仓库的 issues,最朴素的做法是:
python
import subprocess
from langchain_core.tools import tool
@tool
def list_github_issues(repo: str) -> str:
"""
列出指定 GitHub 仓库的开放 issues。
repo 格式:owner/repo,比如 "langchain-ai/langchain"
"""
result = subprocess.run(
["gh", "issue", "list", "--repo", repo, "--limit", "10"],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
return f"Error: {result.stderr}"
return result.stdout
完事。只要本地装了 gh 命令,这个工具就能用。
优点 :直观、零协议开销、任何 CLI 都能用。 缺点:每个工具都要自己写 schema、自己处理输出、自己做错误处理。
1.2 MCP 方式:标准化的"能力中介"
MCP 引入了一个中间层------MCP Server。
原本的架构是:
objectivec
LangChain Agent → 直接调用 subprocess → CLI 工具
MCP 的架构是:
arduino
LangChain Agent → MCP Client → MCP Server → 实际工具 / API / 数据库
看起来多了一层,但这一层带来了标准化。MCP Server 对外暴露:
- Tools:可执行的操作(和 LangChain Tool 对应)
- Resources:可读取的资源(文件、数据库记录等)
- Prompts:预定义的 Prompt 模板
关键点 :同一个 MCP Server 可以被 Claude Desktop、Cursor、LangChain Agent、Cline 等多个客户端复用,不用每个框架都写一遍集成。
社区已经有几百个开源 MCP Server,覆盖 GitHub、GitLab、Slack、Postgres、Google Drive、Filesystem......基本上常用工具都有现成的。
1.3 两者到底差在哪?
我画个对比图你就懂了:
arduino
【CLI Tool 方式】
LangChain Agent
↓ @tool + subprocess
命令行程序(gh / git / curl)
特点:点对点,一对一绑定
【MCP 方式】
LangChain Agent ─┐
Claude Desktop ─┼──→ MCP Client 协议 ──→ MCP Server ──→ 底层能力
Cursor ─┘
特点:标准化,一个 Server 多端复用
CLI 是"你自己造轮子 ",MCP 是"大家一起造一套轮子"。
二、实战 Round 1:CLI Tool 方式
还是 GitHub issues 查询的例子,做一个更完整的版本。
2.1 安装准备
bash
# 装 gh 命令行
brew install gh # macOS
# 或 Windows/Linux 去 cli.github.com 下载
# 登录
gh auth login
# Python 依赖
pip install langchain langchain-openai langgraph
2.2 把 CLI 包成 Tool
python
import subprocess
import json
from langchain_core.tools import tool
@tool
def list_github_issues(repo: str, limit: int = 10, state: str = "open") -> str:
"""
列出指定 GitHub 仓库的 issues。
Args:
repo: 仓库名,格式为 "owner/repo"
limit: 返回数量,默认 10
state: issue 状态,可选 "open" / "closed" / "all"
"""
try:
result = subprocess.run(
[
"gh", "issue", "list",
"--repo", repo,
"--limit", str(limit),
"--state", state,
"--json", "number,title,author,createdAt",
],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
return f"Error: {result.stderr}"
# gh --json 输出是 JSON,自己解析成更易读的格式
issues = json.loads(result.stdout)
if not issues:
return "该仓库没有符合条件的 issue"
lines = []
for issue in issues:
lines.append(
f"#{issue['number']} {issue['title']} "
f"(by {issue['author']['login']}, {issue['createdAt'][:10]})"
)
return "\n".join(lines)
except subprocess.TimeoutExpired:
return "Error: 命令超时"
except Exception as e:
return f"Error: {str(e)}"
@tool
def get_github_issue_detail(repo: str, issue_number: int) -> str:
"""获取 issue 的详细内容(标题、正文、评论数)。"""
try:
result = subprocess.run(
[
"gh", "issue", "view", str(issue_number),
"--repo", repo,
"--json", "title,body,comments",
],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
return f"Error: {result.stderr}"
data = json.loads(result.stdout)
return (
f"Title: {data['title']}\n\n"
f"Body: {data['body'][:500]}\n\n"
f"Comments: {len(data['comments'])}"
)
except Exception as e:
return f"Error: {str(e)}"
2.3 组装 Agent
python
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_agent(
llm,
tools=[list_github_issues, get_github_issue_detail],
)
# 跑起来
response = agent.invoke({
"messages": [
("user", "看一下 langchain-ai/langchain 仓库最新的 5 个 issue,挑一个感兴趣的看详情")
]
})
for msg in response["messages"]:
print(msg.content[:200] if msg.content else "", "\n---")
2.4 这个方案的问题
能跑通,但你会发现几个烦人的点:
- 每个工具的 schema 要手写:函数签名、docstring、参数说明,全靠你自己
- 输出格式不统一:有的 CLI 输出 JSON,有的输出表格,每个都要自己解析
- 错误处理重复造轮子:超时、returncode 非零、stderr 解析,每个工具都写一遍
- 没法被别的 Agent 框架复用 :换个 LlamaIndex、Autogen,这些
@tool全废 - 权限控制要自己做:谁能调什么工具、读哪些目录,都得自己写一套
这些问题在单人小项目里不是问题,规模一大就全变成技术债。
三、实战 Round 2:MCP 方式
同样的需求,用 MCP 怎么做。
3.1 安装准备
arduino
pip install langchain-mcp-adapters langgraph "langchain[openai]"
3.2 方案 A:直接用社区现成的 MCP Server(推荐)
GitHub 官方已经维护了一个 MCP Server:github-mcp-server。装上后直接连就行,一行业务代码都不用写。
ruby
# 装官方的 GitHub MCP Server(用 Go 实现,也可以 npx 启动 npm 版)
brew install github-mcp-server
# 或用 npx:npx -y @modelcontextprotocol/server-github
LangChain 这边接入:
python
import asyncio
import os
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
async def main():
# 配置 MCP Server
client = MultiServerMCPClient({
"github": {
"command": "github-mcp-server",
"args": ["stdio"],
"transport": "stdio",
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": os.environ["GITHUB_TOKEN"],
},
},
})
# 自动发现 Server 提供的所有工具
tools = await client.get_tools()
print(f"从 MCP Server 加载了 {len(tools)} 个工具:")
for t in tools:
print(f" - {t.name}: {t.description[:60]}")
# 组装 Agent
agent = create_agent("openai:gpt-4o-mini", tools=tools)
# 跑同样的任务
response = await agent.ainvoke({
"messages": [
("user", "看一下 langchain-ai/langchain 仓库最新的 5 个 issue,挑一个感兴趣的看详情")
]
})
for msg in response["messages"]:
print(msg.content[:200] if msg.content else "", "\n---")
asyncio.run(main())
注意两个关键差异:
- 工具是 自动发现 的(
client.get_tools()),不用一个个手写 - Server 由 GitHub 官方维护,功能远比我们手写的 2 个函数全------issue、PR、release、workflow 都能查
3.3 方案 B:自己写一个 MCP Server
如果你的工具是内部系统(比如公司自建的工单平台),社区没有现成的 MCP Server,就得自己写。好消息是 FastMCP 让这件事特别简单。
先写 Server:
python
# my_github_server.py
from mcp.server.fastmcp import FastMCP
import subprocess
import json
mcp = FastMCP("GitHub Tools")
@mcp.tool()
def list_issues(repo: str, limit: int = 10, state: str = "open") -> str:
"""列出 GitHub 仓库 issues"""
result = subprocess.run(
["gh", "issue", "list", "--repo", repo,
"--limit", str(limit), "--state", state,
"--json", "number,title,author,createdAt"],
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
return f"Error: {result.stderr}"
issues = json.loads(result.stdout)
return "\n".join(
f"#{i['number']} {i['title']} (by {i['author']['login']})"
for i in issues
) or "无 issue"
@mcp.tool()
def get_issue_detail(repo: str, issue_number: int) -> str:
"""获取 issue 详情"""
result = subprocess.run(
["gh", "issue", "view", str(issue_number), "--repo", repo,
"--json", "title,body,comments"],
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
return f"Error: {result.stderr}"
data = json.loads(result.stdout)
return f"Title: {data['title']}\n\nBody: {data['body'][:500]}"
if __name__ == "__main__":
mcp.run(transport="stdio")
然后在 LangChain 端接入:
python
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
async def main():
client = MultiServerMCPClient({
"github": {
"command": "python",
"args": ["/absolute/path/to/my_github_server.py"],
"transport": "stdio",
},
})
tools = await client.get_tools()
agent = create_agent("openai:gpt-4o-mini", tools=tools)
response = await agent.ainvoke({
"messages": [("user", "看下 langchain-ai/langchain 的最新 issues")]
})
print(response["messages"][-1].content)
asyncio.run(main())
代码量和 CLI 方案差不多,但这个 Server 现在同时可以被 Claude Desktop、Cursor 用 ------把 Server 配置丢进这些客户端的 config 就完事了。一次写,多端用。
3.4 多 Server 混合
MCP 真正的威力在 组合 。MultiServerMCPClient 可以一次连多个 Server:
makefile
client = MultiServerMCPClient({
"github": {
"command": "github-mcp-server",
"args": ["stdio"],
"transport": "stdio",
"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": os.environ["GITHUB_TOKEN"]},
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"],
"transport": "stdio",
},
"postgres": {
"url": "http://localhost:8001/mcp",
"transport": "http",
},
})
tools = await client.get_tools()
# 现在 agent 同时拥有 GitHub、文件系统、Postgres 三类能力
你可以让 Agent 完成这样的任务:
"从 Postgres 查最近一周的错误日志,到 GitHub 对应仓库搜一下有没有相关 issue,没有就创建一个,把日志摘要写进去,然后把分析结果存到本地
~/projects/error_report.md。"
这个任务在 CLI 方案下要自己封装三套 subprocess 调用;在 MCP 下就是把三个现成的 Server 配上去,剩下全自动。
四、多维度对比
把两种方案放到同一张表里对比:
| 维度 | CLI Tool | MCP |
|---|---|---|
| 接入成本 | 低(有 CLI 就能包) | 中(需要 Server,但常用场景有现成的) |
| 工具描述 | 手写 docstring + schema | 自动发现 |
| 输出格式 | 原始字符串,自己解析 | 结构化内容块(content blocks) |
| 可复用性 | 强绑定当前 Agent 框架 | 跨框架复用(Claude/Cursor/LangChain 通吃) |
| 错误处理 | 自己做 | 协议标准化 |
| 权限控制 | 自己做 | Server 端统一管理 |
| 调试链路 | 短(一层 subprocess) | 长(跨进程通信) |
| 传输协议 | / | stdio / HTTP / SSE 可选 |
| 生态成熟度 | 高(任何 CLI 都能包) | 成长中但发展极快 |
| 运行开销 | 低 | 多一个进程,略高 |
五、选型建议:什么场景选什么
5.1 选 CLI Tool 的场景
✅ 快速原型验证 :想法阶段,用最短路径跑通就行 ✅ 工具数量少且单一 :只需要 2-3 个简单工具,写个 @tool 就完事,不需要 Server 那套 ✅ 纯内部脚本 :一次性任务或临时自动化,不考虑长期维护 ✅ 对延迟极度敏感:跨进程通信终究有开销
5.2 选 MCP 的场景
✅ 多 Agent / 多客户端复用 :同一套工具既给 LangChain Agent 用,又给 Claude Desktop 用 ✅ 接入热门外部服务 :GitHub、Slack、Notion、Postgres 都有现成 Server,白嫖即可 ✅ 多人协作的长期项目 :Server 有版本管理、权限隔离、统一鉴权,比散落各处的 @tool 好维护 ✅ 需要能力热插拔:不改 Agent 代码,通过改 Server 配置就能增减工具
5.3 混合方案(生产环境推荐)
真实项目往往是两者混用:
- 底层能力用 MCP:Server 化封装外部系统(数据库、内部 API、第三方服务)
- 胶水逻辑用
@tool:一些简单的本地函数、格式转换、业务特定计算,没必要上 MCP
举个例子------一个内部知识库 Agent:
python
# MCP 提供外部能力
client = MultiServerMCPClient({
"company_wiki": {"url": "http://internal-mcp/wiki", "transport": "http"},
"jira": {"url": "http://internal-mcp/jira", "transport": "http"},
})
mcp_tools = await client.get_tools()
# @tool 提供业务特定函数
@tool
def format_report(title: str, content: str) -> str:
"""按公司模板格式化一份报告"""
return f"# {title}\n\n发布日期:{datetime.now():%Y-%m-%d}\n\n{content}"
# 混合给 Agent
all_tools = mcp_tools + [format_report]
agent = create_agent(llm, tools=all_tools)
这才是真实项目的姿势------该抽象的抽象,该务实的务实。
5.4 一句话决策
工具要被"你一个人、一次性"用 → CLI Tool。 工具要被"多人、多端、长期"用 → MCP。
六、踩坑记录
最后分享一些实际用下来的踩坑经验。
6.1 CLI Tool 的坑
坑 1:子进程编码问题
中文系统 + Windows 下,subprocess 输出经常乱码。一定要显式指定编码:
ini
subprocess.run([...], capture_output=True, text=True, encoding="utf-8")
坑 2:忘了设超时
Agent 调 CLI 时如果某个命令卡住,整个 Agent 会一直等。永远加 timeout 参数:
ini
subprocess.run([...], timeout=30)
坑 3:shell=True 的安全风险
千万别为了方便用 shell=True + 字符串拼接,LLM 生成的参数可能带恶意内容。用 list 形式传参:
python
# ❌ 危险
subprocess.run(f"gh issue list --repo {repo}", shell=True)
# ✅ 安全
subprocess.run(["gh", "issue", "list", "--repo", repo])
6.2 MCP 的坑
坑 1:stdio vs HTTP 搞混
官方文档有句重要提示:stdio 主要是为本地单用户应用设计的 。如果你在 Web Server 里跑 Agent,多用户共用一个 MCP Server,务必用 HTTP / streamable_http,不要用 stdio------stdio 模式下每个用户都要起一个子进程,很快就爆资源。
makefile
# Web 服务里的正确姿势
client = MultiServerMCPClient({
"github": {
"url": "http://mcp-server:8080/mcp",
"transport": "streamable_http",
"headers": {"Authorization": f"Bearer {user_token}"}, # 用户级鉴权
},
})
坑 2:异步上下文管理
MultiServerMCPClient 是异步的。新手经常忘了 await,或者在同步函数里调:
csharp
# ❌ 错误
def my_func():
tools = client.get_tools() # 返回的是 coroutine
# ✅ 正确
async def my_func():
tools = await client.get_tools()
在 Jupyter 里可以直接 await,在普通脚本里要用 asyncio.run() 包一下。
坑 3:Server 起不来但没报错
stdio 模式下,如果 Server 脚本本身有 import 错误,你可能看到的是"工具列表为空"而不是明确的错误。调试的时候先手动跑一下 Server:
bash
python my_github_server.py
# 看看有没有报错,或者直接退出
坑 4:Token / 权限不要硬编码
最容易犯的错是把 GitHub Token 写死在 Server 代码里。正确做法是通过 env 传:
lua
client = MultiServerMCPClient({
"github": {
"command": "github-mcp-server",
"args": ["stdio"],
"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": os.environ["GITHUB_TOKEN"]},
},
})
多租户场景下更要注意------每个用户的 token 不同,要用 HTTP transport + 请求头传,不能塞在启动环境里。
坑 5:协议还在演进
MCP 这个协议 2024 年底才发布,API 和库版本更新很快。langchain-mcp-adapters 从 0.1 到 0.2 就有 breaking change(比如 transport 参数名)。建议锁版本:
ini
langchain-mcp-adapters==0.2.x
升级前看一眼 changelog。
写在最后
回到开头的问题:MCP 和 CLI,到底该选哪个?
这篇文章写下来我的答案是------没有谁取代谁,是工具栈的演进。
- CLI Tool 是短路径:适合快速、内部、一次性
- MCP 是长期路径:适合长期、多端、团队协作
有意思的是,LangChain 官方文档里专门有句话提醒大家:
"Before using stdio in a web server context, evaluate whether there's a more appropriate solution. For example, do you actually need MCP? or can you get away with a simple
@tool?"
翻译一下:别为了用 MCP 而用 MCP 。如果 @tool 就能解决,那就 @tool。
技术选型最怕跟风,以终为始才靠谱------你的工具未来会被谁用、用多久、用多频繁,决定了你现在该怎么接。