MCP:从原理、源码、实战到企业落地,一篇彻底讲透 AI 世界的标准协议
作 者:吴佳浩Alben
撰稿时间:2026.7.15
更新时间:2026.7.20
这篇文章按照"一个工程师认识 MCP 的真实过程"来组织,而不是按"MCP 协议的组成"来组织:先建立认知(为什么要学)、再讲原理(是什么)、再动手实战(怎么写)、再做一次真实复盘(怎么改好)、最后落到企业级(怎么用)。 全文用我自己第一次写的真实项目 ------ OpenLog MCP(AI 日志分析平台的 MCP 封装)------ 贯穿始终。它能跑,但不够标准,这正是大多数人写第一个 MCP 时的真实状态,也是这篇文章最想讲清楚的东西。
第一部分:认知(为什么)
这一部分结束后,你应该已经认可一件事:MCP 值得学。
1 为什么 MCP 会突然火起来?
先不讲 MCP,讲历史。
- 最早,LLM 只会"续写文本",靠 Prompt 引导输出;
- 后来发现可以让模型"决定调用哪个函数",出现了 Function Calling;
- Function Calling 演化出更通用的 Tool Calling,模型可以调用任意结构化定义的工具;
- 各家平台开始做 Plugin 生态,试图让工具可插拔、可复用;
- 随着 Agent(能自主规划、多轮调用工具的应用)成为主流范式,"工具怎么统一接入"这个问题被无限放大------每个 Agent 框架都在重新发明一遍工具接入层;
- MCP 就是在这个节点上出现的:它不是一次突发奇想,而是 Agent 发展到这个阶段的必然产物。
MCP 不是突然冒出来的协议,它是"工具接入"这个问题被无数团队重复造轮子之后,必然收敛出的标准答案。
2 MCP 到底解决了什么问题?
一句话说清楚:
MCP(Model Context Protocol)是一套让 AI 统一调用外部工具的标准协议。
如果你写过 Agent,大概率经历过这几件事:想让 Agent 查一下 GitHub issue,接一遍 GitHub SDK;想操作 Docker,再接一遍 Docker SDK;想查数据库,再写一套 MySQL 连接和权限逻辑。换一个 Agent 框架,前面的活儿全部重来一遍。
有了 MCP 之后:
Agent 只依赖 MCP 协议本身,不关心背后是 GitHub 还是别的什么系统。任何 Agent(Claude Desktop、OpenClaw、Hermes、自研 Agent)都能接入任何 MCP Server,真正做到 Plug & Play。
3 MCP 和 Function Calling 有什么区别?
这是最容易混淆的一对概念。
Function Calling 是模型能力层面的机制:给模型一份函数签名(JSON Schema),模型根据当前对话决定"要不要调用、调用哪个、传什么参数"。这套机制是模型自己实现的能力,OpenAI、Anthropic、Google 各家的 Function Calling 接口格式都不完全一样。
但 Function Calling 本身不规定:这个"函数"部署在哪、怎么被发现、怎么跨应用复用、怎么鉴权。同一个函数定义,换一个模型或框架,往往要重新写一遍接入代码------这正是第 02 节里"if/else 耦合"问题的根源。
MCP 是在 Function Calling 之上,补齐了"传输层 + 发现层 + 生态层":统一的协议格式、统一的 Server 部署方式,让"工具"可以脱离具体某个 Agent 框架独立存在,被任意支持 MCP 的模型/Agent 发现和调用。
一句话:Function Calling 解决"模型怎么决定调用",MCP 解决"工具怎么被任何模型发现和调用"。
4 MCP 和 OpenAPI 有什么关系?
几乎每个懂后端的读者看到 MCP 的第一反应都是:
我都有 REST API、都有 OpenAPI/Swagger 文档了,为什么还要 MCP?
一句话讲清楚:
REST/OpenAPI 是给「程序」调用设计的;MCP 是给「AI」调用设计的。
OpenAPI 文档写得再详细,也是假设"调用方是一个懂 HTTP、能解析 JSON Schema、按文档一步步编码接入的程序员或程序"。而 LLM 面对一份几百个字段的 OpenAPI 文档时,既没法像人一样"理解语境",也没法像程序一样"按类型系统硬编码"------它需要的是一份为决策而生的描述:这个工具是干什么的、什么时候该用、传什么参数、会有什么副作用。这正是 MCP 里 Tool 的 Description 存在的意义。
所以准确的说法不是"MCP 取代 OpenAPI",而是:MCP 是在 OpenAPI 描述的能力之上,重新长出的一层"给 AI 看的接口层"。
5 为什么说 MCP 是 AI 世界的 USB(主流说法都是这么说 但是其实type-c可能更时尚一点 我个人认为)?
先看历史上已经存在的这些"标准",它们各自解决的是什么问题:
| 协议/规范 | 解决的问题 | 面向对象 |
|---|---|---|
| REST | 资源的增删改查如何通过 HTTP 表达 | 程序 调 程序 |
| GraphQL | 客户端如何按需查询数据,减少冗余字段 | 程序 调 程序 |
| gRPC | 高性能、强类型的跨服务调用 | 服务 调 服务 |
| OpenAPI | 如何描述一个 REST API 的结构,方便生成文档/客户端 | 人 / 工具链 |
| JSON-RPC | 如何用 JSON 表达一次远程过程调用 | 程序 调 程序 |
| MCP | LLM 如何发现、理解、调用外部能力 | AI 调 工具 |
前面几个协议标准化的都是"接口怎么描述、怎么传输",服务对象始终是程序或人。而 MCP 标准化的是完全不同的东西:
MCP 标准的不是 API,是「AI Tool」------一个专门给 LLM 消费的能力描述格式。
它填补的是一个真空地带,而不是在已有赛道里抢生意,这正是 MCP 能在短时间内被 Claude、OpenAI、Gemini 同时接纳的根本原因。用 USB 类比最直观:
不管外设是什么品牌,只要遵循 USB 标准,电脑就能识别、能用。MCP 也一样------不管后端是什么系统,只要遵循 MCP 协议封装,任何 Agent 都能接入。
第二部分:原理(是什么)
认知立住之后,才真正进入协议本身。这一部分不会一上来就讲 Tool,而是先把整体架构和全貌摆清楚。
6 MCP 整体架构
这是全网最容易画错的一张图------很多文章把 Client 和 Server 的边界画错,或者漏掉了"谁负责调用 LLM"这一层。
7 MCP Client 与 Server
- MCP Client 内嵌在 Agent 应用里(比如 Claude Desktop、OpenClaw 本身自带 Client),负责发起协议请求;
- MCP Server 是你写的那部分,负责"翻译"------把协议请求翻译成对具体业务 API 的调用。
Client 和 Server 之间只认 MCP 协议,互不关心对方内部怎么实现,这就是解耦的关键。也正因为这样的职责划分:
MCP Server 本质就是 AI 世界的 Adapter(适配层)。
它不生产能力,只做翻译------把已有系统的能力(REST API、数据库、命令行工具......)翻译成 LLM 能理解、能决策调用的结构化描述。
8 MCP Tool
Tool 是 MCP 里最高频使用的能力。这里先建立一个认知:LLM 根本不会读你的实现代码,它决策时只能看到三样东西:
bash
Tool Name(工具名)
Description(描述)
InputSchema(输入参数结构)
这决定了 Description 和 Schema 的质量,直接影响 Tool 会不会被正确调用------第四部分会用真实代码展开细讲。
9 MCP Resource
除了"可调用的工具",MCP Server 还可以暴露可读取的资源(比如一份文件、一段日志、一张配置表)。Resource 和 Tool 的区别在于:Tool 是"让 LLM 主动执行一个动作",Resource 是"直接把一段内容提供给 LLM 读取",不需要经过一次"调用决策"。适合暴露那些"读多改少、上下文本身就该带上"的内容。
10 MCP Prompt
Server 还可以预置一些提示词模板,供 Client 端直接复用。比如一个代码审查类的 MCP Server,可以内置一个"审查该 PR 的标准 Prompt 模板",减少 Agent 自己重新设计 Prompt 的成本,也保证团队内 Prompt 风格的一致性。
11 Sampling
这是更进阶的能力,允许 MCP Server 反过来向 Client 请求"帮我调用一次 LLM 采样"。适用于 Server 内部本身也需要 AI 能力辅助决策的场景------比如一个日志分析 MCP Server,在处理某个 Tool 调用的过程中,可能需要临时借助 LLM 做一次归纳,这时就可以通过 Sampling 反向请求 Client 侧的模型能力,而不用自己额外接一套 LLM API Key。
大部分教程只讲 Tool,是因为 Tool 确实是最高频、最核心的能力,但理解 Resource / Prompt / Sampling 能让你知道:MCP 不只是"工具调用协议",它是一整套"AI 与外部世界交互"的协议族。
12 一次完整调用流程
把 initialize、list_tools、call_tool 串起来看一次完整的会话生命周期:
再展开看一次真实的用户请求,从提问到拿到答案,中间到底发生了什么:
注意关键点:LLM 自己并不知道 Docker API 怎么调 ,它只知道"有一个叫 get_docker_containers 的工具,我可以调用它"。真正的翻译工作,全部发生在 MCP Server 里。
13 三种通信方式,以及协议的演进
| 方式 | 场景 | 现状 |
|---|---|---|
| Stdio | 本地进程,Agent 和 MCP Server 在同一台机器 | 官方推荐用于本地工具(如 Claude Desktop 本地插件) |
| SSE | 早期的远程通信方案 | 已被标记为历史方案,不推荐新项目使用 |
| Streamable HTTP | 远程部署,多客户端共享 | 官方目前推荐的远程通信标准 |
简单判断标准:只在本机跑、给自己用 → Stdio ;要部署成服务、给团队/多个 Agent 共用 → Streamable HTTP。企业级场景几乎都会走 HTTP,因为 Stdio 依赖进程间管道,没法做鉴权网关、没法做多租户、也没法水平扩展。
把时间线拉长看,MCP 本身也在持续演进:
传输方式从"本地进程管道"走向"标准 HTTP",本身就是在为企业级场景铺路------下一步要补齐的,正是标准化的鉴权(Authorization)和治理层(Gateway、Registry),这也是第五部分要重点展开的内容。
第三部分:实战(怎么写)
从这一部分开始进入代码,而且这次不满足于骨架------直接展示一个完整可跑的 Tool,从目录结构到真实的 API 调用。用的是我自己第一版写的 OpenLog MCP ------一个把"AI 日志分析平台"REST API 封装成 MCP 工具的项目。先声明一句:这是我第一次写 MCP 时的版本,能跑,但不是最标准的写法,第四部分会带着大家一起重构它。
14 一个最简单的 MCP Server:目录结构与职责划分
先讲清楚职责划分:
一个 MCP Server 本质只干两件事:告诉 LLM 我有哪些工具 (list_tools),LLM 决定调用后真正去执行 (call_tool)。
我原始的 OpenLog MCP 其实是把所有东西写进了一个单文件 openlog_mcp.py,这也是它后面被点名批评的问题之一。一个更规范的目录结构应该长这样:
bash
openlog-mcp/
├── server.py # 入口:list_tools / call_tool 注册与分发
├── tools/
│ ├── logs.py # 日志相关 Tool 定义
│ ├── docker.py # Docker 相关 Tool 定义
│ └── monitor.py # 监控相关 Tool 定义
├── api.py # 统一的 api_call 封装(异步 + 统一返回结构)
└── config.py # 集中管理环境变量、默认值、启动校验
tools/ 按业务领域拆分文件,api.py 只负责"怎么调用后端",config.py 只负责"配置从哪来、要不要校验"------每个文件职责单一,这一点会在第四部分反复用到。
15 Tool 注册:完整的 list_tools 实现
以 Docker 这一个领域为例,一个真实可跑的 Tool 定义长这样(节选自我的原始实现):
python
Tool(
name="get_docker_containers",
description="获取所有 Docker 容器列表及状态",
inputSchema={"type": "object", "properties": {}}
),
Tool(
name="operate_docker_container",
description="操作 Docker 容器:start/stop/restart",
inputSchema={
"type": "object",
"properties": {
"source_id": {"type": "string", "description": "Docker 源 ID"},
"container_id": {"type": "string", "description": "容器 ID 或名称"},
"operation": {"type": "string", "enum": ["start", "stop", "restart"], "description": "操作类型"}
},
"required": ["source_id", "container_id", "operation"]
}
),
Tool(
name="get_container_logs",
description="获取 Docker 容器日志",
inputSchema={
"type": "object",
"properties": {
"source_id": {"type": "string"},
"container_id": {"type": "string"},
"tail": {"type": "integer", "description": "返回最后 N 行,默认100"}
},
"required": ["source_id", "container_id"]
}
)
list_tools 返回的是一份"能力清单",Client 在 initialize 之后会主动拉取这份清单,交给 LLM 决策。可以看到,operate_docker_container 已经用 enum 限定了 operation 的取值范围------这是原始代码里为数不多做对了的地方。
16 Tool 调用:完整的 call_tool 与 api_call 实现
call_tool 是真正干活的地方,原始实现长这样:
python
def api_call(method, path, data=None):
"""调用 OpenLog REST API"""
url = f"{OPENLOG_URL}{path}"
headers = {"Content-Type": "application/json"}
if OPENLOG_TOKEN:
headers["Authorization"] = f"Bearer {OPENLOG_TOKEN}"
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
resp = urllib.request.urlopen(req, timeout=30)
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()[:200]}
except Exception as e:
return {"error": str(e)}
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "get_docker_containers":
result = api_call("GET", "/api/docker/containers")
elif name == "operate_docker_container":
result = api_call(
"POST",
f"/api/docker/{arguments['source_id']}/{arguments['container_id']}/{arguments['operation']}"
)
elif name == "get_container_logs":
result = api_call(
"GET",
f"/api/docker/containers/{arguments['source_id']}/{arguments['container_id']}/logs"
)
# ... 其余 11 个分支,模式完全一致
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
一次真实调用 get_docker_containers,api_call 会向 http://localhost:3003/api/docker/containers 发起 GET 请求,OpenLog 后端返回类似这样的 JSON:
json
{
"containers": [
{"id": "a1b2c3", "name": "openlog-api", "status": "running"},
{"id": "d4e5f6", "name": "openlog-worker", "status": "exited"}
]
}
17 返回结果:LLM 实际看到的样子
call_tool 最终把上面这段 JSON 原样 json.dumps 之后包进 TextContent 返回。也就是说,LLM 拿到的"工具执行结果",就是一段格式化后的原始 JSON 文本。这里先埋一个伏笔:不同 Tool 返回的字段结构完全不统一,get_docker_containers 返回的是 {"containers": [...]},另一些接口返回的可能是 {"error": "..."} 或裸数组------第四部分会讲为什么这样不够好。
18 接入 Claude Desktop / OpenClaw / Hermes 调试
写完 Server,跑起来的方式很统一:在对应 Agent 的 MCP 配置里,指定启动命令即可。以 Stdio 方式为例,配置大同小异:
json
{
"mcpServers": {
"openlog": {
"command": "python",
"args": ["/path/to/openlog-mcp/server.py"],
"env": { "OPENLOG_URL": "http://localhost:3003" }
}
}
}
- Claude Desktop:在设置里的 MCP 配置文件里加上这一段,重启客户端即可看到工具已挂载;
- OpenClaw / Hermes :这类自研或第三方 Agent 通常也内置了 MCP Client,配置方式类似------指定启动命令和环境变量,Agent 启动时会自动完成
initialize和list_tools握手。
三端调试的通用排查思路是一致的:先用官方提供的 MCP Inspector 工具确认 Server 能独立跑通,再确认 Agent 端的配置文件路径、环境变量是否正确,最后看 Agent 启动日志里有没有握手成功的记录。跑起来之后,效果是真实可用的:在 Claude Desktop 里问一句"帮我看看 Docker 容器状态",模型会自动选中 get_docker_containers,把上面那段 JSON 转述成"当前有 2 个容器,openlog-api 正在运行,openlog-worker 已停止"这样的自然语言回答。这也证明了 MCP 的开发门槛并不高------一个下午就能把已有系统封装成 MCP。但"能跑"和"写得规范"是两回事,接下来做一次真实复盘。
★★★★★ 特别篇:真正决定 MCP 好不好用的,不是代码,而是 Tool Design
这一章不是写代码,而是设计思想 ------也是整篇文章里最值钱的一章。它讲的其实已经不是"MCP"本身,而是一套更通用的方法论:如何设计 Tool。这套方法论不局限于 MCP,未来任何形态的 Agent 工具接入,都绕不开这几步。
很多人(包括我自己第一次写 OpenLog MCP 时)踩的坑,本质上都是同一个:一开始就写代码,把已有 API 一比一翻译成 Tool,跳过了设计阶段。真正应该走的七步是:

- 分析业务能力:先梳理这个系统能对外提供哪些"能力",而不是急着照抄现有的 API 列表------一个系统里未必所有 API 都值得暴露给 AI;
- 划分领域 :把梳理出的能力按业务领域分组(日志是一类,Docker 是一类,告警是一类),这一步决定了你最终会拆出几个 MCP,而不是一个大杂烩;
- 抽象 Tool :在每个领域内部,按"一个 Tool 只做一件事"的原则拆开,避免出现一个 Tool 里塞了多种操作、靠一个
type参数分支的情况; - 设计 Schema :明确每个参数的类型、是否必填、取值范围,能用
enum限定的就不要用自由字符串; - 编写 Description:把每个 Tool 的描述当 Prompt 来写,说明用途、使用场景、副作用;
- 统一返回结构 :所有 Tool 的返回都走同一套
{success, data, error}结构,让 LLM 不用每次重新猜字段; - 接入业务系统 :最后一步才是写
api_call,把前面设计好的 Tool 接到真实的业务 API 上。
代码写得好不好,决定这个 MCP 能不能跑;Tool 设计得好不好,决定这个 MCP 好不好用。 前者是工程问题,后者是产品问题------而后者恰恰是大部分教程完全不讲的部分。
第四部分:OpenLog MCP 复盘------第一次写 MCP,我踩过哪些坑?
这一部分不是抽象的"最佳实践清单",而是一次真实的 Code Review。我们直接拿第三部分展示的 OpenLog MCP 原始代码,对照"特别篇"的七步逐条复盘------先说明白:这份代码是我第一次接触 MCP 时写的,功能是跑通了的,线上也在用,但确实不是最标准的写法,正好带大家一起重构一遍。
21 Tool 如何拆分:我把 7 个领域塞进了一个 Server
原始代码里 list_tools() 一口气注册了 14 个工具,覆盖日志、监控、Docker、远程服务器、告警、AI 分析、系统设置 ------ 7 个完全不同的业务领域挤在同一个 Server("openlog") 里,违反了"特别篇"第二步"划分领域"的原则。
python
# 原始写法:日志、监控、Docker、告警、设置全部混在一个 Server 里
app = Server("openlog")
# get_logs / get_monitor_stats / get_docker_containers /
# get_machines / get_alerts / trigger_analysis / get_settings
# ------ 7 个领域,14 个工具,全部注册在同一个 app 上
改进方向:至少拆成 openlog-logs(日志分析)和 openlog-infra(Docker/机器/监控)两个 MCP,各自职责单一,权限也能分开管理,对应第 14 节里那份按 tools/ 子模块拆分的目录结构。
22 Description 怎么写:我写得像注释,不像 Prompt
python
# 原始写法:只说了能做什么,没说什么时候该用、有什么风险
description="操作 Docker 容器:start/stop/restart"
这条描述信息量不够,容易在边界情况下误判要不要调用(比如用户只是想"看看"容器状态,结果被联想成要"重启")。
python
# 更好的写法:说明输入范围、使用场景、副作用
description=(
"操作 Docker 容器的生命周期,支持 start/stop/restart 三种操作。"
"适用于用户明确要求启动、停止或重启某个容器的场景。"
"注意:restart 会导致容器内服务短暂中断,执行前建议先确认容器用途。"
)
一句话记住:Description 要当 Prompt 写,不是当函数注释写。
23 Schema 怎么设计:参数校验形同虚设
python
# 原始写法:source_id / container_id 取值没做任何存在性校验,直接拼进 URL 路径
result = api_call("POST", f"/api/docker/{arguments['source_id']}/{arguments['container_id']}/{arguments['operation']}")
问题有两个:一是取参数直接用方括号 arguments['source_id'],一旦 LLM 传参缺失会直接抛 KeyError,而不是给出可读的错误提示;二是 container_id 这类字段没有做格式校验,理论上存在路径穿越风险(比如混入 ../)。
改进方向:Schema 层面能用 enum、pattern 限定的就不要用自由字符串;代码层面取参数统一用 .get(),缺失时返回明确的错误信息而不是让异常直接冒出来。
24 一个 MCP 放多少 Tool:数量没超标,但领域超标了
经验值:一个 MCP 不超过 20 个 Tool 。OpenLog MCP 原始版本有 14 个,还没超过这条红线,但因为跨了 7 个领域,实际体验已经打折扣------LLM 在决策阶段要在混杂领域的工具里挑选,选择正确率会下降。数量红线是表象,领域内聚才是本质:宁可多拆几个小 MCP,也不要把无关领域的能力塞进同一个清单。
25 如何返回统一数据:LLM 每次都要重新猜字段
python
# 原始写法:不同接口返回的结构完全不统一,成功/失败也没有统一字段
result = api_call("GET", f"/api/logs{'?' + qs if qs else ''}")
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
呼应第 17 节埋的伏笔:14 个工具对应的 api_call 返回什么结构,完全取决于后端接口本身长什么样,MCP 层没有做任何统一。
改进方向:在 call_tool 出口统一包一层 {"success": bool, "data": ..., "error": ...},让 LLM 每次拿到的结构都是一致的。
26 如何做异常处理:同步阻塞 + 错误信息裸奔
python
# 原始写法:异常信息直接原样返回,可能带出内部路径、堆栈等敏感信息
except Exception as e:
return [TextContent(type="text", text=json.dumps({"error": str(e)}, ensure_ascii=False))]
str(e) 有可能包含内部服务地址、文件路径等信息,直接透传给 LLM(进而可能出现在对话里)不太合适。同时,call_tool 是 async 函数,但原始代码里 api_call 用的是同步阻塞的 urllib:
python
def api_call(method, path, data=None):
req = urllib.request.Request(url, data=body, headers=headers, method=method)
resp = urllib.request.urlopen(req, timeout=30) # 阻塞事件循环
如果同时有多个工具调用在排队,这一个请求会把整个事件循环卡住,其余请求只能干等。
改进方向:
python
import httpx
async def api_call(method: str, path: str, data: dict | None = None) -> dict:
"""统一的异步调用 + 统一返回结构 + 错误脱敏"""
async with httpx.AsyncClient(timeout=30) as client:
try:
resp = await client.request(method, f"{OPENLOG_URL}{path}", json=data, headers=HEADERS)
resp.raise_for_status()
return {"success": True, "data": resp.json(), "error": None}
except httpx.HTTPStatusError as e:
return {"success": False, "data": None, "error": f"后端返回错误状态码 {e.response.status_code}"}
except Exception:
logger.exception("api_call failed") # 详细异常只记本地日志,不透传给 LLM
return {"success": False, "data": None, "error": "后端服务暂时不可用"}
换成 httpx.AsyncClient 做真正的异步请求,同时对外只返回脱敏后的错误分类,详细堆栈只记本地日志。
27 如何做权限控制:破坏性操作和只读查询走的是同一条路
原始代码里,operate_docker_container 这类破坏性操作 (start/stop/restart)和 get_docker_containers 这类只读查询,走的是完全一样的调用路径------只要 LLM 决定调用,就会真的执行,中间没有任何权限分级或二次确认机制。
改进方向:区分只读工具和操作型工具,操作型工具建议在 Tool 层面标记风险等级,并要求更高权限的 Token,或者在业务层加一道"需要用户显式确认"的环节,而不是让 LLM 单方面决定就直接生效。企业级场景下,这一层通常会收口到 Gateway,第五部分会展开讲。
28 如何做日志:日志平台自己不打日志
一个有点讽刺的事实:OpenLog MCP 本身是"日志分析平台"的封装,但 Server 自己却没有输出任何运行日志。一旦线上调用失败,只能靠猜测排查。
改进方向:至少要记录每次 call_tool 的调用参数(脱敏后)、耗时、成功/失败状态,方便事后排查问题,这也是第 26 节改进代码里 logger.exception 的用意所在。
29 如何做配置:Token 默认为空且无校验
python
OPENLOG_URL = os.getenv("OPENLOG_URL", "http://localhost:3003")
OPENLOG_TOKEN = os.getenv("OPENLOG_TOKEN", "") # localhost 免鉴权
OPENLOG_TOKEN 默认给空字符串,且没有启动时的校验;limit=100 这样的默认值分散写在多个 call_tool 分支里,属于典型的魔法数字散落问题。
改进方向:统一一个 config.py 模块集中管理默认值和校验逻辑(对应第 14 节的目录结构),启动时如果关键配置缺失,应该给出明确报错而不是静默运行------尤其是部署到非 localhost 环境时,空 Token 意味着完全没有鉴权,这是一个容易被忽略的安全隐患。
第五部分:企业落地(怎么用)
30 企业为什么需要 MCP
很多 CTO 并不关心 Tool、Description、Schema 这些实现细节,他们只关心一个问题:为什么值得投入资源做这件事?
以前,企业每接一个 Agent 场景,都要为每个业务系统单独开发一套接入逻辑:
现在,只需要把每个业务系统各自封装一次 MCP,剩下的接入工作就不用再重复:
企业以后不用为每一个新 Agent 重新开发一遍接口,封装一次 MCP,所有 Agent 都能复用。 它把"N 个系统 × M 个 Agent"的接入成本,从乘法关系压缩成了加法关系。
31 如何包装已有 REST API
答案是:不需要推倒重来,业务零侵入。
OpenLog MCP 本身就是最好的例子------OpenLog 后端是一个独立的 Express 服务,MCP Server 完全不碰后端一行代码,只是在外面加了一层"翻译层",把 REST 接口包装成 Tool。呼应第 07 节的结论:MCP Server 是 Adapter,天然就该是新增的适配层,不是对原系统的侵入式改造。
32 SpringBoot 如何接 MCP
企业 Java 系统最常见的诉求。核心思路不是改造 SpringBoot 本身,而是新增一个独立的 MCP Server 进程,在 Tool 实现里通过 HTTP Client 回调 SpringBoot 已有的 @RestController 接口。Spring AI 生态已经提供了 MCP Server/Client 相关的 Starter 依赖,可以把已有接口逐个包装成 Tool,不需要脱离 Spring 生态另起炉灶。
33 Go 如何接 MCP
Go 生态有官方及社区维护的 MCP SDK,思路和 SpringBoot 一致:写一个独立的 MCP Server 进程,在 call_tool 里转发调用已有 Go 服务的 HTTP/gRPC 接口。Go 天生的并发模型和轻量协程,反而很适合承载"多个 Tool 并发转发调用"这种场景,能天然规避第 26 节讲的同步阻塞问题。
34 Python 如何接 MCP
就是本文 OpenLog MCP 的例子,用官方 mcp Python SDK 最省心:list_tools / call_tool 两个装饰器即可搭起骨架,配合 stdio_server 或 HTTP 方式对外暴露。Python 生态的 SDK 成熟度目前是几种语言里最高的,适合快速验证和原型开发。
35 一个 MCP 的完整生命周期
企业里一个 MCP Server 从诞生到退役,通常要经历这样一条链路:
大部分团队只关注"开发"和"调用"这两个环节,中间的"注册""接入""监控""升级""废弃"往往是空白的------这正是下面几节要补齐的内容。
36 企业如何建设 MCP 平台
企业级场景通常不是一个 MCP,而是一堆 MCP:
这里有一个非常常见的坑:很多团队第一反应是写一个"万能 MCP",把 100 个工具都塞进一个 Server 里 。这看起来省事,实际上会造成 LLM 决策正确率下降、权限无法细粒度控制、任何改动都要重新发布整个 Server。正确做法是按领域拆分,上层用 Gateway 统一接入。
37 MCP Gateway
企业级部署中,Gateway 承担统一入口的职责:统一鉴权 (不需要每个 MCP Server 各自实现一套认证逻辑)、统一限流与审计日志 、按用户/团队做访问控制、把多个 MCP Server 聚合成一份对 Agent 可见的清单。这也是第 27 节"权限控制"在企业场景下的落地方式------单个 MCP Server 内部做不到的细粒度权限,交给 Gateway 层统一收口。
38 MCP Registry
Registry 解决的是"治理"问题:企业内部到底有多少个 MCP Server、分别是谁维护的、当前版本是什么、是否还在被使用。类似企业内部的"API 市场"------团队开发新 Agent 时,先去 Registry 查有没有现成的 MCP 可用,而不是重新造一个轮子。没有 Registry 的企业,往往会在半年后发现团队里悄悄长出了三四个功能重叠的 MCP Server,谁都不知道该用哪个。
39 MCP 权限:OAuth、Token、RBAC、Tool 白名单
第 27 节讲的是单个 MCP Server 内部该有的权限意识,企业级场景需要一整套体系:
- OAuth:Agent 代表某个真实用户去调用 MCP 时,应该走标准的 OAuth 授权流程,而不是一个共享的静态 Token------这样才能知道"是谁在通过 Agent 做这件事";
- Token:服务间调用(比如 Gateway 到具体 MCP Server)适合用短生命周期的 Token,而不是像 OpenLog MCP 原始代码那样用一个永不过期、默认为空的静态字符串;
- RBAC:不同角色(普通员工 / 运维 / 管理员)能看到、能调用的 Tool 集合应该不一样,这个差异化应该在 Gateway 层统一配置,而不是让每个 MCP Server 各自实现一套判断逻辑;
- Tool 白名单:对高风险操作型 Tool(比如第 27 节提到的容器重启),可以在 Gateway 层维护一份白名单,只有被显式授权的 Agent/用户组合才能调用。
40 MCP 可观测性:企业最终都会问的几个指标
MCP 平台跑起来之后,企业几乎必然会问:"这些 MCP 到底被用得怎么样?"常见的可观测性指标包括:
- Tool 调用次数:哪些 Tool 高频被用、哪些几乎没人用------后者往往是设计阶段"过度设计"的信号;
- 成功率 / 失败率:定位不稳定的 Tool 或后端依赖,呼应第 28 节"日志"里提到的调用记录;
- 耗时:identifying 哪些 Tool 拖慢了 Agent 的响应速度,尤其是第 26 节讲的同步阻塞问题,在可观测性数据上会直接体现为耗时异常;
- Token 消耗:Tool 返回的内容越冗长、结构越不统一(呼应第 25 节),LLM 消耗的 Token 就越多,这也是一项容易被忽略的成本项。
这些指标通常也是收口在 Gateway 层统一采集,而不是要求每个 MCP Server 自己实现一套监控上报逻辑。
41 企业最佳实践
汇总一下前面几部分的核心结论:
- 一个领域一个 MCP,不要大杂烩(第 21、24 节)
- Description 当 Prompt 写,Schema 做好校验(第 22、23 节)
- 返回结构统一,异常信息脱敏(第 25、26 节)
- 破坏性操作要有权限分级和二次确认(第 27、39 节)
- MCP Server 要有基本的可观测性(第 28、40 节)
- 配置集中管理,关键配置缺失时启动即报错(第 29 节)
- 企业级部署统一走 Gateway,谁在维护什么 MCP 交给 Registry 管理(第 37、38 节)
42 MCP 到底是不是万能的?
看完前面这么多内容,很容易产生一个误解:以后是不是什么都该用 MCP?REST 是不是就没用了? 并不是。
- 直接调用 REST 更简单的场景:如果调用方就是一段确定性的程序代码,业务逻辑清晰、不需要"决策",直接调 REST 接口就够了,包一层 MCP 反而多此一举------MCP 解决的是"AI 需要自主决定调用哪个能力"这个问题,不是所有调用场景都存在这个问题;
- Function Calling 就够用的场景:如果这个能力只会被一个特定的 Agent 使用、不需要跨框架复用、也不涉及独立部署和治理,直接在应用内用 Function Calling 定义一个函数即可,没必要为它单独起一个 MCP Server 进程;
- 值得写 MCP 的场景:这个能力需要被多个 Agent / 多个团队复用,或者需要独立部署、独立鉴权、独立生命周期管理,这时候封装成 MCP 才划算------第 30 节讲的"N 个系统 × M 个 Agent"的成本压缩逻辑,只有在真的存在"多对多"关系时才成立。
MCP 不是要取代 REST 或 Function Calling,它解决的是一个更具体的问题:当"多个 AI 调用方"和"多个能力提供方"同时存在时,怎么让它们高效地互相发现和调用。 场景不满足这个前提,就没必要为了赶时髦而写 MCP。
43 MCP 未来的发展
最后拔高一下视角。几个值得关注的方向:
- MCP 会不会真正成为 AI 世界的"USB 标准",任何工具、任何 Agent 即插即用?
- 会不会出现类似应用商店的 MCP 生态,企业和个人都能发布/订阅 MCP?
- 企业内部系统会不会默认标配一层 MCP,作为对外提供 AI 能力的标准接口?
- Agent 生态的竞争,会不会从"模型能力"逐渐转向"谁的 MCP 生态更丰富"?
REST 定义了软件之间如何通信;MCP 正在尝试定义 AI 与现实世界如何交互。它最终会不会成为 AI 世界的 USB,没有人能确定,但至少今天,它已经成为越来越多 Agent 的共同语言。
FAQ:读者常问的几个问题
Q:MCP 和 OpenAPI 有什么区别? REST/OpenAPI 面向程序调用,MCP 面向 AI 决策调用,详见第 04 节。
Q:MCP 和 Function Calling 有什么区别? Function Calling 是模型决定"要不要调用"的能力,MCP 是让工具能被任意模型发现和调用的协议层,详见第 03 节。
Q:MCP 为什么不用 WebSocket? WebSocket 适合双向长连接,但实现和部署复杂度更高。Streamable HTTP 已经能满足"流式返回 + 请求响应"的场景,同时保持了无状态特性,更方便水平扩展和走标准的 HTTP 网关,这也是官方选择它而不是 WebSocket 的原因之一。
Q:HTTP 和 Stdio 怎么选? 本地自用选 Stdio,要给团队或多个 Agent 共用就选 Streamable HTTP,详见第 13 节。
Q:一个系统应该写几个 MCP? 按业务领域拆分,一个领域一个 MCP,不要写"万能 MCP",详见"特别篇"第二步和第 36 节。
Q:一个 MCP 应该有多少 Tool? 经验值不超过 20 个,但数量红线是表象,领域内聚才是本质,详见第 24 节。
Q:MCP 会取代 REST API 吗? 不会。REST 继续服务程序间调用,MCP 是在其之上新增的、面向 AI 的消费层,两者并存,详见第 04、42 节。
Q:Skill 和 MCP 有什么关系? Skill 负责"怎么做"(流程编排),MCP 负责"调用什么"(能力封装)。可以理解为:Skill 是剧本,MCP 是演员能做的动作清单。
写在最后
这篇文章按"认知 → 原理 → 实战 → 复盘 → 企业落地"五个阶段展开,中间穿插了一份真实的、第一次写就跑通但不够规范的代码(OpenLog MCP)------从完整的 Tool 实现,到设计方法论,再到逐条代码复盘,最后落到企业级的权限、可观测性、Gateway、Registry 方案。
如果你也是刚开始写 MCP,不用追求一上来就写出"教科书级"的代码------先建立认知,按"特别篇"的七步设计,再对照第四部分逐一打磨,最后参考第五部分往企业级去演进,也别忘了回头看看第 42 节,想清楚这次是不是真的需要 MCP。这本身就是大多数人写第一个 MCP 的必经之路。