
去年我在折腾Agent项目的时候最头疼的事不是模型切换而是让AI真正“摸”到数据和工具。文件、数据库、浏览器、API每接一个都要单独写一套适配逻辑写完还只能在自家程序里用。直到我把MCPModel 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://userhost/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.isErrortrue 普通内容里。这样模型能直接看到错误文本理解率更高。作为工具开发者你的工具内部抛异常时一定要捕获并把人类可读的错误信息返回给模型而不是让连接层断掉。2.4 协议版本与SDK选择MCP协议还在快速演进目前主流客户端默认支持2025-03-26版本这个版本引入了Streamable HTTP传输取代了早期不稳定的HTTPSSE组合。另外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 SDKnpm 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( host127.0.0.1, port3306, userreadonly, passwordreadonly_pwd, databaseapp, charsetutf8mb4, connect_timeout3, read_timeout10, ) 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_asciiFalse, defaultstr) finally: conn.close() if __name__ __main__: mcp.run()有几个细节值得说我在LIMIT那里强制了int(limit)防止模型传字符串SQL片段进来。defaultstr可以把datetime、Decimal这些类型转成字符串否则json.dumps会炸。所有异常统一抛出ValueErrorFastMCP会自动把它转成isErrortrue的返回模型能读懂。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( nameget_current_time, description获取当前服务器时间, inputSchema{type: object, properties: {}} ) ] app.call_tool() async def call_tool(name: str, arguments: dict): import time return [TextContent(typetext, texttime.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( host127.0.0.1, port3306, userreadonly, passwordreadonly_pwd, databaseapp, charsetutf8mb4, connect_timeout3, ) try: with conn.cursor() as cur: cur.execute(fSHOW 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.jsonmacOS路径~/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资源就说明接入成功了。验证过程我推荐先跑一遍Inspectornpx 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找不到MCPconfig.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设备一样自然了。