去年我在折腾Agent项目的时候,最头疼的事不是模型切换,而是让AI真正“摸”到数据和工具。文件、数据库、浏览器、API,每接一个都要单独写一套适配逻辑,写完还只能在自家程序里用。直到我把MCP(Model Context Protocol,模型上下文协议)这一套标准拉进项目,才感觉AI应用和外部系统的边界终于清晰了。这篇文章不打算复述官方文档,我会从“我为什么需要它、我怎么理解它、怎么动手开发一个自己的MCP工具”这条实际踩出来的路线,把协议里的硬骨头拆开揉碎。
这篇内容适合谁?想给Claude、Cursor、Codex这类AI工具扩展能力的人,在做企业知识库或内部系统AI化的开发,以及在低代码平台里做工具集成的同学。你不需要是协议专家,只要会一点Python或TypeScript,就能在半小时内跑通第一个MCP服务。
1. MCP协议到底解决了什么问题
1.1 一句话理解MCP:给大模型接上“USB-C”
你回想一下USB-C生态,不管是显示器、硬盘、手机、耳机,只要接口统一,一根线就能通吃。MCP做的事情差不多,它把AI模型和外部工具、数据源之间的对接方式标准化了。以前你得为每个工具写“私人定制”的连接代码,现在只要实现MCP协议,任何一个支持MCP的AI客户端都能直接调用你的工具。
MCP的官方定义是“为AI应用提供标准化上下文获取方式”。具体来说,它定义了一套客户端与服务端的通信规则:AI应用是客户端,工具系统是服务端。服务端暴露三类东西,工具(Tools,可执行的动作)、资源(Resources,可读取的数据)、提示(Prompts,可复用的指令模板),客户端通过统一协议去发现和调用它们。
以我自己的经验,最初引入MCP并不是为了“赶时髦”,而是被多工具编排逼的。当时项目里要同时读文件、查数据库、调内部API,如果用老的Function Calling方式,每加一个工具就要改一遍模型侧的Schema定义,工具一多就乱。换成MCP之后,工具发现、参数校验、结果回传都是协议自动完成的,我只需要维护服务端那一份工具清单,模型侧不用再动代码。
1.2 它为什么在2024年底之后突然火起来
MCP的第一个公开版本是2024年11月底发布的,真正爆发是在2025年上半年。原因是Agent类应用开始规模化落地,大家发现“让模型说话”已经不难,难的是“让模型干活”。模型要干活,就必须和真实世界的数据、系统、服务交互,而每家的交互方式都不一样,这成了规模化瓶颈。
传统的Function Calling本质上还是一种“写死在代码里”的方案。每个模型厂商有一套自己的函数定义规范,你换了模型,工具适配代码就要重写。而且Function Calling只解决“模型怎么把参数填对”,不解决“工具从哪来、怎么发现、怎么鉴权、怎么返回复杂结果”。MCP把这些都纳入了一个抽象层,相当于把“工具调用”这件事从模型厂商内部API里剥离开来。
另一个推手是生态。Anthropic开源了官方SDK和一批参考服务器,随后OpenAI在2025年3月宣布在自家产品里支持MCP,紧接着Figma、Notion、Blender、Unreal Engine、JetBrains等纷纷宣布或放出官方MCP服务。开发者发现“一次开发,到处连接”不是口号。我在这段时间见过很多有意思的落地:比如用MCP把SQLite数据库接进Cursor写查询助手,把浏览器自动化Playwright封装成MCP供多个Agent共用,甚至有人给游戏引擎Unreal做MCP插件,让AI在编辑器里生成场景对象。
1.3 MCP能做什么、不能做什么
MCP能做的事,一句话:凡是“数据读取 + 动作执行”都能标准化。具体例子:
- 文件类:读取本地文件、项目代码、日志尾部,属于最基础的Resources应用。
- 数据库类:把PostgreSQL、MySQL、Oracle包装成只读查询工具,让AI辅助写SQL、做数据分析。
- 浏览器类:通过Playwright/Puppeteer封装MCP,让AI操作浏览器、抓取页面、执行前端测试。
- 开发工具类:GitHub、GitLab、JIRA、Jenkins的MCP服务,AI可以提PR、查Issue、触发流水线。
- 创意工具类:Figma设计稿读取、Blender建模操作、Unreal场景控制,这些都在往MCP上靠。
但MCP不是万能的。它不是Agent框架,不负责规划决策,也不带“记忆”。它只是把模型和工具之间的“通信协议”规范化,至于模型怎么规划、什么时候调用哪个工具,那是Agent编排层的事。同时,MCP也不是安全沙箱,它不会自动拦截危险操作。一个MCP服务端如果暴露了删库工具,那AI照样能调,权限控制必须你自己做。理解这一点,后面开发工具时心态就对了。
2. MCP协议的架构与核心运转逻辑
2.1 客户端、服务端与服务端宿主
MCP协议里涉及三个角色,容易搞混,我梳理一下:
- Host(宿主):用户直接面对的应用,比如Claude Desktop、Cursor、IDEA插件。它负责发起会话、展示AI回复,并决定要不要给Agent配MCP工具。
- Client(客户端):在宿主内部,每个MCP服务器对应一个客户端实例。它负责和目标MCP服务端建立连接、发请求、收响应。
- Server(服务端):你写的那个工具进程,实现协议、暴露工具/资源/提示,监听客户端的调用请求。
一个Host可以同时连多个Server,比如同一个Claude Desktop里既能读你本机文件,又能查公司数据库,还能调Figma服务。一个Server也可以同时被多个Host连接,只要传输层支持多路访问(比如HTTP传输)。
我建议你把它们想成“浏览器 + 插件”的关系:Host是浏览器,MCP Server是网页里的服务提供方,Client是浏览器内部管理网络请求的那个模块。你在浏览器里看网页,网页里的数据和服务通过HTTP协议暴露,浏览器负责渲染和交互。MCP就是把这套逻辑搬到了AI场景,只不过“渲染”变成了“模型理解”,“交互”变成了“工具调用”。
2.2 三种原生能力:工具、资源、提示
MCP服务端可以暴露三类原语,很多人只用了工具,其实资源才是它区别于传统Function Calling的关键。
**工具(Tools)**是最直接的能力,适合“做了某个动作”的场景。典型例子:发送邮件、创建工单、执行SQL、启动构建任务。工具定义要包含名字、描述、JSON Schema格式的参数,模型会根据描述决定何时调用。
**资源(Resources)**是MCP里很特别的设计,它提供“数据读取”能力,并且通过URI来定位。比如file:///etc/config.ini、mysql://user@host/db/table、git://commit/xxx,客户端可以把资源当作上下文喂给模型。资源的最大价值是让模型能够主动发现和读取数据,而不是只能靠用户手动粘贴。我做过一个资源是读取每日销售汇总,模型接到分析任务时自己就去读资源,然后给出结论,体验很顺畅。
**提示(Prompts)**是可复用的指令模板,适合标准化工作流。比如一个MCP服务器里内置“SQL调优助手”提示,用户只要填表名,模型就会按你预设的步骤走:先读表结构资源,再分析索引,最后给优化建议。提示本质上把“怎么跟模型说话”变成了可以分发、复用的资产。
我用一个类比:工具是工具箱里的电钻,资源是旁边的图纸和材料清单,提示是贴在墙上的标准作业指导书。三者配合,才能让一个“AI员工”真正独立干活。
2.3 基于JSON-RPC 2.0的消息流
MCP的通信协议基于JSON-RPC 2.0,这是一种轻量的远程调用协议,请求和响应都是JSON格式,字段包括jsonrpc、id、method、params。你不需要从头学JSON-RPC,只要会读报文就行。
MCP客户端和服务端的连接过程可以简化为四次关键交互:
- 客户端发送
initialize请求,说明自己支持的协议版本和客户端信息。 - 服务端回传支持的能力列表(capabilities),比如支持工具还是资源。
- 客户端发
notifications/initialized通知,表示初始化完成,开始正常通信。 - 后续客户端会发
tools/list、resources/list来发现能力,发tools/call、resources/read来实际调用。
我贴一段典型的握手持报文:
--> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"my-client","version":"0.1.0"}}} <-- {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{},"resources":{}},"serverInfo":{"name":"mysql-helper","version":"0.0.1"}}} --> {"jsonrpc":"2.0","method":"notifications/initialized"} --> {"jsonrpc":"2.0","id":2,"method":"tools/list"} <-- {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"query_mysql","description":"只读查询MySQL数据库","inputSchema":{"type":"object","properties":{"sql":{"type":"string"},"limit":{"type":"integer"}}}}]}} --> {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"query_mysql","arguments":{"sql":"select * from orders limit 5"}}} <-- {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"[{\"order_id\":1,\"amount\":100}]"}],"isError":false}}如果服务端处理出错,需要返回:
<-- {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"仅允许SELECT语句"}],"isError":true}}协议里还有一个细节:错误不通过JSON-RPC 2.0的error字段返回,而是放在result.isError=true+ 普通内容里。这样模型能直接看到错误文本,理解率更高。作为工具开发者,你的工具内部抛异常时,一定要捕获并把人类可读的错误信息返回给模型,而不是让连接层断掉。
2.4 协议版本与SDK选择
MCP协议还在快速演进,目前主流客户端默认支持2025-03-26版本,这个版本引入了Streamable HTTP传输,取代了早期不稳定的HTTP+SSE组合。另外2025-06-18版本进一步统一了“原语”概念,把工具、资源、提示统一到一个可发现模型下,新版本对开发者更友好。在实际开发中,我建议服务端最好兼容到2025-03-26,并用SDK自动协商版本,避免客户端连不上。
官方SDK有Python和TypeScript两套,社区还有Go、Rust、Java等实现。我个人的选择是:快速原型用Python SDK里的FastMCP封装,长期维护用TypeScript SDK,因为前端生态里的大模型客户端(比如LangChain、AI SDK)对TS更友好。
Python官方SDK可以这样装:
pip install "mcp[cli]"TypeScript SDK:
npm install @modelcontextprotocol/sdk辅助开发的小工具强烈推荐官方MCP Inspector,一条命令就能在浏览器里调试服务端:
npx @modelcontextprotocol/inspector python /path/to/server.py它能查看工具列表、手动传参调用、观察原始报文,我在写工具时几乎离不开它。
3. 从零开发一个MCP服务器:以MySQL查询助手为例
3.1 场景与规划
我接到的需求有很多都是“让AI帮我查数据库”,所以就拿这个场景当例子。最终目标是做一个MCP工具服务器,给Claude Desktop或Cursor用,让模型能直接执行只读SQL查询、读取表结构、生成分析报告。
动手前先做安全边界规划,这比写代码更重要:
- 只允许
SELECT语句,拒绝其他任何SQL。 - 数据库侧用专用只读账号,只授予目标库的
SELECT权限,不让他碰其他库。 - 强制附加
LIMIT,防止AI一句SELECT *把几百GB的表全查出来。 - 查询超时控制在10秒内,避免慢查询拖垮数据库。
这些约束在代码层面、数据库权限层面、配置层面各设一道,习惯叫“三明治防护”。
3.2 环境准备与项目结构
我习惯用uv管理Python项目,比pip干净很多:
uv init mysql-helper && cd mysql-helper uv add "mcp[cli]" pymysql项目结构很简单:
mysql-helper/ server.py # MCP服务端主程序 .env # 数据库连接信息(不提交到git) README.md3.3 用FastMCP实现核心查询工具
Python官方SDK自带了FastMCP封装,尽量像写普通函数一样写工具。先来一个最简版本:
from mcp.server.fastmcp import FastMCP import time mcp = FastMCP("时间助手") @mcp.tool() def get_current_time() -> str: """获取当前服务器时间,返回可读字符串""" return time.strftime("%Y-%m-%d %H:%M:%S") if __name__ == "__main__": mcp.run()这个例子说明了工具开发的最小范式:装饰器注册、函数文档字符串即工具描述、返回值即模型看到的内容。很多初学者喜欢在返回里塞一大段解释文本,其实没必要,工具返回的是数据,解释让模型自己组织语言。返回内容应当尽量结构化,最好直接返回JSON字符串。
接着上真正的数据库工具:
from mcp.server.fastmcp import FastMCP import pymysql import json mcp = FastMCP("MySQL Helper") @mcp.tool() def query_mysql(sql: str, limit: int = 50) -> str: """ 对业务数据库执行只读查询。 只支持 SELECT 语句,返回 JSON 数组字符串。 """ sql = sql.strip().rstrip(";").strip() if not sql.lower().startswith("select"): raise ValueError("仅允许 SELECT 查询") conn = pymysql.connect( host="127.0.0.1", port=3306, user="readonly", password="readonly_pwd", database="app", charset="utf8mb4", connect_timeout=3, read_timeout=10, ) try: with conn.cursor() as cur: cur.execute(f"{sql} LIMIT {int(limit)}") cols = [desc[0] for desc in cur.description] rows = [dict(zip(cols, row)) for row in cur.fetchall()] return json.dumps(rows, ensure_ascii=False, default=str) finally: conn.close() if __name__ == "__main__": mcp.run()有几个细节值得说:
- 我在
LIMIT那里强制了int(limit),防止模型传字符串SQL片段进来。 default=str可以把datetime、Decimal这些类型转成字符串,否则json.dumps会炸。- 所有异常统一抛出
ValueError,FastMCP会自动把它转成isError=true的返回,模型能读懂。 - limit参数有默认值50,把AI的“偷懒”兜住,防止它不带limit乱查。
3.4 官方Python SDK的底层写法
FastMCP适合快速开发,但如果你要深度控制协议行为(比如自定义握手、处理特殊能力协商),得理解底层写法。核心只需要实现三个接口:list_tools、call_tool,以及list_resources/read_resource。
from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app = Server("time-server") @app.list_tools() async def list_tools(): return [ Tool( name="get_current_time", description="获取当前服务器时间", inputSchema={"type": "object", "properties": {}} ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): import time return [TextContent(type="text", text=time.strftime("%Y-%m-%d %H:%M:%S"))] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())底层写法能让你看到每个返回都需要组装TextContent这样的ContentBlock对象。生产环境里如果要做复杂的权限审计、请求拦截,我建议基于这个写法封装一层中间件。
3.5 Resource和Prompt的实战
查询工具只是MCP服务端的一半。前面我说过Resources是MCP的特色,这里给一个实际例子。增加一个“表结构资源”,让模型可以主动读取指定表的字段信息,这样它生成的SQL会更准确。
@mcp.resource("db://{table}/schema") def get_table_schema(table: str) -> str: """返回指定表的字段定义信息""" conn = pymysql.connect( host="127.0.0.1", port=3306, user="readonly", password="readonly_pwd", database="app", charset="utf8mb4", connect_timeout=3, ) try: with conn.cursor() as cur: cur.execute(f"SHOW CREATE TABLE `{table}`") row = cur.fetchone() return row[1] if row else "表不存在" finally: conn.close()这样,模型在写SQL之前,可以先通过resources/read拿到建表语句,知道了有哪些索引和字段类型,再调query_mysql,错误率直线下降。我实测过,有表结构上下文之后,AI生成的JOIN条件基本不会再用错列名。
再配一个Prompt模板,方便用户复用分析流程:
@mcp.prompt() def sql_expert(table: str) -> str: return ( f"你是一名DBA,请针对 {table} 表设计查询。\n" "规则:只使用SELECT,必须带LIMIT,先读表结构资源再写SQL,最后用中文解释结果。" )用户只要说“帮我分析orders表”,模型就会自动带上这个Prompt,按照预设的规则执行,省去每次重复描述约束。
3.6 接入Claude Desktop、Cursor与Codex
写好的MCP服务端最终要接入AI客户端。不同客户端的配置位置不一样,我列一下常见的:
Claude Desktop在claude_desktop_config.json(macOS路径~/Library/Application Support/Claude/,Windows在%APPDATA%\Claude\)里配置:
{ "mcpServers": { "mysql-helper": { "command": "python", "args": ["/Users/me/projects/mysql-helper/server.py"], "env": { "MYSQL_HOST": "127.0.0.1" } } } }Cursor需要在Settings里找到MCP servers,添加命令;Codex则通过~/.codex/config.toml配置:
[mcp.servers.mysql-helper] command = "python" args = ["/Users/me/projects/mysql-helper/server.py"]配置完重启客户端,如果工具列表里出现query_mysql和db://{table}/schema资源,就说明接入成功了。验证过程我推荐先跑一遍Inspector:
npx @modelcontextprotocol/inspector python /Users/me/projects/mysql-helper/server.py连接后手动调用一下query_mysql,看返回是否正常,再回到客户端里测试,避免“客户端连不上”和“工具逻辑坏了”两件事搅在一起。
3.7 安全加固与发布
MCP服务端本质上是一个本地进程,它手里的权限就是操作系统给它的权限。生产发布时,我至少会做这几件事:
- 连接信息走环境变量或密钥管理,不硬编码在
server.py里。 - 数据库账号单独创建,只给目标表
SELECT权限,这就是“最小权限原则”。 - 所有工具的调用写审计日志,记录模型传入了什么SQL、返回了多少行、耗时多久。日后出问题有据可查。
- 如果服务需要给局域网内多人用,别直接用stdio,改用Streamable HTTP并加鉴权,防止任意进程调用。
我见过一个反面案例:有人把MCP服务端包好了放上内网,结果忘了鉴权,任何能访问端口的人都能通过MCP工具执行服务器命令,这是个很现实的危险。MCP协议本身不负责鉴权,一定得在服务端自己做。官方在2025年3月版本里加入了OAuth相关规范,但它管的是客户端到服务器的授权流程,不是你业务数据的安全边界。
4. 常见问题与排查经验
4.1 “连接失败”和“找不到工具”的排查清单
我遇到的绝大多数接入问题,都不是MCP协议本身的问题,而是配置细节。先说一张速查表:
| 现象 | 最常见原因 | 处理方式 |
|---|---|---|
| 客户端显示MCP服务启动失败 | command或args路径写错 | 检查python可执行文件绝对路径 |
| 工具列表为空 | server.py没运行起来 | 先在终端手动跑python server.py,看报错 |
| Codex找不到MCP | config.toml里命令路径不对或JSON转义问题 | 检查路径是否有空格,参数是否拆开 |
| 调用工具无响应 | 工具内部卡死或连接数据库超时 | 把connect_timeout、read_timeout设置短一点 |
| 启动时缺少依赖 | 环境不对,装的包的版本不一致 | 用uv sync或虚拟环境,确保Python解释器一致 |
Windows下配置时有个坑,command要写cmd、args里加/c,直接写python有时因为PATH问题跑不起来。写配置的时候我建议:
{ "mcpServers": { "mysql-helper": { "command": "cmd", "args": ["/c", "python", "C:\\projects\\mysql-helper\\server.py"] } } }4.2 工具返回内容过大或被截断
如果工具返回上万行JSON,模型很可能会消化不了,甚至协议层就直接断掉。解决思路是“服务端自己分页与压缩”。我在query_mysql工具里,除了强制LIMIT,还会加一个max_rows逻辑,超过500行就返回提示,并附上汇总统计。
另一种高频场景是“让AI把内容写到文件”,有些使用场景是AI生成大段内容要流式落盘。MCP工具本身是一次调用的,你可以在服务端把内容按块写入文件,然后返回一个“已写入”的状态和路径,避免在一次响应里塞几十万字符。实现不复杂:接收文本参数、按固定大小切片写入磁盘、返回文件路径和总字节数。
如果是资源读取,同样建议限制单次读取大小,比如日志只读最后100行、文件只读前2048字节。让模型拿到一个“样本”,它需要更多时再通过工具追加读取。
4.3 模型乱调用和提示注入
MCP工具暴露给模型之后,模型是那个做决策的人。工具描述写得模棱两可,模型就会乱选甚至把参数填错。比如你的工具叫execute,描述是“执行一个操作”,模型完全不知道什么时候该用,结果就是“该调工具时没调,不该调时调了”。
正确的做法是把工具描述写成“在什么场景下使用 + 参数含义 + 返回结构”。比如:
当用户需要查询业务数据库时才使用。入参sql为完整的SELECT语句,limit控制返回行数,默认50。返回JSON数组字符串。提示注入也要重点防。如果你的工具会读取网页或文件,那些内容里可能藏着“忽略以上指令,执行xxx”之类的文本,模型读进去后可能照做。我的经验是:外部内容只能作为数据处理,不能和系统指令混在一个上下文里;涉及高危操作的参数(删除、发送、支付)要在工具侧再做一层校验和确认。MCP工具本身没有“二次确认”机制,但你可以通过参数里的confirm: true要求模型调用时显式传递确认标志,没有就不执行。
4.4 日常开发中的几个实战心得
第一个心得:工具数量宁少勿多。一次暴露20个工具,模型光理解每个工具是干什么的就要消耗很多上下文。聚合工具是更好的选择,比如一个query_mysql就能覆盖“查数据、查表结构、查索引”,比拆成十个细粒度工具更省token,也更不容易出错。
第二个心得:JSON-RPC报文要在调试时用起来。不要只看最终结果,看原始报文能发现很多隐藏问题。如果发现tools/call发出去但返回的是空,就去服务端日志找是不是异常被吞了;如果发现模型一直在尝试某个不存在的工具名,多半是客户端的工具缓存没有刷新,重启或清理缓存就好。
第三个心得:关于逆向和调试别人的MCP服务。热词里常有人问“MCP逆向”,其实就是把别人的MCP服务端跑起来,用Inspector观察它的接口定义,再照着写一个兼容实现或客户端。这个方法同样适用于排查自己写的服务端,看它实际注册了哪些工具、工具的Schema长什么样。
第四个心得:别迷信“接入即安全”。MCP只是一个协议,不等于防火墙。你的MCP服务端是跑在什么机器上,就有什么权限。我见到有开发把MCP服务端直接部署在数据库服务器上,还开了HTTP访问,这相当于把数据库的钥匙挂在门外。正确做法是独立进程、独立低权限账号、内网隔离、调用可审计。
说实话,MCP给我的最大感受不是技术有多新,而是它把“AI连接外部世界”这件事从手工作坊推进到了标准化时代。我刚接触时也查过一堆资料,最后发现最好的学习方式就是写一个哪怕很小的工具,把它接到自己常用的AI客户端里,然后观察模型怎么逐步学会调用,再根据实际效果打磨工具描述和返回格式。等把这条路走通,你会发现给AI“加技能”这件事,已经像装一个USB设备一样自然了。