MCP 实践:把本地工具接进 LLM 客户端
MCP 解决的是工具接入的重复劳动。本文用最小 server 讲清传输方式、三个原语、权限边界和一套可复用的调试顺序。
每接一个新客户端就把工具重写一遍,是 LLM 应用里最浪费的工程之一。MCP(Model Context Protocol)用一套协议把这个矩阵拆开:客户端实现一次协议,工具方写一个服务端,两边就能对接。
一、它解决的是一张矩阵
没有统一协议时,N 个客户端要接 M 个工具,就得写 N×M 套适配:每个客户端一套函数定义、一套参数校验、一套错误处理。MCP 把它拆成两层,客户端只实现协议,工具方只实现服务端,两者用 JSON-RPC 通信,接入成本从 N×M 降到 N+M。
这也是它在 2026 年迅速铺开的原因:桌面客户端、编辑器插件和自研 Agent 大多已经支持 MCP,所以“写一个 server”往往比“在每个客户端里塞一份函数定义”更划算。
二、三个原语,先想清楚用哪个
- tools:模型可以主动调用的操作,有输入 schema,可能产生副作用。绝大多数集成用的是它。
- resources:可被读取的数据,比如文件、数据库行、日志片段,通常由用户或客户端挑选后注入上下文。
- prompts:预置的提示词模板,适合把团队常用的工作流固化下来。
简单判断:需要模型做决定并执行动作,用 tools;只是把数据喂给模型,用 resources。把纯只读数据做成 tool,模型不得不多调一次,还平白多一次失败机会。
三、两种传输
stdio:客户端把 server 作为子进程启动,通过标准输入输出收发 JSON-RPC。适合本地工具,不需要端口和鉴权,但 server 的权限等于当前用户,写文件、读密钥都挡不住。
HTTP + SSE:server 独立部署,客户端通过 HTTP 发送请求、用 SSE 接收服务端推送。适合团队共享的工具,需要自己处理鉴权、限流和超时。
选择标准很直接:工具跑在谁的数据上、要不要多人共用。本地笔记、本地代码库走 stdio;公司内部的查询服务走 HTTP。
四、最小 server 长什么样
协议本身只是一组 JSON-RPC 方法:握手 initialize,能力声明 tools/list,调用 tools/call。用官方 SDK 写,核心就是注册两个回调:
# python 伪代码,SDK 版本不同,接口以官方文档为准
server = Server("notes")
@server.list_tools()
def list_tools():
return [Tool(
name="search_notes",
description="按关键词搜索本地笔记,只读。",
inputSchema={"type": "object",
"properties": {"q": {"type": "string"}},
"required": ["q"]},
)]
@server.call_tool()
def call_tool(name, args):
if name != "search_notes":
raise ValueError(f"unknown tool: {name}")
hits = index.search(args["q"], limit=5)
return [TextContent(type="text", text=json.dumps(hits, ensure_ascii=False))]
server.run_stdio()
两个容易踩的点:stdout 只能输出协议消息,任何日志、print 都会污染 JSON-RPC 流,必须写到 stderr;tools/list 要返回完整 schema,客户端靠它生成给模型的函数定义,description 写不好,模型就不会用。
五、客户端侧配置
大多数客户端用一份 JSON 描述 server 怎么启动:
{
"mcpServers": {
"notes": {
"command": "/usr/local/bin/notes-mcp",
"args": ["--index", "/home/me/notes"],
"env": { "NOTES_READONLY": "1" }
}
}
}
stdio 模式下,这份配置就是安全边界:能启动什么程序、带什么参数、注入什么环境变量,全在这里决定。不要让它变成用户随手可改、却没有任何校验的文件。
六、安全:stdio 等于把 shell 交出去
MCP server 是被客户端拉起的本地进程,权限就是启动它的用户。边界要提前想清楚:
- 只读优先。先做检索类工具,确认稳定后再考虑写操作。
- 工具白名单加参数校验,和普通 API 一样做,别因为是“本地”就放松。
- 写操作要二次确认,尤其涉及删除、覆盖、对外发请求。
- 资源内容不可信。网页、邮件、第三方接口返回的文字里如果出现指令,模型可能当真,执行层要按数据对待。
- 记录调用日志,出问题能回溯是哪一次调用改了什么。
七、调试顺序
协议问题常常表现为“客户端里就是看不到工具”,按这个顺序查最快:
- 手动跑 server,确认进程能启动、不崩溃、stdout 没有多余输出。
- 发一次
initialize,看返回的 capabilities 是否包含 tools。 - 调
tools/list,确认 schema 合法、JSON 可解析。 - 再调
tools/call,确认返回结构符合 content 数组约定。 - 最后接客户端,看它的日志里有没有握手失败或工具名冲突。
JSON-RPC 的 id 类型(数字还是字符串)和错误字段的位置,是最常见的两处不一致。对着规范用最简脚本调一遍,比在客户端里反复猜快得多。
八、什么时候不该用
如果只有一两个固定调用、链路要求毫秒级延迟,或者在强事务流程里,直接把函数调用写死在应用里更简单:少一层进程、少一次握手、少一处日志拼接。协议的价值在复用和生态,而不是“更先进”。