MCP 协议入门:统一模型与外部工具的连接
前段时间我连续试了几个 Agent 框架。它们都能调用工具:一个框架把天气查询写成 tool,另一个框架把数据库查询包成 function,还有一个框架要求你实现一套自定义插件接口。单独看,每个方案都能跑;真正把它们放在一起,麻烦就来了。
同一个搜索能力,要为不同的模型、不同的 Agent 宿主重复接入。参数 Schema 要重写,错误格式要重写,权限判断也要重写。工具一多,连接代码比业务代码还抢戏。那一刻我意识到,Agent 生态缺的未必是更多工具,而是一种大家都能说的“工具语言”。
MCP,也就是 Model Context Protocol,解决的正是这类连接问题。它不是一个更大的 Prompt 模板,也不是一个自动让模型变聪明的框架。它更像 USB:设备本身各不相同,但只要遵守接口,主机就有机会识别和使用它。理解这一点很重要——协议负责把边界说清楚,业务系统仍然要负责正确性、安全和可恢复性。
一、先记住三层:Host、Client、Server
MCP 最容易让初学者混淆的地方,是“模型”和“Server”并不是一回事。一个典型的连接关系可以画成这样:
┌──────────────────────────────┐
│ Host:聊天应用 / IDE / Agent │
│ ┌──────────┐ ┌──────────┐ │
│ │ MCP │ │ MCP │ │
│ │ Client A │ │ Client B │ │
│ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼────────┘
│ │
▼ ▼
文件 Server 博客 Server
Host 是用户正在使用的应用,例如桌面聊天客户端、代码编辑器或企业内部 Agent。Host 负责管理模型会话、展示结果、决定什么时候询问用户确认,也负责创建和管理一个或多个 MCP Client。
Client 是 Host 里的一条协议连接。它知道如何与某个 MCP Server 完成初始化、发现能力、发送请求和接收通知。一个 Host 可以同时连接文件系统 Server、数据库 Server 和博客搜索 Server,但每条连接通常有自己的会话和权限边界。
Server 则是能力提供者。它可以是本地进程,也可以是远程服务,向 Client 暴露工具、资源或提示模板。Server 不应该假设自己能直接控制模型,更不应该把“模型一定会怎样调用我”当成安全前提。
这三层分开以后,很多设计问题就变得清楚了:模型负责决定是否需要能力,Host 负责用户交互和策略,Server 负责执行能力并重新校验权限。任何一层都不应该把自己的职责偷偷推给另一层。
二、MCP 暴露的不是只有工具
很多介绍 MCP 的文章只讲 Tools,这很容易把协议理解窄。实际设计时,至少要区分三类能力。
1. Tools:让模型发起动作
Tool 是可调用的函数,例如搜索博客、查询订单、创建日历事件。它一般有名称、描述、输入 Schema 和调用结果。Tool 可能产生副作用,所以创建、删除、发送、付款这类操作不能只因为“模型调用成功”就直接执行,通常需要用户确认或额外策略。
2. Resources:让应用读取上下文
Resource 更接近一个可读取的地址,例如某篇文章、一个配置文件或数据库中的只读记录。它强调“读取什么”,不等同于“执行什么”。把一份文档作为 Resource 暴露出来,和提供一个任意 SQL Tool,是完全不同的风险等级。
3. Prompts:复用交互模板
Prompt 可以把一套经过验证的任务模板暴露给 Host,例如“总结这份代码的风险”或“根据当前文档生成发布说明”。它不是把系统提示词偷偷塞给模型,而是让 Host 和用户能够发现、选择和填充一套明确的模板。
我在实践中最看重的是这三个词的边界:读取用 Resource,执行用 Tool,复用任务结构用 Prompt。边界清楚,权限、审计和用户确认才有落脚点;所有能力都塞进 Tool,最后一定会变成一把过宽的万能钥匙。
三、一次工具调用到底发生了什么
MCP 通常使用 JSON-RPC 风格的消息来表达请求、响应和通知。你不需要一开始就背完所有方法,但应该看懂一条完整链路:
1. Client 连接 Server
2. Client 发送 initialize,声明协议版本与客户端能力
3. Server 返回自己的协议版本与服务能力
4. Client 发送 initialized 通知
5. Client 请求 tools/list
6. Server 返回工具名称、描述与 inputSchema
7. 模型决定调用某个工具
8. Host 通过 Client 发送 tools/call
9. Server 校验参数、身份、权限并执行
10. Server 返回结构化结果,Host 再交给模型或用户
这里有两个经常被忽略的事实。
第一,tools/list 是能力发现,不是权限授予。Server 把一个工具列出来,不代表当前用户就有权执行它;真正调用时必须再次检查身份、租户、资源范围和风险等级。
第二,工具结果不是“模型说了算”的文本。返回内容最好包含明确的结构、来源和错误类型,让 Host 能够决定是继续推理、展示引用、请求确认,还是停止任务。只返回一段模糊字符串,后面很难审计。
四、一个最小的 TypeScript Server 骨架
下面的代码故意不绑定某个 SDK。先把协议层和业务层分开,之后换 SDK、换传输方式,业务函数仍然可以复用。
type SearchInput = {
query: string
limit?: number
}
type SearchResult = {
title: string
url: string
snippet: string
}
const tools = {
search_blog: {
description: '搜索公开发布的博客文章,只读,无副作用',
inputSchema: {
type: 'object',
additionalProperties: false,
required: ['query'],
properties: {
query: { type: 'string', minLength: 1, maxLength: 200 },
limit: { type: 'integer', minimum: 1, maximum: 20 },
},
},
},
}
async function searchBlog(input: SearchInput): Promise<SearchResult[]> {
const query = input.query.trim()
if (!query) throw new Error('query 不能为空')
const limit = Math.min(Math.max(input.limit ?? 5, 1), 20)
return blogIndex.searchPublicPosts(query, limit)
}
async function handleToolCall(name: string, args: unknown) {
if (name !== 'search_blog') {
throw new Error(`未知工具:${name}`)
}
// 这里还应接入 JSON Schema 校验与可信连接上下文中的权限检查。
const input = args as SearchInput
const results = await searchBlog(input)
return {
content: [{ type: 'text', text: JSON.stringify(results) }],
structuredContent: { results },
isError: false,
}
}
这段代码还不是完整的 MCP Server,因为真正的 Server 还需要传输层、初始化处理、tools/list 和 tools/call 的协议映射。但它先把最重要的分界线展示出来了:工具描述是协议元数据,搜索函数是业务能力,调用处理器是边界层。
如果把这三块揉成一个大函数,后续加鉴权、超时、审计和测试时就会很痛苦。协议适配器应该薄,业务函数应该可独立测试,安全策略应该有明确入口。这不是为了追求“架构漂亮”,而是因为工具一旦接入模型,错误会被自动放大。
五、传输方式不是小细节
本地 Server 常见的方式是通过标准输入输出与 Host 通信。它的优点是启动简单、权限容易绑定到本机进程;缺点是进程生命周期、日志污染和环境变量管理需要格外小心。尤其不要把调试日志随意写到 stdout,否则协议消息和日志混在一起,Client 看到的就不再是合法数据。
远程 Server 则需要考虑 HTTP、鉴权、连接复用、超时和断线重连。远程连接的好处是能力可以集中维护,坏处是攻击面和运维责任都会增加。无论使用哪种传输方式,超时、取消和请求 ID 都应该是设计的一部分。一个永远不结束的工具调用,会把整个 Agent 循环拖死。
我的建议是:本地原型先用最简单的传输跑通初始化、列工具和调用;准备共享给团队时,再补上身份认证、速率限制、审计日志和版本兼容。不要在第一天就做一个“万能远程平台”,那通常意味着还没有把真正的能力边界想明白。
六、MCP 不会自动替你解决安全问题
MCP 统一了连接方式,却没有替你判断一个工具是否值得调用。至少要认真处理下面几件事:
- 工具描述不能夸大能力,尤其不能把“只读搜索”描述成“可以访问所有数据”。
- 参数必须做 Schema 校验、长度限制和业务规范化,不能相信模型生成的每个字段。
- 身份和权限要从可信连接上下文获得,不能由模型在参数里自报家门。
- 有副作用的动作要区分预览和执行,必要时要求用户明确确认。
- 结果要带来源、版本和请求 ID,便于回放和审计。
- 对外部内容保持不信任,网页、文档和工具返回值都可能包含 Prompt Injection。
- Server 要设置超时、取消和幂等键,避免重试造成重复写入。
尤其要记住:工具调用链越长,越不能只在入口做一次鉴权。用户权限、租户范围和资源状态可能在执行前已经变化,关键操作需要在真正写入前再次确认。
七、我研究完 MCP 后留下的判断
我一开始以为 MCP 的价值是“让更多工具接入模型”。后来实际看了几套 Server,又自己拆了一遍消息流程,结论变了:MCP 真正重要的地方,是它迫使我们把能力写成可发现、可描述、可校验、可审计的接口。
这件事听起来朴素,却正是 Agent 从 Demo 走向系统的分水岭。以前我们常把工具调用写在 Prompt 旁边,模型知道一点,代码知道一点,权限又藏在另一个中间件里。出了问题,大家只能互相猜。协议化之后,至少可以明确回答:有哪些能力、输入是什么、谁能调用、返回什么、失败如何表达。
但协议不是魔法。它不能替你设计好数据库,也不能阻止模型误解业务,更不能替你承担高风险操作的责任。一个糟糕的万能 Server,即使披上 MCP 的外衣,仍然是糟糕的系统;一个职责单一、权限清楚、结果可追踪的 Server,哪怕只提供一个搜索工具,也已经是很好的开始。
如果你准备学习 MCP,我建议按这个顺序动手:先实现一个只读 Tool,再观察 initialize 和能力发现;接着增加 Resource 和引用;然后加入超时、错误分类和权限测试;最后才考虑远程部署和多 Server 编排。每一步都要能回答“它允许什么、拒绝什么、失败后怎么办”。
当模型开始调用你的工具时,你就不再只是写一个函数,而是在设计一条由模型、应用和真实世界共同参与的执行边界。边界越清楚,系统越可靠;边界越含糊,所谓智能越容易变成自动化事故。