首页 / AI教程 / 正文
AI教程

MCP教程:从零掌握模型上下文协议,让AI轻松连接外部工具与数据

chuanbook chuanbook
发布于 2026 年 10 月 06 日
阅读 约17分钟
浏览 3
评论 0

1.1 什么是MCP:协议定位、背景与核心价值

我第一次听到MCP这个词,脑子里冒出来的是“又一个新协议?”那会儿我正在折腾一个AI助手项目,想让模型能查数据库、读本地文件,还要调用几个第三方API。每个集成都得写一套胶水代码,烦得很。后来朋友丢给我一个链接,说“你试试这个,MCP”。我花了一个下午看完文档,才意识到它想做的事情有多朴素:给大模型和外部世界之间搭一座标准桥。

MCP全称是Model Context Protocol,翻译过来叫“模型上下文协议”。它不教模型怎么变聪明,也不管你用的是GPT还是Claude。它只关心一件事——当模型需要用到外部数据或工具时,怎么用统一的方式去要、去拿、去执行。你可以把它想象成USB-C接口。以前每个设备都有自己的充电口,现在一根线走天下。MCP就是AI应用领域的那个“USB-C”。

它的核心价值在我看来有三层。第一层是省事,开发者不用为每个模型、每个工具重复写对接代码。第二层是解耦,工具提供方和AI应用方可以各自独立演化。第三层是生态,当大家都遵守同一个协议,工具就能像积木一样插来插去。我后来把项目里的三个数据源都改成了MCP Server,代码量少了将近一半,维护起来也清爽多了。

1.2 MCP解决什么问题:大模型连接外部数据与工具的标准化方案

大模型本身是个“闭卷考生”。它脑子里装了大量训练时见过的知识,可一旦你问它“我们公司上个月的销售数据”,它就抓瞎了。传统做法有两种:要么把数据塞进提示词里,要么给模型挂上函数调用能力。第一种受限于上下文窗口,塞不了太多东西。第二种每个平台都有自己的函数调用格式,换一个模型就得重写一遍。

MCP瞄准的就是这个痛点。它定义了一套标准接口,让模型可以通过客户端去问服务器:“你有什么资源?你能执行什么工具?”服务器用统一的JSON-RPC格式回答。这样一来,数据源只要实现一次MCP Server,所有支持MCP的AI应用都能直接用。我试过把一个MySQL查询封装成MCP工具,之后在Claude Desktop、在自研Agent、在某个开源IDE插件里都能调用,确实省了不少重复劳动。

还有一个容易被忽略的问题:安全边界。以前把数据库密码直接写在AI应用里,权限控制很粗糙。MCP允许你把敏感操作留在Server端,Client只负责转发请求。Server可以自己决定要不要鉴权、要不要脱敏、要不要限流。这种分层设计让“让AI碰生产数据”这件事变得稍微让人放心一点。

1.3 MCP核心架构:Host、Client、Server三者关系

MCP的架构图第一眼看上去有点绕,其实角色就三个:Host、Client、Server。我刚开始总把Client和Host搞混。后来用了一个类比才记住——Host是“公司”,Client是“前台”,Server是“外部供应商”。公司(Host)想采购东西,前台(Client)负责打电话、传话、收快递。供应商(Server)只管按订单发货。

Host通常是你正在用的那个AI应用,比如Claude Desktop、Cursor、或者你自己写的聊天界面。它负责管理多个Client,每个Client对应一个Server连接。Client是协议的实际执行者,它和Server建立一对一的会话,处理初始化、能力协商、消息收发。Server就是能力提供方,它可以暴露资源(Resources)、工具(Tools)、提示(Prompts)。

三者之间是松耦合的。一个Host可以同时连好几个Server,一个Server也可以被多个Host连接。我做过一个实验:让同一个文件系统Server同时服务两个不同的Client,一个用来读文档,一个用来写日志。两边互不干扰,因为每个Client-Server对都有自己的会话状态。这种设计让扩展变得很自然——你想加新能力,就再加一个Server,不用动Host的核心逻辑。

1.4 MCP与Function Calling、插件系统、Agent工具调用的区别

很多人第一次接触MCP会问:这不就是Function Calling吗?我一开始也这么想。Function Calling是模型层面的能力,你告诉模型“有个函数叫get_weather,参数是城市名”,模型决定调用它,然后你的代码去执行。MCP不是模型能力,它是应用层的通信协议。Function Calling可以走MCP,也可以不走。MCP管的是“怎么把工具描述传给模型”、“怎么把调用请求发给执行方”、“怎么把结果拿回来”。

插件系统又是另一回事。ChatGPT插件、浏览器插件,它们通常绑定在特定平台上,有各自的清单文件、鉴权方式、部署要求。MCP不绑定平台,你写一个Server,理论上任何支持MCP的Host都能用。插件更像“应用商店里的App”,MCP更像“App之间的通用语言”。

Agent工具调用就更宽泛了。Agent可以自己规划、自己选工具、自己反思。MCP不负责规划,它只负责“当Agent决定要用某个工具时,怎么把这个决定变成一次可靠的远程调用”。我现在的理解是:Agent是大脑,Function Calling是决策输出,MCP是神经和血管。三者配合,才能让Agent真正动手干活。

1.5 MCP教程学习路径:前置知识、目标读者与实战路线

如果你打算学MCP,先看看自己会不会写一点JavaScript或Python。不需要精通,能看懂异步函数、会装npm包或pip包就行。我建议的目标读者是:想给AI应用加外部能力的开发者、想把自己的API或数据源开放给AI的工具作者、以及好奇Agent底层怎么连工具的技术爱好者。完全零编程基础的话,先补一补基础语法会更顺。

学习路线我踩过一些坑,后来整理成一条相对平滑的路径。先理解协议的基本概念和通信模型,别急着写代码。接着用官方SDK跑一个最小的Server和Client,感受一下消息怎么流动。之后挑一个真实场景,比如把本地文件夹暴露成资源,或者把某个HTTP API包装成工具。再往后可以研究采样、根、进度通知这些进阶能力。最后才是部署、安全、性能优化。

整个路线里,动手写比读文档重要。我最初看了三遍文档还是懵,直到自己用TypeScript写了一个只能返回“Hello MCP”的Server,才突然明白初始化握手是怎么回事。所以别怕代码丑,先跑通再说。

1.6 开发环境准备:Node.js、Python、SDK与调试工具

环境准备这块,我推荐Node.js 18以上或者Python 3.10以上。Node.js的生态对MCP支持比较早,官方TypeScript SDK更新也快。Python SDK用起来更简洁,适合快速验证想法。你可以两个都装,切换着用。我自己的主力是Node.js,因为前端项目本来就在用,顺手。

SDK方面,官方提供了@modelcontextprotocol/sdk(TypeScript)和mcp(Python)。安装方式跟普通包一样:npm install @modelcontextprotocol/sdk或者pip install mcp。装完之后建议先跑一遍官方仓库里的examples,别急着改代码。调试工具里,MCP Inspector是个好东西,它像个浏览器开发者工具,能让你看到Client和Server之间每一条JSON-RPC消息。我第一次用它抓到参数类型不匹配的问题,比看日志快多了。

另外准备一个顺手的编辑器,VS Code就行。装个REST Client或者Thunder Client插件,方便你手动发HTTP请求测试Streamable HTTP传输。日志方面,Server端尽量用结构化日志,把请求ID、方法名、耗时打出来。我习惯在开发阶段把日志级别调到debug,上线前再改成info。这样排查连接失败或者工具调用异常时,不会两眼一抹黑。

2.1 MCP通信模型:客户端与服务器如何建立连接

我第一次画MCP通信图的时候,把Client和Server之间的线画成了双向箭头,旁边写了个“长连接”。后来实际写代码才发现,建立连接的过程比我想的更有仪式感。Client主动发起连接,Server接受,双方要交换一次初始化握手。这个握手不是可有可无的寒暄,它决定了后面能用哪些能力。我会在Client里调用connect方法,传入Server的传输对象。Server那边则监听连接请求,收到initialize消息后返回自己的协议版本和支持的能力列表。

从Host的角度看,通信模型有点像公司内部的总机系统。Host是CEO,它不直接跟外部供应商打电话。它让Client去建立专线。每个Client只负责一个Server,这样会话状态不会串。我试过一个Host同时管三个Client,分别连数据库Server、文件Server和天气Server。它们各自独立初始化,互不干扰。Client和Server之间的消息都带会话ID,或者靠传输层自己维持上下文。我刚开始忘了处理会话状态,结果两个请求交叉返回,数据全乱了。

还有一点,MCP的连接是有状态的。不是发一条HTTP请求就完事。连接建立后,双方会记住对方的能力、协议版本、甚至一些上下文。这种设计让工具调用和资源读取更高效,不用每次重新协商。代价是你要管理连接的生命周期。我后来养成了一个习惯:每次调试连接问题,先看初始化握手有没有成功。握手失败,后面全白搭。

2.2 传输方式详解:stdio、SSE、Streamable HTTP等

传输方式这块我踩的坑最多。最开始我只知道stdio。本地跑Server最简单,我一开始只用它。stdio就是标准输入输出,Client把Server当成子进程启动,通过stdin写请求,从stdout读响应。这种方式延迟极低,配置也简单,特别适合本地工具。我写过一个文件搜索Server,用stdio传输,启动脚本里直接写node server.js,Client就能连上。缺点也很明显:Server必须和Client在同一台机器上,没法远程部署。

后来我想把Server放到云服务器上,就试了SSE。SSE全称Server-Sent Events,它基于HTTP长连接。Server可以向Client推送消息,Client通过POST发请求。听起来不错,实际用起来有点别扭。SSE是单向的,Server到Client的推送很顺畅,Client到Server的请求要另开通道。我配置的时候经常遇到连接超时。中间有代理或者防火墙,它们经常切断空闲连接。SSE适合那种Server主动通知的场景,比如进度更新,作为主传输方式,它的双向通信能力偏弱。

Streamable HTTP是更新的一种传输方式,我最近才开始用。它允许在一个HTTP连接上做双向流,既支持请求响应,也支持服务器推送。配置起来比SSE灵活,支持长轮询和流式响应。我用它部署了一个远程天气Server,Client通过HTTPS连接,效果挺稳。三种传输方式没有绝对好坏,看场景。本地开发我首选stdio,远程服务我倾向Streamable HTTP。SSE在特定推送场景下还有用,新项目我很少选了。

2.3 消息格式与JSON-RPC:请求、响应、通知与错误

MCP的消息格式完全基于JSON-RPC 2.0。我一开始觉得JSON-RPC太简单了,简单到有点简陋。后来发现它的约束刚刚好。请求消息里有jsonrpc、id、method、params四个字段。id用来匹配响应。响应消息里有jsonrpc、id、result或error。通知消息没有id,因为它不需要回复。错误对象里有code、message和可选的data。我调试的时候经常盯着Inspector里的消息流,看id对不对得上。

有一次我写的Server在处理工具调用时抛了异常,但忘记返回标准错误对象。Client收到一个奇怪的响应,直接卡死了。后来我改用JSON-RPC的错误格式,返回{"code": -32603, "message": "Internal error"},Client就能正常捕获并显示。通知消息我用得不多,进度通知是个例外。Server可以在执行长任务时发通知,告诉Client“我完成了30%”。这种单向消息不需要id,也不会阻塞后续请求。错误码里我最常遇到的是-32601,表示方法不存在。通常是因为Client请求了一个Server没实现的工具。

JSON-RPC的另一个好处是传输无关。同样的消息格式,可以跑在stdio上,也可以跑在HTTP上。我切换传输方式的时候,业务代码几乎不用改。这种分层设计让我省了很多重构时间。JSON-RPC也有坑,比如params可以是数组或对象,我一开始混着用,结果Server解析出错。后来统一用对象,问题就少了。

2.4 MCP能力原语:资源Resources、工具Tools、提示Prompts

MCP定义了三种能力原语:资源、工具、提示。我花了一段时间才分清它们。资源是只读的数据,比如文件内容、数据库记录、API返回的JSON。Client可以通过URI读取资源,比如file:///project/readme.md。工具是可执行的函数,模型可以调用它来做事,比如发送邮件、查询天气、创建订单。提示是预定义的模板,通常由用户选择,比如“帮我总结这段代码”。三个原语面向不同的交互模式。

从我的使用经验看,资源适合“给模型看”的场景。比如我把公司文档目录暴露成资源,模型在回答问题时可以主动读取相关文件。工具适合“让模型做”的场景。我封装过一个send_slack_message工具,模型决定要发消息时,Client调用它,Server执行。提示适合“引导用户”的场景。我做过一个代码审查提示,用户选中一段代码,点击提示,Client把代码和模板一起发给模型。三者的区别可以用一句话概括:资源是名词,工具是动词,提示是形容词。

实现的时候,Server要在初始化时声明自己支持哪些原语。Client发现后,才能调用。我犯过一个错误:Server实现了工具,但初始化时没声明,Client死活发现不了。后来我在Server的capabilities里加上tools: {},问题解决。资源还有订阅机制,Client可以订阅某个资源的变更,Server在资源变化时发通知。这个功能我还没在项目里用,想想挺适合实时数据看板。

2.5 进阶能力:采样Sampling、根Roots、进度与取消

采样是我觉得最反直觉的一个能力。通常我们认为是Client调用Server的工具,采样允许Server反过来请求Client调用LLM。比如Server在处理一个复杂任务时,需要模型帮忙生成一段文本,它就可以发起采样请求。Client收到后,调用自己背后的LLM,再把结果返回给Server。这种反向依赖让Server变得更智能。我试过写一个翻译Server,它采样Client的LLM来润色翻译结果。效果不错,但要注意别形成死循环。

根(Roots)是另一种约束机制。Client可以告诉Server:“你只能访问这个目录下的文件。”Server收到根列表后,应该限制自己的资源访问范围。我用根来限制文件Server,防止它读到项目外的敏感文件。配置的时候,Client在初始化时传入根URI,Server检查每个资源请求是否在根范围内。这个能力对安全很重要,尤其是在多租户环境里。

进度和取消是提升用户体验的细节。长任务执行时,Server可以发notifications/progress通知,Client收到后更新进度条。取消则是Client发notifications/cancelled,告诉Server“别做了”。我实现过一个批量图片处理Server,没有进度通知的时候,用户以为程序卡死了。加上进度后,体验好很多。取消功能要小心处理,Server收到取消后要清理资源,而不是粗暴地杀掉进程。我一般会在Server里维护一个任务ID到取消函数的映射,收到取消就调用对应的清理逻辑。

2.6 生命周期管理:初始化、能力协商、关闭与异常处理

MCP连接的生命周期从初始化开始。Client发送initialize请求,带上协议版本、客户端信息、能力列表。Server返回initialize响应,带上自己的协议版本、服务器信息、能力列表。这个交换过程就是能力协商。我写Client的时候,会检查Server返回的协议版本是否兼容。不兼容的话,要么降级,要么断开。能力协商决定了后续能用哪些功能。比如Server没声明tools能力,Client就不该去调用工具。

初始化之后,Client发送notifications/initialized通知,表示握手完成。这时候连接正式可用。关闭连接时,Client可以发shutdown请求,Server清理资源后返回响应。Client收到后关闭传输层。异常处理是生命周期里最麻烦的部分。网络断了、Server崩溃、请求超时,这些都要考虑。我一般在Client里设置超时定时器,超时后发取消通知,再重连。Server端则要捕获未处理异常,返回JSON-RPC错误,而不是让进程挂掉。

我遇到过一种情况:Server在处理请求时陷入死循环,Client一直等不到响应。后来我加了心跳机制,Client定期发ping,Server回pong。如果连续几次没收到pong,Client就认为连接失效,主动关闭并重连。生命周期管理听起来枯燥,它决定了你的MCP集成稳不稳定。我现在的习惯是,每次上线前都手动模拟一遍断网、超时、Server重启,看看Client能不能优雅恢复。真出问题的时候,至少不会手忙脚乱。

import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod";

const server = new Server( { name: "echo-server", version: "1.0.0" }, { capabilities: { tools: {} } } );

server.tool("echo", { message: z.string() }, async ({ message }) => { return { content: [{ type: "text", text: 你说的是:${message} }] }; });

const transport = new StdioServerTransport(); await server.connect(transport);

import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({ command: "node", args: ["build/server.js"] }); const client = new Client({ name: "my-client", version: "1.0.0" }, { capabilities: {} }); await client.connect(transport);

server.tool("queryOrdersByUser", { userId: z.string().describe("用户ID"), startDate: z.string().optional(), endDate: z.string().optional() }, async ({ userId, startDate, endDate }) => { const sql = `SELECT id, amount, status, created_at

           FROM orders WHERE user_id = $1 
           AND created_at BETWEEN $2 AND $3 LIMIT 100`;

const result = await pool.query(sql, [userId, startDate, endDate]); return { content: [{ type: "text", text: JSON.stringify(result.rows) }] }; });

赞0
踩0
☆收藏0
版权声明
文章版权声明:除非注明,否则均为ZBLOG原创文章,转载或复制请以超链接形式并注明出处。
分享到
chuanbook

链接已复制到剪贴板