ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MCP协议开发实战:手写Server到Dify智能体接入

MCP协议开发实战:手写Server到Dify智能体接入 这一期“直接发”系列vol.002我们来聊一个2025年绕不开的协议——MCP协议开发实战。标题里的MCP全称Model Context Protocol模型上下文协议是Anthropic在2024年底开源、2025年迅速成为AI应用层事实标准的通信协议。简单说它解决的是“大模型怎么调用外部工具、怎么读取外部数据”这个老大难问题。这篇文章我会从零开始手写MCP Server把它接到真实业务后端再挂到Dify这类智能体平台上完成一次端到端调用顺便把生产环境里最容易踩的坑挨个讲明白。适合正在做AI应用、智能体开发或者想把现有系统开放给大模型调用的后端工程师。1. MCP协议解决的不只是“连上”而是“让模型会用工具”1.1 从一个被参数写死的Function Calling说起很多读者可能已经用过Function Calling。思路很简单定义好JSON Schema告诉模型有什么函数可以调模型在对话中决定调哪个、填什么参数然后你的后端去执行。但用久了就会发现问题——function calling的定义、格式、调用方式每家都不一样模型平台换一个就要重写一套。更麻烦的是你辛辛苦苦给这个平台写的工具定义换一个Host比如从Claude Desktop换到Dify、从Cursor换到自研应用全部作废。MCP要干掉的就是这种“每个AI应用一套私有协议”的碎片化状态。它把工具、资源、提示词这些能力抽象成一套标准协议工具提供方写一次所有支持MCP的AI应用都能用。这里借用业内一个很常见的类比AI应用是笔记本电脑工具是外设MCP就是USB-C——以前每个设备都要专用线现在一根线全解决。不管你是做Web后端、嵌入式还是客户端只要手头有一个系统需要被大模型触达就有一条MCP的接入路径。这也是为什么榜单上能看到Flask开发、Python项目开发、Dify智能体这些热词和MCP同时出现——它们组合起来正好构成一条完整的AI应用链路。1.2 Host、Client、ServerMCP里的三角色MCP架构理解起来很轻就是三个角色加各自分工Host宿主真正跑大模型的应用比如Claude Desktop、Cursor、Dify、自研Agent。它是用户提问的入口也是模型生成回复的地方。Client协议客户端Host内部与某个Server保持一对一连接的组件负责发起握手、拉取工具列表、发起调用请求。一个Host里可以同时跑多个Client每个Client连一个Server。Server工具服务端独立的工具服务进程通过MCP协议把自己的能力暴露出去。它不关心大模型是谁只负责“提供工具”和“执行工具”。这三个角色画成图很简单但理解它们的边界很重要Server不直接和模型对话Client才是真正的翻译和桥梁。因此只要协议层兼容同一个Server可以被Claude Desktop用也可以被Dify用还可以被你自己写的Python程序当普通SDK调用。1.3 Tools、Resources、Prompts三种原语在动手写代码之前必须分清MCP协议里三个最基础的能力原语否则后面看文档会一脸懵。Tools工具代表“动作”比如查订单、发消息、改配置。它有输入参数有执行结果是开发实战中最常用、最核心的能力。Resources资源代表“数据”比如一份Markdown文档、一个配置文件、一张图片的URI。通常用于把静态信息注入模型上下文让回答更有依据。Prompts提示词可复用的提示词模板。用户或模型可以按模板快速生成结构化的提示内容。我做的绝大多数MCP Server只用到Tools这一种能力。Resources在需要给模型提供大量知识背景时很好用Prompts则适合做成团队内的标准化提问模板。整套协议不复杂但边界清晰这三个原语覆盖了“让模型拿到信息、执行动作、按模板做事”全部场景。1.4 为什么有人会说“MCP就是个带标准协议的Function Calling”说实话MCP在模型层做的事情和Function Calling确实有重叠但关键区别在于Function Calling是模型平台私有的JSON Schema约定跟具体厂商绑定MCP是独立于任何模型提供方的应用层协议底层用JSON-RPC 2.0通信支持stdio和HTTP两种传输层。再加上Client-Server的解耦设计使得工具可以被任意支持MCP的Host反复接入。对后端工程师来说最直观的感受就是以前你给OpenAI写了函数定义换到别的模型还得重写现在你写一个MCP Server哪都能挂。这种“一次开发、到处接入”的特性才是它能在短时间内成为事实标准的根本原因。提示新手把MCP理解成“大模型世界的工具接口标准”就够了先跑通一个小例子再回头细看JSON-RPC消息格式比一上来就啃完整份规范效率高得多。2. 第一个MCP Server十分钟跑通最小可用服务2.1 环境准备Python 官方SDK先准备环境。我用Python 3.10配合官方MCP SDKmcp包。安装依赖就一行命令pip install mcp requests flaskrequests和flask是后面接业务后端要用的提前一起装上省事。建议在独立虚拟环境里操作避免污染全局Python环境。如果你是uv用户用uv add mcp之类的命令创建项目也可以。注意这个SDK现在的版本迭代非常快接口可能会有小差异但FastMCP这个高层封装非常稳定目前还没遇到过破坏性变更。2.2 用FastMCP写一个带业务的订单查询Server废话不多说直接上完整代码。我写一个查询订单状态的MCP Serverfrom mcp.server.fastmcp import FastMCP # 创建MCP Server实例name会显示在Host的工具列表中 mcp FastMCP(order-server) mcp.tool() def query_order(order_id: str) - str: 根据订单号查询订单的当前处理状态与最新物流轨迹。 仅当用户明确提供了订单号时调用如果用户没给订单号先向用户索要。 # 这里先返回模拟数据下一节接真实业务后端 return ( f订单 {order_id} 当前状态已发货 f最新轨迹2025-06-01 14:30 包裹已到达上海转运中心。 ) if __name__ __main__: mcp.run()这段代码就是完整的Server。mcp.tool()把函数声明为一个MCP Tool函数的类型注解会被转换成JSON Schemadocstring会成为工具描述。运行方式也很简单python order_server.py启动后你会看到进程一直挂着等待Host发起连接。现在的Server还没有任何界面和日志输出所以要验证它得借助调试工具。2.3 用MCP Inspector做第一次联调怎么验证Server真的能用最常用也最推荐的是官方MCP Inspector。在项目目录下运行npx modelcontextprotocol/inspector python order_server.py启动后会打开一个可视化调试页面。右上角点Connect可以看到三步自动完成Initialize协议握手、Tools/List拉取工具列表、Tools/Call手动调用工具。输入order_id点击Call立刻能看到返回结果。Inspector是MCP开发里最重要的调试工具没有之一。它让你不依赖任何Host直接以协议Client的身份连接你的Server快速确认协议层没问题。后面排查“模型不调用工具”“工具报错”这类问题几乎全靠它。2.4 握手和调用背后的协议消息长什么样用Inspector的最大好处是能看到MCP的底层消息。整个流程本质是JSON-RPC 2.0拆开来看一共三条核心消息第一次请求是initializeClient发送协议版本和自身能力声明Server回应自己的协议版本、能力支持并声明服务名叫order-server。握手完成后Client发tools/list拉取工具列表返回里包含工具的name、description、inputSchema即JSON Schema。最后当模型决定调用时Client发出tools/callparams里带工具名和参数JSONServer执行后返回结果。我在调试时经常看到这样的消息{jsonrpc: 2.0, id: 3, method: tools/call, params: {name: query_order, arguments: {order_id: SO20250601001}}}整个过程中FastMCP帮你完成函数到工具的映射、类型校验、结果序列化。如果你直接手写协议还得考虑请求ID管理、错误码、协议版本协商、Capabilities声明等一堆细节所以日常开发无脑用官方SDK就好。3. 让它真正干活从Flask业务后端到Dify智能体3.1 为什么MCP Server旁边还要一个Flask后端有读者会问刚才的查询订单不是已经返回数据了吗干嘛还要Flask因为在真实项目里MCP Server只是协议翻译层它本身不应该直接访问数据库、内网系统或调用第三方服务。如果工具直接怼数据库连接后续改表结构、加权限、做审计都会非常痛苦。更常见的架构是MCP Server通过HTTP调用你们已有的业务API把外部能力包装成AI能理解的工具。这样的好处显而易见——业务系统不用因为引入AI做任何改动MCP Server只是一个适配器。所以这里我用Flask写一个最小的订单查询REST API作为“已有业务系统”的替身。等你要接真实系统时把下面代码里的requests.get指向真实的接口地址、补上鉴权header即可。3.2 Flask提供REST接口MCP负责协议翻译先写Flask后端from flask import Flask, jsonify app Flask(__name__) ORDERS { SO20250601001: {status: 已发货, logistics: 已到达上海转运中心}, SO20250601002: {status: 已签收, logistics: 2025-05-30 签收人前台}, } app.get(/api/order/order_id) def get_order(order_id): order ORDERS.get(order_id) if not order: return jsonify({success: False, message: 订单不存在}), 404 return jsonify({success: True, data: order}) if __name__ __main__: app.run(host0.0.0.0, port5001)然后把上一节的MCP Server改一下把模拟数据换成调用Flask接口import requests from mcp.server.fastmcp import FastMCP mcp FastMCP(order-server) BUSINESS_API http://127.0.0.1:5001 mcp.tool() def query_order(order_id: str) - str: 根据订单号查询订单的当前处理状态与最新物流轨迹。 仅当用户明确提供了订单号时调用如果用户没给订单号先向用户索要。 返回结构化文本包含订单状态和物流节点。 try: resp requests.get(f{BUSINESS_API}/api/order/{order_id}, timeout10) data resp.json() except requests.RequestException as e: return f查询订单失败原因{e}请稍后重试。 except ValueError: return 业务系统返回了无法解析的数据请稍后重试。 if not data.get(success): return f查询失败{data.get(message, 未知错误)} order data[data] return f订单 {order_id} 当前状态{order[status]}最新轨迹{order[logistics]}。 if __name__ __main__: mcp.run()这里有几个关键点工具函数里做HTTP调用必须设置超时比如timeout10否则MCP Client那边等太久会先放弃。返回给模型的永远是“整理好的文本”不要直接把整个JSON原样塞回去。模型要的是能直接二次组织语言的摘要信息。业务接口本身是RESTMCP Server做的是协议转换加信息裁剪。这个薄层保持得越薄越好。3.3 把MCP Server挂到Dify智能体再来看怎么接入Dify。Dify是目前很主流的智能体/AI应用开发平台相关热词里也总出现“dify ai智能体开发实战”它在工具接入层面原生支持MCP协议。步骤大概是这样创建Agent应用进入编排页面。在工具列表选择“自定义”或“MCP”入口新增MCP服务器。填写服务器名称、协议类型。本地开发期我用stdio填启动命令python order_server.py工作目录指向项目根目录。服务器连接成功后Dify会自动列出这个Server上报的工具列表也就是上面的query_order。在Agent编排里启用该工具保存后在对话测试里输入“帮我查一下SO20250601001这个订单”模型就会在需要时调用工具并组织答案。注意不同Dify版本的UI文案不一样有的叫“MCP服务器”有的在“工具”里直接选MCP类型但逻辑都一样填Server地址或命令拿到工具列表启用工具。3.4 一次完整调用链路全复盘我以本地Dify Flask为例把一次调用完整过一遍用户问“SO20250601002到哪了”→ Dify编排的模型根据query_order的description判断应该调用它于是Dify的MCP Client发tools/call指令 → order-server收到后调用Flask的GET /api/order/SO20250601002→ Flask查到“已签收”数据并返回JSON → order-server把JSON整理成一句话文本 → 返回给Dify → 大模型基于这句文本组织自然语言回复给用户。整个过程里大模型不知道订单数据存在哪Flask也不知道大模型存在两边通过MCP Server这个翻译层解耦。这就是MCP在真实产品里最典型的落位协议层很薄但它让“AI能力”和“业务系统”可以独立演进互不绑架。这个架构里的任何一环都可以替换后台业务可以是Java、Go或Node写的MCP Server也可以用TypeScript写官方TS SDK同样成熟。只要协议一致接什么都行。Flask只是做原型和中小项目时最顺手的选型因为Python生态里起一个REST服务实在太快了。4. 生产环境绕不开的坎传输方式、鉴权和并发4.1 stdio还是Streamable HTTP别等上线前才纠结本地调试和Dify接入可以玩stdio但一旦Server要部署到远程服务器、供多个Host连接就必须用HTTP。MCP的HTTP传输方式经历了从HTTPSSE到Streamable HTTP的演进目前主流SDK默认推荐后者。简单说原来Client和Server通过SSE长连接双向通信后来改成基于HTTP POST加流式响应的方式部署、调试、过代理都更友好。FastMCP里切换到HTTP很直接if __name__ __main__: mcp.run(transportstreamable-http)不同SDK小版本的启动方式略有差异有些版本还需要配合ASGI服务器来跑。我的建议是本地开发用stdio看日志最舒服测试环境开始就换成Streamable HTTP提前暴露网络、鉴权、超时问题。下面把两种方式放在一起对比对比项stdioStreamable HTTP通信载体标准输入输出HTTP POST 流式响应适用场景本地调试、单机部署远程服务、多Host接入是否跨机器否是鉴权方式基本不用API Key / OAuth调试成本低直接看日志中需要抓HTTP请求并发模型单进程较简单依赖Web服务并发能力4.2 鉴权开发和上线用的是两套方案MCP规范里推荐OAuth 2.1但自部署场景完全可以用更简单的API Key方案。在Dify配置MCP服务器时如果Server要求鉴权Header你可以在配置里填上对应的Authorization头自己的Client请求里同样带上。我在实际项目中用一个中间件检查请求头没有正确Key就返回401。Key的生成、轮换、吊销按公司已有的密钥管理体系来不要把Secret硬编码在代码里。还有个容易被忽略的点日志里不要打印出完整Header否则钥匙就漏了。4.3 并发一个Server被多个Agent同时调用当多个Host同时连接你的MCP Server时并发问题就会浮出水面。FastMCP能不能扛住高并发取决于底层ASGI服务器的并发能力和工具本身是否幂等。这里有两个建议第一工具实现里不要用全局可变状态记录调用结果尤其不要用普通全局变量存session多线程下很容易踩数据错乱。第二不要在工具函数里启动长任务如果工具本身要5分钟才完成模型早就超时放弃了。正确的做法是先用工具返回“任务已提交任务ID为xxx”再提供另一个工具按任务ID查询结果。这样既避免了超时又让用户能拿到异步任务的进度。4.4 工具返回数据的“上下文预算”陷阱容易被忽视的是工具返回的数据最终会拼进模型的上下文里。你让工具返回一份几十KB的JSON模型可能根本看不完还会占用大量上下文窗口。所以工具输出要做三件事裁剪字段、尽量文本化、必要时分页。这个坑我在日志查询工具上真实踩过。最初工具把原始日志数组直接返回给模型模型经常漏看关键行。改成摘要格式后只返回错误等级、时间范围、错误码出现次数模型分析准确率明显提升。记住一个原则工具返回的不是给程序看的数据而是给模型看的证据。5. 从“能跑”到“好用”调试手段与排错清单5.1 善用MCP Inspector和日志双重定位我调试MCP Server的惯用套路是双通道先开MCP Inspector看协议层消息再看应用日志定位业务问题。Inspector里能看到每次initialize、tools/list、tools/call的请求和响应耗时能快速确认问题是出在“模型没调用工具”还是“工具调用了但报错”。业务层日志记录每次工具入参、HTTP调用耗时、返回内容摘要。两层一对照问题基本就锁定了。这里有个小技巧给每次调用加一个request_id追踪从Host发起一直记到业务API返回排查链路问题会省非常多的时间。5.2 高频踩坑工具描述写太差模型不知道怎么用工具调用的第一步不是代码而是让模型知道“什么时候该调这个工具、参数填什么、返回什么意思”。描述写得太短比如只写“查询订单”模型会在订单号缺失时也盲目去调然后拿到失败结果或者在不需要查询时调用了工具浪费一次完整往返。我的写法是描述里写清触发条件、前置要求、返回结构。比如根据订单号查询订单的当前处理状态与最新物流轨迹。 仅当用户明确提供了订单号时调用如果用户没给订单号先向用户索要。 返回结构化文本包含订单状态和物流节点。一段描述可能决定一次调用是否成功。生产环境里模型调用一次工具需要几秒甚至更久描述含糊造成的误调用是隐性成本值得花时间去打磨。5.3 高频踩坑JSON Schema与类型注解的错位有段时间我写了一个工具参数类型是Optional[str]结果生成的JSON Schema变成了一堆嵌套结构某些Host解析后参数直接没传进来。后来我规范了所有工具签名参数尽量用基础类型默认值写清楚需要复杂结构就用dict接收再内部解析。记住你写的Python类型注解最终会被翻译成JSON SchemaHost端模型是根据这个Schema生成参数的Schema越清晰模型猜得越准。这一点在工具数量超过五个之后体现得尤其明显——参数模糊的工具往往成了调用失败率最高的地方。5.4 异常处理的正确姿势把错误变成结构化返回工具里最忌讳的是直接抛异常。模型调用工具抛异常Host往往只会给用户一句“工具调用失败”。而业务上你可能希望模型能根据错误类型给出不同对策比如“订单不存在”和“系统繁忙”是两种完全不同的回复。所以我习惯在每个工具内部做try-except把业务错误、网络错误、解析错误全部转成结构化文本返回给模型。还要注意一点返回错误信息时不要把数据库连接串、内网IP这些敏感信息带出去。模型只是传话的不该知道底层的敏感细节。5.5 从“能跑”到“好用”的三个日常习惯最后分享三个我在项目里养成的习惯。第一每个工具的返回都带明确的成功/失败标志结构尽量统一模型容易理解。第二HTTP调用的超时时间统一配置不要散落在代码各处超时后快速失败并返回可读信息不要让模型干等。第三给Server加一个ping或health工具Host和运维都能快速确认服务存活。这些习惯看起来不是MCP协议层面的东西但真实项目里模型调用质量往往就是被这些细节决定的。工具数量少的时候看不出差别工具一多接口风格不统一带来的混乱就会成倍放大。这次直接发002先聊到这儿。回头看整个过程我个人最大的感受是MCP协议本身并不复杂复杂的是你如何设计一个让模型愿意用、用得对的工具层。真正上线之前不要急着堆工具数量先把两三个核心工具画清楚边界、写好描述、处理好异常效果比挂二十个粗糙工具好得多。如果你们团队也在做智能体或者工具接入欢迎把遇到的奇怪问题发出来我后面可以继续用实际案例拆解。
返回列表