这篇笔记是我在连续研究了大半个月MCP(Model Context Protocol,模型上下文协议)之后整理出来的。说实话,第一次看到这个缩写我还以为是某个消息通信协议的缩写,深入进去才发现,这根本就是大模型应用开发的一个分水岭级别的东西。如果你正在做大模型应用、Agent开发、或者单纯想知道AI助手是如何"调用工具"的,这篇笔记应该能帮你在几个小时内建立起一个相对完整且可落地的认知框架。
我不想把MCP讲成一堆抽象概念,也不会只贴官方文档的翻译。我会直接告诉你它解决了什么问题、核心设计是怎么考虑的、我实际动手写Server时发生了什么、踩了什么坑、以及为什么我认为它对AI应用的未来是个关键基础设施。文章里所有代码示例,都是我自己在本地调试通过的真实案例,你可以直接拿来改一改就跑起来。
1. 为什么一个协议会值得你专门写学习笔记
先说说背景。在MCP出现之前,如果你想让一个AI模型去查询某个数据库、调用某个业务系统、读取某个文件目录,你通常要自己写一套"函数调用(Function Calling)"机制,然后根据每家模型厂商的格式去适配。比如有的模型给一个JSON Schema做工具描述,有的模型要求在系统提示词里写死工具列表,有的模型需要你自定义特殊的输出格式。一个稍微复杂点的AI应用,光是在"工具接入"这一层,就要写大量胶水代码,而且这些代码还不通用,换个模型就要重新适配。
MCP想做的事情非常明确:把AI应用和外部工具之间的交互方式标准化。你可以把它理解为AI领域的"USB接口"。USB让鼠标、键盘、U盘能即插即用,不管它们是谁生产的;MCP则是让AI应用能即插即用地对接各类数据源和工具服务,不管服务端是用什么语言写的、部署在哪里。
这个思路听起来不复杂,但它解决的实际痛点非常真实:
- 模型厂商之间的工具调用格式完全不统一,MCP提供了一套中立标准。
- 一套工具服务如果按MCP标准暴露,就可以被任何支持MCP的AI应用直接使用,不用重复开发。
- 工具服务可以进行集中部署和权限管控,而不是把各种API密钥散落在提示词或前端代码里。
我当时为什么会觉得这个值得专门写笔记?因为在一个模拟项目里,我需要让AI助手访问本地的一堆Markdown笔记、SQLite数据库、还有一个内部API。传统思路下,我要分别为这三个数据源写三套访问逻辑,再为编排层写适配代码,工程量不小。后面改成MCP方案后,三个数据源都包成了MCP Server,AI应用侧一个标准客户端搞定,代码量少了一大截。这个体验让我意识到,MCP对AI应用的可维护性提升是结构性的,而不仅仅是节省了几行代码。
这篇笔记适合谁?如果你正在用主流AI编程助手、正在开发自己的AI Agent、或者只是好奇"AI怎么才能真正操作外部工具",都值得往下看。基础要求不高,懂一点Python或者Node.js,能运行终端命令就行。
2. 把MCP拆开看:四个角色和三个核心概念
MCP的架构看起来有点抽象,但如果理解了它的角色划分,后面写代码就会很顺畅。整个体系的角色和概念不算多,也没有复杂到需要专门画图才能理解的程度,用文字完全可以讲透。
2.1 四个角色:Host、Client、Server、Transport
MCP体系里定义了清晰的参与者角色,它们在一次完整的工具调用中各司其职。搞清楚这四个角色,是理解MCP的第一步。
- MCP Host(宿主应用):也就是用户正在使用的那款AI应用,比如某个AI编程助手、某个智能IDE、某个能对话的桌面应用。宿主应用是逻辑入口,负责理解用户的意图,并决定调用什么工具。宿主自身不做工具的具体实现,它只消费工具。
- MCP Client(客户端):在宿主进程内运行的一个组件模块,负责与MCP Server建立连接、维护会话、发送请求、接收结果。通常一个宿主可以管理多个客户端,一个客户端对应一个MCP Server连接。
- MCP Server(服务器):提供工具、资源或提示词的服务端程序。它最核心的职责是把外部能力封装成标准化的接口。这个Server可以是你自己写的小脚本,也可以是第三方提供的成熟服务。
- Transport(传输层):Client和Server之间的通信通道。目前最常见的是stdio(标准输入输出,适合本地子进程)和HTTP+SSE(适合远程服务)两种方式。传输层是MCP灵活性的关键之一,因为不同的部署场景需要不同传输方式。
我第一次看这些概念时,脑子里最大的困惑是:"Client和Host有什么区别?"后来用了个日常类比才真正想通:Host是餐馆本身,Client是服务员,Server是后厨,Transport是传菜的通道。顾客(用户)在餐馆里(Host)向服务员(Client)点菜,服务员把需求传给后厨(Server),做好后原路端回来。有了这套分工,后厨加了新菜,顾客不需要知道后厨内部怎么变,服务员按照固定流程端上桌就行。
2.2 三个核心概念:Tools、Resources、Prompts
角色之外,MCP还定义了三种能力单元,这是Server对外暴露的具体内容。
- Tools(工具):这是最常用的能力,对应"让AI执行一个动作"。比如"查询天气""创建待办事项""执行SQL",每个工具都有一个名字、一段描述、以及输入参数Schema。AI模型会根据用户请求自动决定是否调用某个工具,并在调用后处理返回结果。
- Resources(资源):对应"让AI读取结构化内容"。资源可以是文件内容、数据库记录、API响应,甚至是错误日志。和Tools不同,Resources通常不是主动动作,更像是可以被客户端检索的数据来源。协议里定义了资源的URI格式,比如file:///文档路径或db://记录ID,客户端可以像浏览文件一样获取资源。
- Prompts(提示词模板):对应"可复用的交互模板"。服务端可以预先定义一组提示词模板,客户端调用后生成一组结构化消息,引导模型按照指定流程执行任务。举个例子,服务端可以定义一个"代码审查"提示词,客户端调用后模型就会被要求按照特定角度审查代码。
这三种能力的核心逻辑是分层解耦:工具负责动手,资源负责查资料,提示词负责引导思路。一个功能完整的MCP Server可以同时暴露这三种能力,也可以只暴露其中一种。需求简单时千万不要过度设计,先从一个顺手的Tool做起就行。
2.3 MCP的消息模型
还有一个容易被忽略但很重要的设计细节:MCP的消息格式基于JSON-RPC 2.0。
也就是说,所有请求和响应都是标准JSON结构,消息里包含method、params、id等字段。这跟我以前做微服务时的自定义TCP协议完全不同,这样好处很明显:调试时可以随便用一个HTTP工具直接查看消息内容,不用依赖特殊的客户端库;任何一个语言只要会解析JSON,都能快速实现协议交互。
实际消息看起来大概是这样的一个请求:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "read_file", "arguments": { "path": "/home/user/notes/todo.md" } } }对应响应:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "- 完成MCP学习笔记\n- 测试stdio传输" } ] } }消息层虽然简单,但它规定了很多细节,比如不支持批量消息、错误码有统一标准、请求生命周期有明确阶段(初始化、操作、关闭)。这些细节正是协议能保持稳定互通的基础。
3. 动手实现一个最小可用的MCP Server
纸上谈兵再多,也不如亲手搭一个Server来得直接。我建议第一次尝试别搞Too复杂,先实现一个"文件读取与列表工具",它能让你快速看到完整的工具调用链路。下面是我在Ubuntu环境里用Python完成的全部流程,你可以原样复现。
3.1 准备工作:环境与依赖
我用的Python 3.11,SDK选择的是官方发布的Python SDK。安装很简单:
pip install mcp如果你是在虚拟环境里操作,记得先将pip升级一下,避免依赖解析出问题。另外准备一个AI应用作为Host端,我用的是一个配置了MCP能力的开源对话应用,你手头如果有其他支持MCP的AI应用也可以,原理都一样。
注意:SDK的版本迭代挺快的,不同版本API可能有一点点差异,但整体调用逻辑是稳定的。如果发现方法名对不上,第一个动作是查看当前版本对应文档,而不是猜。
3.2 编写第一个Server
我的目标是实现两个工具:一个叫做list_directory,用来列出某个目录下的文件;一个叫做read_text_file,用来读取文本文件内容。
完整代码如下:
import os from pathlib import Path from typing import List, Union from mcp.server.fastmcp import FastMCP mcp = FastMCP("fs-note-tool") ALLOWED_ROOTS = [ Path("/home/user/notes"), ] def _resolve_path(path_str: str) -> Path: p = Path(path_str).expanduser().resolve() for root in ALLOWED_ROOTS: if p == root or root in p.parents: return p raise ValueError(f"路径 {path_str} 不在允许访问的范围内") @mcp.tool() def list_directory(path_str: str = "/home/user/notes") -> List[str]: """列出指定目录下的所有文件和子目录名称""" path = _resolve_path(path_str) if not path.is_dir(): raise ValueError(f"{path_str} 不是有效目录") entries = [] for entry in sorted(path.iterdir()): suffix = "/" if entry.is_dir() else "" entries.append(entry.name + suffix) return entries @mcp.tool() def read_text_file(path_str: str) -> str: """读取指定文本文件的完整内容,仅支持UTF-8编码""" path = _resolve_path(path_str) if not path.is_file(): raise ValueError(f"{path_str} 不是有效文件") content = path.read_text(encoding="utf-8", errors="replace") if len(content) > 20000: content = content[:20000] + "\n...[内容截断,超出20000字符]" return content if __name__ == "__main__": mcp.run(transport="stdio")这里有几个细节我想单独解释一下。
_resolve_path函数是本Server的安全基石,我故意加了一个ALLOWED_ROOTS白名单。因为AI模型在生成工具参数时可能出现不可控的路径拼接,如果Server不加限制,模型可能调用工具去读取任意文件。这不是危言耸听,而是AI应用落地时最常见的安全隐患之一。路径校验必须在Server端做,不能指望模型端做。
read_text_file里做了长度截断,也是一个务实的选择。大语言模型的上下文窗口有限,如果工具返回一个巨大的文件内容,可能会把上下文窗口撑爆,导致后续对话质量骤降。控制返回内容体积,是每个Server作者都要有的自觉。
3.3 用Host端发起对话并触发工具调用
Server写完后,启动它很简单:
python fs_note_server.py正常的话进程会一直挂起,等待客户端连接。在Host应用里,我需要新增一个MCP Server配置,把启动命令指向上面的脚本。我用的是JSON配置文件,大概长这样:
{ "mcpServers": { "fs-note-tool": { "command": "python", "args": ["/path/to/fs_note_server.py"], "env": {} } } }配置完成后重启Host,然后在对话框里问:"帮我看一下/home/user/notes目录下有哪些文件?"正常情况下,Host会识别出需要调用list_directory工具,然后通过stdio通道把请求发给Server,拿到结果后整理成自然语言回复。
这一步走通之后,你会看到整个链路其实很短:Host解析意图 → Client建立连接 → 调用工具 → Server执行并返回 → Host组织回复。理解了这个链路,后面所有MCP相关的开发都是在丰富这条链路里的某个环节。
3.4 一个略进阶:支持HTTP+SSE模式的远程Server
stdio适合本地场景,但对于需要远程部署、多客户端共享的服务,就得换成HTTP模式。官方SDK里的FastMCP可以直接切transport参数:
python fs_note_server.py --transport http不过我建议在代码里显式指定:
if __name__ == "__main__": mcp.run(transport="http", host="0.0.0.0", port=8123)启动后,Server会提供一个HTTP端点,客户端通过HTTP建立持久连接并接收Server推送的事件流(SSE)。这个模式下的客户端配置,不是指定command,而是指定url:
{ "mcpServers": { "fs-note-tool-remote": { "url": "http://127.0.0.1:8123" } } }远程模式的优点很明显:Server可以部署在独立的机器上,多个Host应用共享一套工具服务,权限管控和负载均衡都更方便。我第一次把本地Server改造成HTTP模式后,发现开发调试其实更直观,因为HTTP消息可以直接抓包看到。
4. 工具选型与实现细节的关键考量
如果你打算把MCP用到真实场景,而不是只跑通Demo,有几个选型和设计决策值得提前想清楚。这一部分虽然不直接产代码,但它决定了你的MCP项目是能落地还是要返工。
4.1 用现成SDK还是自己实现协议
我强烈建议:除非你有非常特殊的需求,否则直接用官方SDK。
官方的Python SDK和TypeScript SDK都已经封装好了生命周期管理、会话初始化、消息路由等复杂逻辑。你自己实现一个简单Server可能只需要几十行代码,但要正确处理好初始化握手、错误重试、能力协商、分块传输这些细节,工程量和踩坑成本会直线上升。
我自己刚开始也试图"为了更深入地理解协议"而手写一个最小实现,后来发现光是处理各种传输层的边界情况就浪费了不少时间。最终我的做法是:协议理解靠读文档,工程落地用SDK。
4.2 一个Server暴露多个工具,还是一个工具一个Server
这个问题没有标准答案,但有一个推荐的判断原则:看工具之间的关联性和安全边界。
如果一组工具共享同一个数据源和访问权限,比如订单查询和订单修改,应该放在同一个Server里。这样Server内部可以直接复用数据库连接和权限校验逻辑,客户端也只需要管理一个连接。如果一组工具面向完全不同的数据域,比如一个是操作本地文件,一个是调用第三方支付接口,强烈建议拆成两个Server。原因首先是安全隔离,文件工具不应该有支付接口的凭据;其次是故障隔离,支付服务出问题不会影响文件工具的运行。
我在实践里通常按照"领域边界"来划分Server,而不是"功能数量"。一个Server对应一个业务域,这样不管工具多少,逻辑上都是内聚的。
4.3 工具Schema设计直接影响模型调用准确性
我踩过最深的坑,是工具描述和参数Schema写得太随意,导致模型要么不知道该在什么时候调用工具,要么传错参数。
MCP工具调用的准确率,一大半取决于你写的描述和参数定义。说到这里,有几个具体的经验可以分享:
- 工具描述要说明"调用场景"和"不调用场景"。比如"读取文本文件内容,适合在用户询问文件内容时使用,不用于文件列表功能",这样模型能更精准触发。
- 参数要有清晰的类型约束。能用枚举类型的地方不要用自由字符串,能用number不要用string。
- 参数描述要写清"值的格式"和"默认行为",尤其当参数可空时,要说清楚不传会怎样。
举个例子,我一开始写的list_directory参数是:
def list_directory(path_str: str = "/home/user/notes"):在工具定义里,这虽然是必要的默认值,但它带来了一个问题:模型可能会认为这个路径是唯一路径,用户输入任意目录时不会想到传参。改成了显式描述之后:
def list_directory(path_str: str = "/home/user/notes"): """列出指定目录下的所有文件和子目录名称。 参数path_str为绝对路径目录,默认情况下列出/home/user/notes。 当用户给出目录路径时,使用该路径;当用户没有给出时,使用默认路径。 """效果好了很多。亲自对比过才发现,工具描述里的"使用说明"远比"功能上一个词一个词地罗列功能"有用。
4.4 认证与权限:容易被低估的环节
MCP协议本身并不规定如何做认证,它把认证问题留给了传输层和应用层。本地stdio场景侥幸不需要认证,但一旦用到HTTP远程模式,没有认证就是裸奔。
我在一个模拟项目里实现过一个简单的Bearer Token认证方案:Server启动时读取环境变量MCP_AUTH_TOKEN,客户端连接时在初始化请求头里带上这个Token,Server在收到初始化请求时校验Token,不合法直接断开连接。代码配置如下:
import os TOKEN = os.environ.get("MCP_AUTH_TOKEN", "") # 在FastMCP的transport参数中支持header回调 async def auth_headers(): return {"Authorization": f"Bearer {TOKEN}"}需要注意的是,Token认证只解决了"谁在调用"的问题,没有解决"能调到哪一层数据"的问题。更细的权限控制(比如不同客户端只能调用Server的一部分工具)还是得在Server内部结合业务逻辑做,这个没有捷径。
5. 常见问题排查实录:我踩过的那些坑
这一部分是我真正觉得有价值的内容。官方文档往往只写怎么用,不会告诉你运行时会翻什么车。下面的问题,每一个我都实际遇到过,并且折腾出了解决方案。
5.1 工具一直没被模型调用
这是新手最容易疑惑的现象:Server配置好了,Host也连上了,但不管怎么问,模型就是不调用工具,只会"一本正经"地回答"我无法完成这个操作"。
排查路径通常是这样的:
- 确认Server是否真正被Host连接。可以在Server代码里加日志,打印客户端的初始化信息,看有没有连上来。
- 确认工具Schema是否够清晰。如果工具的语义跟用户的问法不容易关联,模型确实不会触发调用。可以尝试在对话中明确说"请使用工具XXX",看看是不是描述模糊的问题。
- 确认工具描述里是否带有触发词。在描述里加入上文的示例,比干巴巴写一个功能更容易命中。
- 确认你是不是在用一个不支持工具调用的模型。不同的模型对工具调用的支持差异非常大,有的模型甚至在上下文里根本看不到工具定义。
5.2 stdio模式启动后,Host挂起白屏
这个问题典型症状是:配置文件心跳正常,但Host界面转圈或直接卡死。多半是Server端的stdin/stdout被其他日志输出污染了。
有一个非常容易踩的坑是:在Server代码里写了print()用于调试。但以为这些输出会回到终端,实际上stdio模式下print会写到stdout,而stdout正是Client和Server通信的通道,杂乱的print会直接破坏协议消息流。
解决办法很简单:调试日志一律输出到stderr,或者写到文件里。官方文档里也反复强调过这个问题。养成用logging模块向stderr写日志的习惯,能给你省去大量排查时间。
5.3 工具调用成功,但返回内容太长导致客户端报错
我遇到过的一个经典场景是Server返回了一个超大的数据库查询结果,Host直接显示"响应超时"或"响应大小超出限制"。
排查后发现是上下文窗口撑爆导致的。这之后我养成了两个好习惯:
一是在Server端做结果裁剪或分页,不管什么工具,返回的内容体量要控制在合理范围内;
二是在协议配置里查看客户端支持的最大响应长度限制,在SDK初始化时可以调整相关配置,避免默认值太小。
5.4 远程HTTP模式下连接超时或频繁断开
HTTP+SSE模式依赖持久连接和心跳机制。默认的心跳间隔可能不适合某些网络环境,需要显式调整。
在我实践的模拟项目中,调大心跳间隔后,连接稳定性有了明显改善。但这里也要注意,心跳间隔不能无限大,否则客户端无法及时感知Server故障。一般建议设置在30秒到60秒之间。
另外,远程模式下尽量不用明文HTTP,至少加一层TLS加密,否则传输内容可以被中间人读取,包括你的请求参数和返回结果。
5.5 排查最快的调试组合
如果你确定要深入调试MCP,我给你推荐一个很顺手的组合:先写一个自动化脚本完成初始化握手,然后手动发一条tools/list消息,确认工具列表,再调用具体工具验证返回值。
这条调试路径的好处是绕开了Host应用的各种缓存和UI逻辑,直接把问题定位在协议层。我在开发新Server时都会先跑一遍这个流程,跑通了再去接Host,效率高非常多。
6. 从学习笔记到实战:基于MCP扩展自己的AI应用
我这篇笔记写到这里,其实已经从协议原理讲到了单点实践。但MCP真正让我觉得有价值的,是它作为"扩展层"能把一个AI应用演进成什么形态。
6.1 组合多个Server:从单点工具到复合能力
你可以在同一个Host里同时接入多个MCP Server,每个Server负责一块能力。比如我有一个模拟的"个人工作台"项目,同时接了三类Server:
- 一个文件Server,负责读写工作目录下的文档。
- 一个数据Server,负责查询SQLite里的任务表。
- 一个集成Server,负责调用内部待办API。
用户问"昨天有哪些任务未完成",Host先调用数据Server查询任务表;用户接着问"把未完成的任务整理成一个报告放到工作区",Host同时调用数据Server获取数据,再调用文件Server把报告写入文件。整个过程用户看到的只是对话,背后是多个Server的协同。
这种"能力即插即用"的感觉,很像我第一次用微前端改造大型项目时的体会:每个模块可以独立演进,集成层的成本降到了最低。
6.2 MCP Server与Agent的关系
不少人会问一个问题:有了MCP,是不是就可以不用写Agent逻辑了?
我的答案是:MCP解决的是"工具接入"的问题,Agent解决的是"决策流程"的问题,两者不是替代关系,而是分层协作关系。
你仍然需要一个Agent编排层来决定何时调用工具、如何解读工具返回结果、如何在多轮对话里维护状态。但MCP让Agent的"行动边界"变宽了很多,因为接入一个新能力的成本大大降低了。以前让Agent多一个技能可能需要专门写一套适配代码,现在只需要新启动一个MCP Server。
6.3 生态现状与选型建议
截至我编写这篇笔记时,MCP的生态已经相当活跃。各大主流AI应用几乎都原生支持或规划支持MCP,社区里有大量现成的Server实现,覆盖了文件操作、数据库访问、项目管理、通信集成等常用场景。
但我有一个很诚恳的建议:社区Server可以直接用,但生产环境用之前一定要做代码审计。你是在把一把"能让你AI助手操作外部世界的钥匙"交给那个Server,如果Server里有恶意代码或后门,后果比传统API密钥泄露更直接。我在研究时发现,有些社区Server就存在路径穿越或命令注入类的安全风险。宁可花一个下午把代码看一遍,也不要图省事直接信任第三方Server。
6.4 适合自己的MCP学习路线
如果你按照这篇笔记看到这里,已经比很多"只知道MCP这个名字"的人扎实多了。再往后走,我建议的顺序是:
- 用SDK自己写一个简单的文件工具Server,跑通stdio模式。
- 写一个调用第三方HTTP API的Server,熟悉异步请求和错误处理。
- 改造一个Server为HTTP+SSE模式,再加一层Token认证。
- 尝试在同一个Host里接两个不同领域的Server,组合调用它们。
- 通读一遍协议规范中关于生命周期和消息类型的那几个章节,补齐概念死角。
完成这五步后,MCP就不再是"一个听说过的新词",而会成为你工具箱里一个随时可用的标准接口。
最后分享一个我自己的体会:技术协议这东西,刚出来时看起来都很热闹,真正能留下来的通常有一个共性——它让开发者的协作成本变低、集成成本变低。MCP当前正处在这样一个"早期标准化"的阶段,越早吃透,后面做AI应用时的组件化能力就越强。我在踩完那些坑之后,最深的感受是:协议本身并不复杂,复杂的是它连通的真实世界,而处理这些复杂性的经验,只能靠动手和踩坑来积攒。这篇笔记就是一个记录,希望能让你少走弯路。