
这篇笔记是我在连续研究了大半个月MCPModel 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 ServerAI应用侧一个标准客户端搞定代码量少了一大截。这个体验让我意识到MCP对AI应用的可维护性提升是结构性的而不仅仅是节省了几行代码。这篇笔记适合谁如果你正在用主流AI编程助手、正在开发自己的AI Agent、或者只是好奇AI怎么才能真正操作外部工具都值得往下看。基础要求不高懂一点Python或者Node.js能运行终端命令就行。2. 把MCP拆开看四个角色和三个核心概念MCP的架构看起来有点抽象但如果理解了它的角色划分后面写代码就会很顺畅。整个体系的角色和概念不算多也没有复杂到需要专门画图才能理解的程度用文字完全可以讲透。2.1 四个角色Host、Client、Server、TransportMCP体系里定义了清晰的参与者角色它们在一次完整的工具调用中各司其职。搞清楚这四个角色是理解MCP的第一步。MCP Host宿主应用也就是用户正在使用的那款AI应用比如某个AI编程助手、某个智能IDE、某个能对话的桌面应用。宿主应用是逻辑入口负责理解用户的意图并决定调用什么工具。宿主自身不做工具的具体实现它只消费工具。MCP Client客户端在宿主进程内运行的一个组件模块负责与MCP Server建立连接、维护会话、发送请求、接收结果。通常一个宿主可以管理多个客户端一个客户端对应一个MCP Server连接。MCP Server服务器提供工具、资源或提示词的服务端程序。它最核心的职责是把外部能力封装成标准化的接口。这个Server可以是你自己写的小脚本也可以是第三方提供的成熟服务。Transport传输层Client和Server之间的通信通道。目前最常见的是stdio标准输入输出适合本地子进程和HTTPSSE适合远程服务两种方式。传输层是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.11SDK选择的是官方发布的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(encodingutf-8, errorsreplace) if len(content) 20000: content content[:20000] \n...[内容截断超出20000字符] return content if __name__ __main__: mcp.run(transportstdio)这里有几个细节我想单独解释一下。_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 一个略进阶支持HTTPSSE模式的远程Serverstdio适合本地场景但对于需要远程部署、多客户端共享的服务就得换成HTTP模式。官方SDK里的FastMCP可以直接切transport参数python fs_note_server.py --transport http不过我建议在代码里显式指定if __name__ __main__: mcp.run(transporthttp, host0.0.0.0, port8123)启动后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客户端连接时在初始化请求头里带上这个TokenServer在收到初始化请求时校验Token不合法直接断开连接。代码配置如下import os TOKEN os.environ.get(MCP_AUTH_TOKEN, ) # 在FastMCP的transport参数中支持header回调 async def auth_headers(): return {Authorization: fBearer {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模式下连接超时或频繁断开HTTPSSE模式依赖持久连接和心跳机制。默认的心跳间隔可能不适合某些网络环境需要显式调整。在我实践的模拟项目中调大心跳间隔后连接稳定性有了明显改善。但这里也要注意心跳间隔不能无限大否则客户端无法及时感知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为HTTPSSE模式再加一层Token认证。尝试在同一个Host里接两个不同领域的Server组合调用它们。通读一遍协议规范中关于生命周期和消息类型的那几个章节补齐概念死角。完成这五步后MCP就不再是一个听说过的新词而会成为你工具箱里一个随时可用的标准接口。最后分享一个我自己的体会技术协议这东西刚出来时看起来都很热闹真正能留下来的通常有一个共性——它让开发者的协作成本变低、集成成本变低。MCP当前正处在这样一个早期标准化的阶段越早吃透后面做AI应用时的组件化能力就越强。我在踩完那些坑之后最深的感受是协议本身并不复杂复杂的是它连通的真实世界而处理这些复杂性的经验只能靠动手和踩坑来积攒。这篇笔记就是一个记录希望能让你少走弯路。