尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零搭建MCP Server:AI Agent工具链实战指南

从零搭建MCP Server:AI Agent工具链实战指南 最近这半年只要你在搞 AI 应用开发耳朵边就不可能没听过 MCP 协议。我自己的感受是从年初开始身边但凡做 Agent 落地项目的团队几乎都从自研 Function Calling 切到了 MCP 这套东西上。原因也很直白——你与其给每个 AI 项目写一套工具接入层不如直接按 MCP 的规范暴露工具一次接入处处复用。这篇文章我打算直接以“从零搭一个能跑的 Agent 工具链”为线索讲清楚三件事MCP 协议到底解决了什么问题、怎么快速写一个 MCP Server、怎么让 AI Agent 真正把工具调起来。全程基于我自己的实际工程经验踩过的坑、选型时的纠结、线上排查的日志我都会写进来希望能帮你省掉几周的摸索时间。1. 为什么是 MCPAI Agent 工具化的核心问题1.1 AI Agent 要干活先解决“工具接入”问题我们做 Agent 的人都知道一个尴尬现实大模型本身不会执行任何动作它只会生成文本。要让 Agent 去查数据库、发邮件、操作浏览器必须把能力封装成“工具”交给模型调用。传统做法是各自为政项目里写一堆 JSON Schema然后硬编码到模型请求里。举个例子以前我在一个项目里给模型暴露了查天气、查订单、发短信三个工具。每个工具的入参定义、鉴权方式、返回格式都不一样后面又加了一个内部 CRM 的工具结果模型三天两头把参数传错。最烦的是换一个大模型厂商的 API工具描述全要重写一遍。这不是配置问题而是整个工具调用链路的协议不能复用。MCP 的全称是 Model Context Protocol它做的事情就是给“模型怎么调用工具”定了一套标准化协议。这个概念很像 USB-C以前每个设备都有自己的充电口现在统一成一个口哪个充电器都能插。MCP 把 AI 应用的“工具端”和“模型端”彻底解耦工具提供方写好一次任何支持 MCP 的客户端都能直接用。1.2 统一标准之后开发模式发生了什么变化引入 MCP 之后开发 Agent 工具链的方式发生了一个关键转变不再为了某个 Agent 写工具而是为工具生态写 Agent。这里我想用我的实际体会来说明。以前我们做智能客服机器人需要把商品查询、订单追踪、退换货物流写进同一个系统里所有逻辑都耦合在一个进程。现在结构变成商品服务作为独立 MCP Server 启动订单服务也作为独立 MCP Server 启动Agent 主程序只负责发现这些 Server、读取它们的工具列表然后在对话中动态决定调哪个。这个模式的好处我在项目里体会特别深业务方新增一个数据查询能力只需要让后端同事按 MCP 规范写一个 Server我这边一行 Agent 代码都不用改重启一下就能看到新工具出现在模型可调用的列表里。这种“即插即用”的开发节奏比之前每个接口都要手动对接高效太多。1.3 和 Function Calling 对比不是一个层级的方案很多人问OpenAI 的 Function Calling 不是也能让模型调工具吗为什么还要学 MCP我的理解是Function Calling 是“模型厂商提供的 API 特性”而 MCP 是“工具接入的行业协议”。Function Calling 是闭源的属于某个模型服务商的私有协议换个厂商就要重新适配MCP 是开放的Anthropic 提出后迅速被 OpenAI、Google 等多家公司接纳大家统一遵循同一套协议Function Calling 只解决“模型理解工具描述”这一环而 MCP 还把工具发现、身份认证、权限控制、跨进程传输都包括进来了。所以在实际选型时如果只是在某个单一模型 API 上做小规模实验用 Function Calling 省事但如果要搭建一个可持续扩展的 Agent 工具链MCP 是现在最优解。2. 从零搭建 MCP Server实战准备与协议细节2.1 项目初始化与 SDK 选型我先明确一下技术栈选择。当前 MCP 官方提供了 TypeScript SDK 和 Python SDK两者都很成熟。我自己的主力语言是 Python整个实操示例用它来写但你换成 TypeScript 完全没问题协议层面的逻辑是一样的。Python 这边强烈建议直接用官方维护的mcp库因为我们在实践中发现它有几个很实用的特性已经封装好了底层 JSON-RPC 处理、自带传输层适配stdio 和 SSE 都支持、工具装饰器风格非常顺手能让代码量少一半还不容易出错。# 创建项目目录 mkdir mcp-agent-toolchain cd mcp-agent-toolchain # 创建虚拟环境Python 3.10 python -m venv .venv source .venv/bin/activate # 安装 MCP SDK 与 FastAPISSE 传输需要 pip install mcp[cli]1.2.0 fastapi uvicorn这里要特别提醒一个版本问题MCP 协议本身还在快速演进SDK 版本差异可能带来 API 不兼容。在实际项目中我吃过一次亏原先在 0.9 版本下写的 server 代码升级到 1.0 之后mcp包引入路径和装饰器参数变了改代码花了不少时间。建议从项目第一天就锁定 SDK 版本记录到requirements.txt里别随手装最新版。2.2 搭一个最简单的 Server让 Agent 能用上计算器为了把流程跑通我不去写那种“Hello World”级别的空壳而是直接实现一个有实际价值的工具——四则运算计算器。虽然工具简单但它涵盖了 MCP Server 的核心要素工具定义、参数声明、执行逻辑。# server.py from mcp.server.fastmcp import FastMCP # 创建一个 MCP Server 实例 mcp FastMCP(calculator-server) mcp.tool() def calculate(expression: str) - str: 计算数学表达式例如 1 2 * 3返回计算结果。 # 安全考虑只允许数字、运算符和括号 import re if not re.fullmatch(r[0-9\-*/().\s], expression): return 表达式包含非法字符仅支持数字、四则运算符和括号 try: result eval(expression, {__builtins__: {}}, {}) return str(result) except Exception as e: return f计算出错: {str(e)} if __name__ __main__: # 以 SSE 方式运行监听 8000 端口 mcp.run(transportsse, host0.0.0.0, port8000)这段代码里有两个地方我要展开讲讲。第一FastMCP是 MCP Python SDK 提供的高层封装它把协议握手、工具注册、参数校验全都自动处理了。你只需要写一个普通函数加mcp.tool()装饰器它就能被远程发现和调用。这里我没有手写 JSON Schema因为 SDK 会自动根据函数签名生成规范。第二为什么传输方式选 SSE 而不是默认的 stdio这个选择在实际场景中很重要。stdio 模式适合 Agent 与 Server 同机的本地部署比如 Claude Desktop 调用本地的 MCP Server但如果是云端部署Agent 和 Server 在不同机器上就必须通过网络传输。HTTP SSEServer-Sent Events是最通用的方案模型客户端发起 POST 请求工具结果通过 SSE 流返回。启动这个 Server 后怎么验证它是好的我教你一个小技巧直接用 MCP 官方提供的调试客户端工具它会把服务器工具清单拉出来给你看# 新开一个终端窗口 pip install mcp[cli] mcp dev http://localhost:8000/sse如果一切正常你会看到类似输出Connected to MCP server at http://localhost:8000/sse Tools available: - calculate: 计算数学表达式例如 1 2 * 3返回计算结果。2.3 协议层发生了什么initialize、tools/list、tools/call前面那个 Server 能跑通很多人觉得“这不就是写了个函数吗”。但底层其实是 MCP 协议在干活。了解协议层的最小三个环节对后面排查线上问题至关重要。整个 MCP 交互流程本质上是一次标准的 JSON-RPC 会话。客户端连接上来的第一步是发送initialize请求告诉服务端自己支持的协议版本和客户端能力。这一步决定了两端能不能正常对话。紧接着客户端会发送tools/list请求服务端把所有注册的工具名、描述、参数 Schema 返回给客户端。你可以把这一步理解为“模型的工作说明书”它告诉模型可以帮用户做哪些事、每件事需要什么参数。等到用户在对话里提出了真正的需求模型会选择一个工具并产生参数然后客户端发送tools/call请求。服务端执行工具函数把结果以content列表的形式返回。整个链路最核心的逻辑就是这三板斧所有 MCP 工具调用都逃不开这个模型。我在实际调试中有一个心得大部分“工具调不通”的问题90% 出在 protocol 版本不兼容或者工具参数不符合 Schema 这两点上。所以遇到问题别急着看业务逻辑先用调试客户端确认 tools/list 返回是否正常再看模型生成的参数有没有通过 JSON Schema 校验。3. 把 MCP 接入 AI Agent从 Server 到完整工具链3.1 Agent 主循环的最小闭环MCP Server 搭起来了接下来就是重头戏让一个 AI Agent 能真正用它。Agent 的本质是一个循环接收用户输入 → 决定要不要调工具 → 调用工具 → 带着工具结果继续生成回复 → 循环直到任务完成。我强烈建议你不要上来就接那些复杂的 Agent 框架先自己写一个最简循环把整个流程彻底走通。这样你对 Agent 工作方式的理解会完全不一样。# agent.py import json import requests from openai import OpenAI MCP_SERVER_URL http://localhost:8000/sse def get_tools_from_mcp(): 向 MCP Server 拉取可用工具列表转换成 OpenAI 工具格式 # 这里简化处理实际开发直接用 mcp client SDK 更可靠 # 先手动定义一份与 Server 端对应的工具描述 return [{ type: function, function: { name: calculate, description: 计算数学表达式例如 1 2 * 3返回计算结果。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式 } }, required: [expression] } } }] def call_mcp_tool(tool_name, arguments): 通过 MCP 服务调用工具 # 实际工程中这里应该使用 mcp.client 完成 JSON-RPC 调用 # 为清晰展示这里模拟一次 HTTP 调用流程 payload { jsonrpc: 2.0, id: 1, method: tools/call, params: { name: tool_name, arguments: json.loads(arguments) } } response requests.post(http://localhost:8000/mcp, jsonpayload) result response.json() # MCP 返回格式: result.content[0].text try: return result[result][content][0][text] except (KeyError, IndexError): return f工具调用异常: {result} client OpenAI() # 请配置好你的 API Key def run_agent(user_input): messages [{role: user, content: user_input}] tools get_tools_from_mcp() for _ in range(5): # 最大循环次数防止无限循环 response client.chat.completions.create( modelgpt-4o, messagesmessages, toolstools, ) msg response.choices[0].message if not msg.tool_calls: # 模型不再需要调用工具直接输出最终回复 return msg.content # 将模型回复加入上下文 messages.append(msg.model_dump()) # 处理每一个工具调用 for tool_call in msg.tool_calls: tool_name tool_call.function.name tool_args tool_call.function.arguments result call_mcp_tool(tool_name, tool_args) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 达到最大循环次数任务未完成 if __name__ __main__: print(run_agent(帮我算一下 (23 45) * 2 等于多少))这个示例虽然短但完整展现了 Agent 的核心机制。有几个细节值得注意模型返回的tool_calls数组可能包含多个工具调用请求所以要用循环处理而不是取第一个每次工具调用的结果都要按role: tool并带上对应的tool_call_id回填给模型否则模型无法把结果和之前的请求对应起来最大循环次数是必须的否则遇到需要反复试错的场景会无限消耗 token。3.2 意图识别与任务拆分Agent 不只是“调 API”如果你把 Agent 理解成“模型生成参数、程序调接口”那格局小了。真正好用的工具链需要在模型前面加一层“任务规划器”。这一步在业界叫 Agent 的推理层。我用这个计算器的例子说明。用户如果直接问“(2345)*2 等于多少”模型会直接生成calculate调用。但用户如果问“我买了两件衬衫一件 23 块一件 45 块买两件一共多少钱”模型就需要先把自然语言理解成数学表达式再调用工具。这个能力不是 MCP 提供的而是模型本身的推理能力。所以搭建工具链的时候有一个重要的设计原则MCP Server 提供的工具应该是细粒度的原子能力而 Agent 负责把用户意图拆解成一系列工具调用。很多新手容易犯的错误是把工具做得太“重”比如直接写一个“处理订单全流程”的工具这会严重限制模型的任务规划能力因为模型并不知道流程里有哪些可选的中间步骤。3.3 上下文管理工具结果怎么“喂”给模型最合理工具调用完成后返回的结果需要拼接到对话上下文里。这一步看起来简单但有一个非常关键的工程问题工具返回结果太长把上下文塞爆怎么办。我在做数据库查询类工具时踩过坑。用户问了一句“今年所有订单”数据库可能返回 5000 行记录如果原样塞进上下文轻则浪费大量 token重则直接超过模型的上下文窗口导致报错。解决思路是在工具内部做结果裁剪。MCP Server 的每个工具在返回内容前应该根据使用场景决定返回的详细程度。比如查询订单列表的工具可以支持limit参数默认只返回 20 条查询具体订单详情的工具才返回全字段。更进一步可以给工具加一个“总结模式”当数据量过大时先让大模型对结果做摘要再返回摘要给主 Agent。下面是我在项目里用的一段通用结果处理逻辑效果很好mcp.tool() def search_orders(keyword: str, limit: int 20) - str: 按关键词搜索订单返回最多 limit 条结果。 orders query_db(fSELECT * FROM orders WHERE title LIKE %{keyword}% LIMIT {limit}) if len(orders) limit: return f查询到大量订单已返回前 {limit} 条如需更精确结果请添加更多过滤条件\n format_orders(orders) return format_orders(orders)4. 生产级工具链多 Server 管理、安全与调试4.1 多 MCP Server 管理从单工具到工具矩阵真实项目里一定不会只有一个 MCP Server。我目前维护的项目中线上跑了十几个 Server涵盖订单查询、库存同步、日志检索、工单创建等。当 Server 数量多起来Agent 端的工具管理就变得很有挑战。最先遇到的问题就是工具名冲突。两个不同团队写的 Server 可能都定义了get_user_info工具但参数和语义完全不一样。MCP 协议目前对工具名冲突没有做强制处理所以我在实践中定了一个内部规范工具名采用 “服务名.具体动作” 的格式比如orders.get_detail、inventory.check_stock。这样即使在 Agent 端做工具合并也不会混淆。另一个问题是 Agent 端要动态发现和加载工具。总不能每加一个 Server 就改一次 Agent 代码。我的方案是写一个工具注册中心服务启动时读取配置文件里的 Server 列表逐个发起tools/list请求把工具信息汇总后存到缓存中。Agent 每次分配工具时从注册中心拉取最新的工具清单。# mcp_servers.yaml servers: - name: calculator url: http://localhost:8000/sse enabled: true - name: orders url: http://order-server:9001/sse enabled: true - name: weather url: http://weather-server:9002/sse enabled: false4.2 安全边界不能让 Agent 乱调用MCP 解决了工具接入的统一问题但它天然也带来了新的安全风险一旦 Agent 拿到了工具调用权限谁都拦不住它乱调。尤其是模型的理解出现偏差时可能出现灾难性的误操作。我给出几条我在实战中沉淀的安全红线你可以直接用工具权限分级把工具分成只读、可写、高危三类。只读工具如查询订单Agent 可以随意调用可写工具如创建工单需要带上用户确认标记高危工具如删除数据禁止 Agent 直接调用必须先由外部审核。参数白名单校验在 MCP Server 工具函数内部不要完全相信模型生成的参数。比如计算器工具里我特意对表达式做了正则校验防止恶意代码注入。这是一个非常好的习惯。敏感信息脱敏工具返回给模型的数据中如果包含手机号、银行卡号等敏感字段一定要在 Server 层做脱敏。因为模型生成回复时可能会把工具结果原样输出造成数据泄露。调用审计日志每一个tools/call请求都要记录委托人、调用时间、参数、结果状态。出了问题能快速定位是谁在什么场景下发起的调用。我自己的一个惨痛教训之前某个项目里模型在理解用户“把所有测试订单都清掉”这句话时直接调用了删除接口幸好带了一层层 SQL 保护没酿成大祸。从那以后高危工具一律要求模型先跟用户确认得到肯定答复后才能执行。4.3 调试 MCP 连接日志、超时与重试线上运行 MCP 工具链最容易翻车的三个环节连接建立失败、工具调用超时、协议版本不匹配。我逐个说说排查思路。连接建立失败通常出现在 SSE 模式下。服务端和客户端对 SSE 的握手逻辑一不一致很关键。最常见的问题是没有正确配置跨域或者服务端绑定的地址客户端访问不到。排查时先用curl直接访问 SSE 端点看text/event-stream是否正常返回curl -N http://localhost:8000/sse如果正常你会看到持续输出的 event stream。如果这里就挂掉肯定不是 Agent 代码的问题而是 Server 或网络的问题。工具调用超时在真实场景里特别常见。大模型生成参数通常很快但工具真正执行时可能需要查数据库、调第三方接口一旦慢于模型侧的超时设置整个调用链就断了。我的经验是不要依赖默认超时要在 MCP 客户端显式设置一个比工具最慢执行时间更长的超时值。同时工具设计时要尽量把耗时操作做成异步先返回“任务已接收”再通过另一个查询接口获取结果。协议版本不匹配的坑我在 2.1 节提过。这里再补充一个细节如果 Agent 客户端和 MCP Server 的 SDK 版本差得太多握手阶段会直接失败。排查时可以看初始化请求里protocolVersion字段对比两边版本是否一致。5. 常见问题与排查技巧实录我的踩坑实操记录5.1 “连接被拒绝”类先分清是协议问题还是网络问题遇到的第一个问题是 Agent 端明明拿到了工具列表但一调用就报错 Connection refused。排查过程先 ping 了一下 MCP Server 的地址通再用 curl 请求 SSE 端点发现服务能响应。最后的根因出乎意料——Client 端用的是httpx库而服务端 FastAPI 启动时没开 CORS浏览器环境的请求和 Python 请求的处理逻辑不同导致 SSE 流被阻断。解决办法是在 FastAPI 上显式添加 CORS 中间件允许所有来源访问。这个坑提示我们生产环境部署 MCP Server 时必须提前考虑调用方的网络环境不能只在本机测试通过就上线。5.2 “工具调用成功但没有返回结果”检查 JSON-RPC 的 id 匹配有一次线上调查用户反馈“模型说查不到数据”但日志里工具明明执行成功了。后来我抓包看请求返回发现客户端发请求时给每个调用分配了自增 id但服务端返回结果时 id 字段没有正确回填导致客户端无法把响应和请求匹配直接丢弃了结果。这个问题在手动实现 MCP client 时特别容易犯。如果你直接用官方 SDK框架会帮你处理 id 匹配但如果自己封装协议一定要确保请求和响应的id严格一致这是 JSON-RPC 协议的基本要求。5.3 “上下文越权”工具描述写得太模糊导致模型乱调还有一类问题不是报错而是模型“用错了”工具。比如同时有get_order_amount查询订单金额和get_order_list查询订单列表两个工具因为描述写得模糊模型经常把本应调用列表功能的场景误判成查询金额。解决办法是给工具的 description 写清楚使用条件和边界还可以加上典型的使用示例。不要小看这段描述它是模型唯一的“使用说明书”写得越好调用准确率越高。我自己后面写工具时已经养成习惯——每个工具描述必须包含三个部分功能说明、适用场景、一个具体的输入示例。5.4 常见问题速查表问题现象可能原因解决思路连接被拒绝服务未启动/端口错误/CORS 未配置先用 curl 验证 SSE 端点再检查网络与中间件tools/list 返回空服务端没注册工具/装饰器没生效检查代码中mcp.tool()装饰器是否正确工具调用超时工具执行耗时过长/客户端超时太短设置更长的超时时间或改成异步任务模式模型乱调工具description 不清晰重写工具描述加场景说明和示例返回结果被丢弃JSON-RPC id 不匹配检查客户端 id 生成与服务端回填逻辑协议版本冲突SDK 版本差异过大统一版本锁定依赖版本号5.5 我的排查心得从日志里快速定位问题排查 MCP 工具链问题我总结出一套固定的日志分析顺序先看 Agent 的请求日志确认模型选择了哪个工具、生成了什么参数再看 MCP Server 的访问日志确认服务端收到了什么请求最后看 MCP Server 的业务日志确认工具内部有没有报错。这三步走完99% 的问题都能定位到具体环节。很多同学一上来就盯着代码看反而浪费时间。MCP 链路有明确的信息传递路径按照网络、协议、业务三层逐层排查是最快的。而且强烈建议你把 Agent 端的请求和响应日志都打开开 DEBUG 级别第一次调试时把完整 JSON-RPC 报文全部打印出来。6. 工具链扩展与未来演进我的实践方向6.1 引入 MCP Client 封装层屏蔽协议细节前面的示例里我为了展示原理用requests手动构造了tools/call请求。但在实际项目中手动处理 JSON-RPC 的握手、版本协商、流式响应非常痛苦所以生产环境一定要用官方客户端 SDK。这里给一个更真实的生产级调用写法使用mcp官方 Python 客户端的示例import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def run(): server_params StdioServerParameters( commandpython, args[server.py] ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: # 初始化连接 await session.initialize() # 获取工具列表 tools await session.list_tools() for tool in tools.tools: print(f发现工具: {tool.name} - {tool.description}) # 调用工具 result await session.call_tool( calculate, arguments{expression: (23 45) * 2} ) print(result.content) asyncio.run(run())这个封装层帮我解决了很多底层烦恼自动管理连接生命周期、自动处理初始化握手、对工具结果做了结构化处理。团队里的其他同学不需要理解 MCP 底层协议也能开发业务工具效率提升非常明显。6.2 工具链的可观测性让每次调用都有据可查Agent 工具链跑久了你会发现最难的已经不是“调通工具”而是“解释为什么这样决策”。这时候就需要给工具链加上完整的可观测性能力。我现在每个 MCP Server 都在tools/call入口打结构化日志用户 ID、会话 ID、工具名、输入参数、输出摘要、耗时、错误信息。同时利用 OpenTelemetry 对每次调用做了链路追踪。这样当业务方问“为什么这笔订单被模型标记为异常”时我能快速拉出当时的完整决策上下文知道模型看到了什么、调用了什么工具、工具返回了什么。这一步很容易被忽视但它是 Agent 应用上生产环境必不可少的基础设施。6.3 MCP 的发展趋势与我个人的学习建议说实话MCP 协议的进化速度比我想象中快很多。现在社区已经有人在讨论把资源访问Resource和提示词模板Prompt Template也纳入统一管理未来 Agent 不仅能调用工具还能通过同一个协议访问知识库、加载预设提示词。对想入坑的朋友我个人的建议是不要一上来就追框架先把前面 2.1 节到 2.3 节的协议流程亲手跑一遍理解initialize、tools/list、tools/call这三个核心环节。然后用官方 SDK 把 3.1 节那个最简 Agent 循环写出来。等这两步走完再去看 LangChain、CrewAI 这类框架的 MCP 集成你会发现一切都豁然开朗。我最后再分享一个小技巧在搭建工具链时始终给自己留一个“人肉调试入口”。比如我在 Agent 循环里加了一个环境变量开关当设置为DEBUGTrue时每次模型生成工具调用后都暂停等待我手动确认再执行。这个入口在开发和排查问题时帮了大忙甚至线上出了突发情况也能第一时间兜底。MCP 这套东西从“概念很火”到“真正好用”中间隔着的就是亲手把一个一个工具接进去、踩坑、再优化的过程。希望这篇文章能让你少踩几个弯路的坑早点把工具链跑起来。
返回列表