1. MCP 协议到底是什么,为什么工具开发绕不开它
1.1 从一个真实场景说起:为什么我们需要 MCP
我最早接触 MCP 是在做一个内部知识库助手的时候。当时的需求很朴素:让 AI 能读我们本地的文档、查数据库、调内部 API。听起来不难对吧?但真动手就发现问题了——每接一个数据源,就要写一套专门的适配代码。今天接 MySQL,明天接飞书文档,后天接公司自研的工单系统,每个都要单独处理鉴权、参数格式、返回结构。代码越堆越多,维护成本直线上升。
这就是 MCP 要解决的核心痛点。MCP 全称 Model Context Protocol,翻译过来叫“模型上下文协议”。你可以把它理解成 AI 应用和外部工具之间的“USB-C 接口”——以前每个设备有自己的充电口,现在统一成一个标准,谁都能插。MCP 做的事情就是定义一套标准协议,让 AI 模型(或者更准确地说,Agent)能够用统一的方式去发现、调用外部工具和数据源。
没有 MCP 的时候,你写一个 Agent 要调 10 个工具,就得写 10 套对接逻辑。有了 MCP,你只需要实现一次 MCP 客户端,所有遵循 MCP 协议的服务端都能直接接入。这个价值在工具数量少的时候不明显,但一旦超过三五个,差距就出来了。
1.2 MCP 的核心架构:三个角色各干什么
MCP 的架构其实不复杂,核心就三个角色:
- MCP Host:承载 AI 对话的应用,比如你用的桌面客户端、IDE 插件、聊天工具。它是发起请求的一方。
- MCP Client:Host 内部用来和 Server 通信的组件,负责协议握手、消息收发、能力协商。
- MCP Server:对外暴露工具、资源、提示词的服务端。你写的“工具开发”主要就是写这个。
三者之间的关系是:Host 创建 Client,Client 连接 Server,Server 提供能力。一个 Host 可以同时连接多个 Server,每个 Server 提供不同的工具集。比如你可以同时接一个文件系统 Server、一个数据库 Server、一个搜索 Server,Agent 在需要的时候自动选择合适的工具。
这里有个容易混淆的点:MCP Server 不是 HTTP 服务器那种概念。它可以是本地进程(通过 stdio 通信),也可以是远程服务(通过 SSE 或 Streamable HTTP 通信)。选择哪种传输方式,取决于你的工具是本地资源还是远程资源。
1.3 MCP 能提供什么:工具、资源、提示词三件套
MCP Server 对外暴露的能力分三类:
Tools(工具)是最常用的。它代表可执行的操作,比如“查询数据库”“发送邮件”“创建工单”。Agent 会根据用户意图决定调用哪个工具,传入参数,拿到结果。工具的定义包含名称、描述、参数 schema,这些信息会告诉模型“这个工具能干什么、需要什么参数”。
Resources(资源)代表可读取的数据。比如文件内容、数据库记录、API 返回的 JSON。和工具的区别在于,资源是“读”的,工具是“做”的。资源通常用 URI 标识,比如file:///path/to/doc.md或db://users/123。
Prompts(提示词)是预定义的提示模板。比如你可以定义一个“代码审查”提示词,Agent 调用后会自动填充上下文。这个功能用得相对少,但在标准化工作流里很有价值。
理解这三者的区别很重要,因为它决定了你开发工具时的设计思路。如果你要暴露的是“查询天气”这种操作,那是 Tool;如果要暴露的是“天气数据文件”,那是 Resource。
1.4 为什么现在学 MCP 工具开发正当时
从热搜词能看出来,MCP 的讨论热度在快速上升。mcp是什么、mcp协议、codex无法找到mcp、codex 接入 figma mcp 怎么授权这些搜索词说明两件事:一是大量开发者在尝试接入 MCP,二是接入过程中遇到了各种问题。
同时,ruoyi-vue-pro合并mcp功能、unreal 5.8 mcp、x32dbg 的 mcp插件这些词说明 MCP 正在向各个领域渗透——不只是 AI 工具,传统软件也在考虑怎么和 MCP 结合。这意味着掌握 MCP 工具开发能力,在未来一段时间内会是很有竞争力的技能。
而且 MCP 的学习曲线其实不算陡。只要你懂 Python 基础,理解 JSON-RPC 的基本概念,就能上手写一个可用的 MCP Server。下面我会从零开始,把整个开发流程拆开讲。
2. 开发环境搭建与第一个 MCP Server
2.1 Python 环境准备:避开新手常踩的坑
写 MCP Server 最顺手的语言是 Python,官方也提供了 Python SDK。但 Python 环境本身就有不少坑,我见过太多人卡在安装环节。
首先确认你的 Python 版本。MCP Python SDK 要求 Python 3.10 及以上,推荐 3.11 或 3.12。用python --version检查,如果低于 3.10,先去 python 官网下载新版。Windows 用户注意安装时勾选“Add Python to PATH”,否则后面命令行找不到 python 命令。
虚拟环境是必须的。不要直接在系统 Python 里装包,否则依赖冲突会让你怀疑人生。创建虚拟环境的命令:
python -m venv mcp-env # Windows mcp-env\Scripts\activate # macOS/Linux source mcp-env/bin/activate激活后命令行前面会出现(mcp-env)标识。然后安装 MCP SDK:
pip install mcp如果你还需要处理 HTTP 请求、数据库连接等,按需安装httpx、sqlalchemy等库。安装 numpy 这类科学计算库时,如果遇到编译错误,优先用pip install numpy而不是源码编译,官方 wheel 包通常能直接装上。
注意:国内网络环境下 pip 安装可能较慢,可以配置镜像源加速。但不要混用多个镜像源,否则可能出现包版本不一致的问题。
2.2 MCP Server 的最小骨架:20 行代码跑起来
先看一个能跑的最小例子,理解 MCP Server 的基本结构:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("demo-server") @mcp.tool() def add(a: int, b: int) -> int: """计算两个整数之和""" return a + b if __name__ == "__main__": mcp.run()就这么简单。FastMCP是官方提供的高层封装,帮你处理了协议握手、消息路由、参数校验这些底层细节。你只需要用@mcp.tool()装饰器标记一个函数,它就自动变成一个 MCP 工具。
函数名add就是工具名,docstring 就是工具描述,类型注解a: int, b: int会自动生成参数 schema。模型看到这些信息后,就知道“有个叫 add 的工具,需要两个整数参数,返回整数”。
运行python server.py,Server 会通过 stdio 启动,等待客户端连接。你可以用 MCP Inspector 这个调试工具来测试,命令是npx @modelcontextprotocol/inspector python server.py。Inspector 会打开一个网页界面,让你手动调用工具、查看返回结果。
2.3 工具定义的三个关键要素:名称、描述、参数
写工具最容易犯的错误是描述写得太随意。模型是靠描述来判断该不该调用这个工具的,描述不清楚,模型就不会用,或者用错。
名称要简洁明确,用动词开头。get_weather比weather好,search_documents比doc_search好。名称是模型首先看到的信息,好的名称能让模型快速理解工具用途。
描述要写清楚三件事:这个工具做什么、什么时候用、返回什么。比如:
@mcp.tool() def query_user_orders(user_id: str, status: str = "all") -> list: """查询指定用户的订单列表。 当用户询问订单相关问题时使用此工具。 user_id: 用户唯一标识 status: 订单状态筛选,可选值 all/pending/completed/cancelled 返回订单列表,每条包含订单号、金额、状态、创建时间 """ ...参数的类型注解要准确。int和str不要搞混,可选参数用默认值标记。如果参数是枚举类型,在描述里列出所有可选值。模型会根据这些信息生成调用参数,类型不对会导致调用失败。
2.4 用 MCP Inspector 调试你的第一个工具
写完工具不要急着接到 Agent 里,先用 Inspector 单独测试。Inspector 能让你看到工具列表、手动传参、查看原始请求和响应。
启动 Inspector 后,你会看到左侧列出所有工具。点击某个工具,右侧会显示它的 schema 和调用界面。填入参数点“Call”,就能看到返回结果。如果报错,错误信息会显示在下方。
我习惯在开发阶段把每个工具都单独测一遍,确认参数边界情况。比如传空字符串会怎样、传超大数字会怎样、传错误类型会怎样。这些边界情况在 Agent 实际调用时都可能出现,提前处理好能省很多排查时间。
提示:Inspector 的“History”面板会记录所有调用,方便你对比不同参数的结果。调试复杂工具时很有用。
3. 工具开发进阶:从能用到好用
3.1 参数校验与错误处理:让工具更健壮
最小示例能跑,但离生产可用还有距离。第一个要补的是参数校验和错误处理。
MCP SDK 会自动做基础类型校验,但业务逻辑校验要自己写。比如 user_id 不能为空、status 必须是合法枚举值、分页参数不能为负。这些校验要在工具函数开头做,发现问题立即返回错误信息。
错误返回的格式有讲究。不要直接抛异常,而是返回结构化的错误信息,让模型能理解并决定下一步。比如:
@mcp.tool() def query_user_orders(user_id: str, status: str = "all") -> dict: if not user_id: return {"error": "user_id 不能为空", "code": "INVALID_PARAM"} if status not in ("all", "pending", "completed", "cancelled"): return {"error": f"status 必须是 all/pending/completed/cancelled 之一", "code": "INVALID_PARAM"} try: orders = db.query_orders(user_id, status) return {"orders": orders, "count": len(orders)} except Exception as e: return {"error": str(e), "code": "QUERY_FAILED"}这样模型拿到错误后,可以决定是重试、换参数、还是告诉用户出错了。如果直接抛异常,整个调用链可能中断,体验很差。
3.2 异步工具与并发处理:别让一个慢查询卡死整个 Agent
MCP 支持异步工具,用async def定义即可。当工具有 IO 操作(数据库查询、HTTP 请求、文件读写)时,强烈建议用异步。
@mcp.tool() async def fetch_remote_data(url: str) -> dict: async with httpx.AsyncClient() as client: resp = await client.get(url, timeout=10) return resp.json()异步的好处是,当一个工具在等待 IO 时,Server 可以处理其他请求。如果 Agent 同时调多个工具,异步能显著提升吞吐。
但异步也有坑。不要在异步函数里调用阻塞的同步代码,比如time.sleep()、同步的数据库驱动。这会阻塞事件循环,让异步失去意义。如果必须用同步库,用asyncio.to_thread()包一层。
关于并发,MCP Server 本身不限制并发数,但你的下游资源可能有限制。比如数据库连接池只有 10 个连接,同时来 100 个请求就会排队。这种情况下要在工具内部做限流,或者用信号量控制并发。
3.3 资源与提示词的开发要点
Resource 的开发用@mcp.resource()装饰器,参数是 URI 模板:
@mcp.resource("file://docs/{path}") def read_doc(path: str) -> str: with open(f"./docs/{path}", "r") as f: return f.read()Resource 适合暴露静态或半静态数据。和 Tool 的区别是,Resource 通常不需要复杂参数,主要是按标识读取。
Prompt 用@mcp.prompt()装饰器:
@mcp.prompt() def code_review(code: str) -> str: return f"请审查以下代码,指出潜在问题:\n\n{code}"Prompt 的价值在于标准化常用指令。团队里每个人写提示词的水平参差不齐,把好的提示词固化成 Prompt,能保证输出质量一致。
3.4 工具设计的心法:让模型“想用、会用、用对”
工具能不能被模型正确使用,设计占七成。我总结了几条经验:
粒度要适中。太细的工具(比如get_user_name、get_user_age)会让模型调用多次,效率低;太粗的工具(比如do_everything)模型不知道怎么传参。一个好的工具应该对应一个完整的用户意图。
描述要具体。不要写“查询数据”,要写“根据用户 ID 查询该用户的订单列表,支持按状态筛选”。模型是靠描述做决策的,描述越具体,调用越准确。
返回值要结构化。返回 JSON 比返回纯文本好,因为模型能更好地理解字段含义。如果返回列表,带上总数、分页信息。
错误信息要可操作。不要只说“失败了”,要说“user_id 格式错误,应该是 32 位十六进制字符串”。模型看到具体原因后,能自动修正参数重试。
4. 接入真实场景:MCP 工具开发的实战案例
4.1 案例一:数据库查询工具
这是最常见的场景。假设你有一个 MySQL 数据库,想让 Agent 能查询数据。
import asyncmy from mcp.server.fastmcp import FastMCP mcp = FastMCP("db-server") @mcp.tool() async def query_table(table: str, limit: int = 100) -> dict: """查询指定表的数据。 table: 表名,仅支持 users/orders/products limit: 返回条数,默认 100,最大 1000 """ allowed_tables = {"users", "orders", "products"} if table not in allowed_tables: return {"error": f"表名必须是 {allowed_tables} 之一", "code": "INVALID_TABLE"} if limit > 1000: limit = 1000 conn = await asyncmy.connect(host="localhost", user="readonly", password="***", db="mydb") async with conn.cursor() as cur: await cur.execute(f"SELECT * FROM {table} LIMIT %s", (limit,)) rows = await cur.fetchall() columns = [d[0] for d in cur.description] conn.close() return {"columns": columns, "rows": rows, "count": len(rows)}这里有几个安全考虑:表名用白名单校验,防止 SQL 注入;limit 设上限,防止一次拉太多数据;数据库用只读账号,避免误操作。
4.2 案例二:调用外部 API 的工具
很多场景需要调第三方 API。以天气查询为例:
@mcp.tool() async def get_weather(city: str, days: int = 1) -> dict: """查询指定城市未来几天的天气。 city: 城市名称,如"北京"、"上海" days: 预报天数,1-7,默认 1 """ if days < 1 or days > 7: return {"error": "days 必须在 1-7 之间", "code": "INVALID_PARAM"} async with httpx.AsyncClient() as client: resp = await client.get( "https://api.weather.com/v1/forecast", params={"city": city, "days": days}, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=10 ) if resp.status_code != 200: return {"error": f"API 返回 {resp.status_code}", "code": "API_ERROR"} data = resp.json() return {"city": city, "forecast": data["forecast"]}外部 API 调用要处理超时、重试、错误码。超时设 10 秒左右比较合理,太短容易误判,太长会拖慢 Agent 响应。
4.3 案例三:文件系统操作工具
让 Agent 能读写本地文件是常见需求,但安全风险也最大。必须限制访问范围:
import os from pathlib import Path BASE_DIR = Path("/safe/workspace").resolve() @mcp.tool() def read_file(relative_path: str) -> dict: """读取工作区内的文件内容。 relative_path: 相对于工作区的路径,如 "docs/readme.md" """ target = (BASE_DIR / relative_path).resolve() if not str(target).startswith(str(BASE_DIR)): return {"error": "路径越界,只能访问工作区内文件", "code": "PATH_ESCAPE"} if not target.exists(): return {"error": "文件不存在", "code": "NOT_FOUND"} if target.stat().st_size > 1024 * 1024: return {"error": "文件超过 1MB 限制", "code": "TOO_LARGE"} with open(target, "r", encoding="utf-8") as f: content = f.read() return {"path": relative_path, "content": content, "size": len(content)}路径越界检查是必须的。用户可能传../../etc/passwd这种路径,如果不检查就会读到敏感文件。用resolve()解析真实路径后再判断前缀,能防止大部分路径穿越攻击。
4.4 案例四:流式输出到文件
热搜词里有个使用mcp工具流式输出内容到文件,这个需求在生成大段内容时很常见。MCP 工具本身返回的是完整结果,但你可以设计一个工具来追加写入:
@mcp.tool() def append_to_file(filename: str, content: str) -> dict: """将内容追加到指定文件。 filename: 文件名,仅支持 .txt/.md/.log content: 要追加的内容 """ if not filename.endswith((".txt", ".md", ".log")): return {"error": "仅支持 txt/md/log 文件", "code": "INVALID_TYPE"} target = (BASE_DIR / filename).resolve() if not str(target).startswith(str(BASE_DIR)): return {"error": "路径越界", "code": "PATH_ESCAPE"} with open(target, "a", encoding="utf-8") as f: f.write(content + "\n") return {"status": "ok", "file": filename, "appended": len(content)}Agent 可以多次调用这个工具,每次追加一段,实现流式写入的效果。配合read_file工具,还能实现读取-修改-写回的完整流程。
5. 常见问题排查与避坑指南
5.1 连接类问题:Client 找不到 Server 怎么办
codex无法找到mcp是高频搜索词,说明很多人卡在连接环节。常见原因和排查方法:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| Client 提示找不到 Server | 命令路径不对 | 用绝对路径,确认 python 在 PATH 中 |
| 连接后立即断开 | Server 启动报错 | 手动运行 server.py 看报错信息 |
| 工具列表为空 | 装饰器没生效 | 确认@mcp.tool()在mcp.run()之前定义 |
| 调用超时 | 工具执行太久 | 加日志,确认卡在哪一步 |
最常见的是路径问题。Client 配置里写的python server.py,如果工作目录不对就找不到文件。建议用绝对路径:/usr/bin/python3 /home/user/mcp/server.py。
另一个坑是虚拟环境。如果 Server 依赖装在 venv 里,Client 启动时用的却是系统 Python,就会报模块找不到。解决办法是在 Client 配置里直接指向 venv 的 python:/home/user/mcp-env/bin/python。
5.2 工具调用失败:参数与返回值问题
工具调用失败通常有几类:
参数类型不匹配。模型传了字符串,但工具期望整数。这种情况要在描述里写清楚类型,或者用 Pydantic 做强制转换。
必填参数缺失。模型可能漏传参数。给参数设默认值,或者在函数开头检查并返回友好错误。
返回值太大。如果工具返回几十 MB 的数据,可能超出协议限制或导致超时。大结果要分页,或者返回摘要加引用 ID。
返回值不是 JSON 可序列化的。比如返回了 datetime 对象、自定义类实例。要转成字符串或字典再返回。
提示:在工具里加日志很有必要。用
logging模块记录每次调用的参数和结果,出问题时能快速定位。
5.3 授权与鉴权:接入第三方 MCP 的注意事项
codex 接入 figma mcp 怎么授权这类问题说明第三方 MCP 的授权流程是个门槛。通常分几种情况:
API Key 方式。最简单,在配置里填 Key 即可。注意 Key 不要硬编码在代码里,用环境变量或配置文件。
OAuth 方式。需要走授权流程,通常要在浏览器里登录并授权。这类 MCP 的配置文档会说明具体步骤,跟着做就行。
本地授权。有些 MCP 需要读取本地凭证文件,比如~/.config/xxx/credentials.json。确认文件存在且权限正确。
授权失败时,先检查凭证是否过期,再检查网络是否能访问授权服务器。如果都不行,看 Server 的日志输出,通常会有具体错误信息。
5.4 性能优化:让工具响应更快
工具响应慢会拖累整个 Agent 体验。几个优化方向:
缓存。对于不常变的数据,加内存缓存。比如配置信息、字典数据,启动时加载一次,后续直接读内存。
连接池。数据库、HTTP 客户端都用连接池,避免每次调用都新建连接。httpx.AsyncClient可以复用,asyncmy有连接池。
异步。IO 密集的操作全部异步化,让多个请求能并发处理。
限制返回量。默认返回条数设小一点,比如 20 条,让模型按需请求更多。一次返回几千条既慢又浪费 token。
超时控制。每个外部调用都设超时,避免一个慢请求卡死整个工具。超时后返回错误,让模型决定是否重试。
5.5 安全红线:工具开发不能碰的几条线
工具开发有几条安全底线必须守住:
路径限制。文件操作必须限制在指定目录内,用resolve()检查真实路径,防止路径穿越。
命令注入。不要用os.system()或subprocess执行用户传入的命令。如果必须执行,用白名单限制可执行文件。
SQL 注入。表名、字段名用白名单,值用参数化查询。不要拼接 SQL 字符串。
权限最小化。数据库用只读账号,文件系统用受限目录,API 用最小权限的 Key。
敏感信息。不要在返回值里包含密码、密钥、内部 IP。日志里也要脱敏。
这些不是可选项,是必须做的。一旦出问题,影响的不只是你的项目,还可能波及整个系统。
6. 从工具开发到 Agent 集成:完整链路打通
6.1 MCP Server 的部署方式选择
开发完 Server 后,要决定怎么部署。三种常见方式:
本地 stdio。Server 和 Client 在同一台机器,通过标准输入输出通信。适合个人使用、本地工具。配置简单,不需要网络。
SSE(Server-Sent Events)。Server 作为 HTTP 服务运行,Client 通过 SSE 连接。适合团队共享、远程访问。需要处理端口、防火墙、鉴权。
Streamable HTTP。较新的传输方式,支持更灵活的流式通信。适合需要双向流、大文件传输的场景。
选择依据:如果工具只给自己用,stdio 最简单;如果要团队共享,用 SSE 或 HTTP;如果涉及敏感数据,优先本地 stdio,避免网络暴露。
6.2 多 Server 协同:一个 Agent 接多个工具源
实际项目里,一个 Agent 往往需要接多个 MCP Server。比如一个接数据库,一个接文件系统,一个接搜索。Client 会同时连接这些 Server,把所有工具汇总成一个列表。
这里要注意工具名冲突。两个 Server 都有search工具,Client 会怎么处理?通常会给工具名加前缀,比如db_search、file_search。你在开发 Server 时,工具名要尽量独特,避免和常见工具重名。
另一个问题是上下文长度。工具太多会占用大量 token。如果接了 50 个工具,光工具描述就几千 token。解决办法是按需加载,或者把相关工具合并成一个 Server。
6.3 调试技巧:用日志和 Inspector 定位问题
调试 MCP 问题,两个工具最有用:
Server 日志。在工具函数里加logging.info(),记录入参和出参。出问题时看日志,能快速定位是参数问题还是逻辑问题。
MCP Inspector。前面提过的调试工具,能独立测试每个工具,不依赖 Agent。当 Agent 调用出问题时,先用 Inspector 确认工具本身是否正常。
如果 Inspector 正常但 Agent 调用失败,问题通常在 Client 配置或模型理解上。检查 Client 配置是否正确,工具描述是否清晰。
6.4 版本管理与兼容性:别让升级搞崩现有工具
MCP 协议还在演进,SDK 版本更新可能带来不兼容。几个建议:
锁定版本。在requirements.txt里写死版本号,比如mcp==1.2.0,避免自动升级导致问题。
关注变更日志。升级前看 SDK 的 release notes,确认有没有破坏性变更。
工具描述向后兼容。如果修改了工具的参数或返回值,要考虑现有 Agent 是否还能正常工作。必要时保留旧版本工具,新增新版本。
测试覆盖。每个工具都要有测试用例,升级后跑一遍,确认没坏。
7. 工具开发的扩展方向与个人体会
7.1 从单工具到工具链:组合出更强大的能力
单个工具的能力有限,但组合起来能实现复杂功能。比如:
list_files+read_file+write_file= 文件管理能力query_db+format_data+send_email= 报表自动化search_web+summarize+save_note= 研究助手
设计工具时要有“工具链”思维。每个工具做好一件事,Agent 会自动组合它们完成复杂任务。这比做一个大而全的工具更灵活,也更容易维护。
7.2 工具生态的想象空间
MCP 的价值会随着工具数量增长而放大。现在已经有很多现成的 MCP Server,覆盖文件、数据库、搜索、通讯等常见场景。你可以直接用,也可以参考它们的实现来写自己的工具。
对于垂直领域,比如工业协议(Modbus、OPC UA、CAN)、硬件调试(UART、MIPI)、特定软件(Unreal、Figma),MCP 工具能把专业能力封装成标准接口,让 AI 助手直接调用。这个方向的空间很大,值得深入探索。
7.3 我踩过的坑和总结的经验
最后分享几条个人经验:
先跑通再优化。不要一开始就追求完美架构,先用最小实现跑通流程,再逐步加校验、加缓存、加异步。
描述比代码重要。工具能不能被模型用好,描述占七成。花时间打磨描述,比优化代码收益更大。
错误信息要具体。模型是靠错误信息来修正行为的。模糊的错误信息会让模型反复试错,具体的错误信息能让模型一次改对。
安全不能妥协。路径限制、参数校验、权限最小化,这些是底线。宁可功能少一点,也不能留安全漏洞。
多测试边界情况。空值、超长字符串、特殊字符、并发调用,这些在开发时就要测到。等上线后出问题再修,成本高得多。
工具开发这件事,入门容易精通难。但只要你理解了 MCP 的核心思路,掌握了工具设计的基本原则,剩下的就是不断实践和迭代。每写一个工具,你对“模型需要什么”“怎么让模型用对”的理解就会更深一层。这个积累过程本身,就是最有价值的部分。