ARTICLE DETAIL

资讯详情

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

MCP协议实战:从零构建AI工具调用Server

MCP协议实战:从零构建AI工具调用Server 1. 先搞清楚MCP到底是个什么东西1.1 一句话说透MCP的本质MCP全称Model Context Protocol翻译过来叫“模型上下文协议”。名字听着挺唬人但你把它理解成一个标准化的插头就对了。以前每个AI应用想接一个外部工具都得自己写一套对接代码——你接数据库写一套接文件系统写一套接某个SaaS服务再写一套。MCP做的事情就是定义一套统一的接口规范让所有工具提供方按照这个规范暴露能力所有AI应用按照这个规范去调用能力。两边都遵守同一个协议就不用再一对一地写胶水代码了。我刚开始接触MCP的时候第一反应是“这不就是个API网关吗”。用了一段时间之后发现它比API网关多了一层很重要的东西上下文管理。传统API调用是你传参数、它返结果一次性的。MCP的设计里Server可以主动向Client声明自己有哪些资源Resources、哪些工具Tools、哪些提示模板PromptsClient端的大模型可以根据当前对话的上下文自主决定要不要调用、调用哪个。这就从“人找工具”变成了“模型自己找工具”。1.2 MCP解决的核心痛点没有MCP之前一个LLM应用要接入外部能力大概要经历这些破事每个工具单独写适配层参数格式、鉴权方式、错误处理全都不一样工具更新了接口应用端得跟着改想换一个同类工具几乎等于重写对接逻辑多个工具之间的上下文无法共享模型看不到全局MCP把这些统一了。你只要实现一次MCP Client理论上就能对接所有遵守MCP协议的Server。反过来你写一个MCP Server所有支持MCP的客户端都能直接用。这个思路跟当年USB统一接口是一样的——不是技术有多难而是统一标准带来的生态效应。1.3 谁适合看这篇内容如果你属于以下几类人这篇内容会对你有直接帮助正在做AI应用开发需要让模型调用外部工具或数据的开发者想把自己内部系统暴露给AI助手使用的后端工程师对Agent开发感兴趣想理解工具调用底层机制的技术人已经在用某些支持MCP的客户端想自己写Server扩展能力的人不需要你之前接触过MCP但需要你对HTTP、JSON-RPC、基本的客户端-服务端模型有概念。如果这些也不熟建议先补一下网络通信的基础知识不然看协议细节会比较吃力。2. MCP的架构设计与核心概念拆解2.1 三个角色Host、Client、ServerMCP的架构里定义了三个核心角色很多人一开始会搞混我用一个生活场景来解释想象你去餐厅吃饭。Host就是这家餐厅它负责整个用餐体验Client是服务员专门负责跟你这桌客人对接Server是后厨真正干活出菜的地方。你用户跟餐厅Host交互服务员Client把你的需求传给后厨Server后厨做完菜再由服务员端回来。对应到技术层面角色职责典型例子Host管理多个Client协调整体交互流程一个AI编程助手应用Client与单个Server建立一对一连接转发请求和响应Host内部为每个Server创建的连接实例Server提供具体的工具、资源、提示模板文件系统Server、数据库Server关键点在于一个Host可以管理多个Client每个Client对应一个Server。这样设计的好处是隔离性——一个Server挂了不会影响其他Server的连接。2.2 三种核心能力原语MCP Server对外暴露的能力分为三类这个分类很重要决定了你在什么场景下用什么Resources资源可以理解为“只读数据”。比如文件内容、数据库查询结果、API返回的JSON。Client可以读取这些资源把它们作为上下文喂给模型。Resources的特点是由应用控制——什么时候读、读哪个通常是Host端决定的。Tools工具这是最核心的能力。Tool是由模型控制的——模型根据当前对话上下文自主决定要不要调用某个工具、传什么参数。比如一个“查询天气”的Tool模型判断用户问的是天气相关的问题就会主动发起调用。Prompts提示模板预定义的提示词模板可以带参数。这个能力用得相对少一些主要用于标准化某些常见任务的输入格式。注意Resources和Tools的核心区别在于“谁来决定调用”。Resources是应用逻辑决定Tools是模型自主决定。搞混这两个会导致设计出来的Server不符合使用预期。2.3 通信层JSON-RPC 2.0MCP的通信基于JSON-RPC 2.0这是一个非常成熟的远程调用协议。为什么选它而不是REST因为JSON-RPC天然支持双向通信、通知机制、批量请求而且格式足够简单。一个典型的MCP请求长这样{ jsonrpc: 2.0, id: 1, method: tools/call, params: { name: query_database, arguments: { sql: SELECT * FROM users LIMIT 10 } } }响应{ jsonrpc: 2.0, id: 1, result: { content: [ { type: text, text: 查询结果共10条记录... } ] } }传输层支持两种方式stdio标准输入输出和HTTP with SSEServer-Sent Events。stdio适合本地进程间通信HTTPSSE适合远程Server。选哪种取决于你的部署场景本地工具用stdio就够了远程服务用HTTPSSE。2.4 为什么不用现成的Function Calling很多人会问OpenAI的Function Calling不是已经能做工具调用了吗为什么还要搞MCP这个问题我当初也纠结过。核心区别在于Function Calling是模型层面的能力它定义了模型如何表达“我要调用某个函数”这个意图。但函数的具体实现、参数校验、错误处理、连接管理这些Function Calling都不管。MCP补的就是这一层——它定义了工具提供方和工具使用方之间的完整交互规范。打个比方Function Calling像是你说了一句话“帮我查一下明天天气”MCP则是确保这句话能被正确传达、执行、返回结果的整套通信系统。两者不是替代关系是互补关系。3. 动手写一个MCP Server从零到跑通3.1 环境准备与依赖安装我用Python来演示因为官方SDK对Python的支持比较完善。先确认你的Python版本在3.10以上然后安装MCP的Python SDKpip install mcp如果你用的是Node.js对应的包是modelcontextprotocol/sdk安装方式npm install modelcontextprotocol/sdk我建议刚开始用Python因为调试起来更直观print大法随时能用。Node.js版本适合最终部署到生产环境类型系统更严格。3.2 最小可运行Server的完整代码下面是一个完整的MCP Server示例提供两个Tool一个做加法计算一个查询当前时间。from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import asyncio from datetime import datetime # 创建Server实例 app Server(demo-server) # 声明Server提供哪些工具 app.list_tools() async def list_tools(): return [ Tool( nameadd_numbers, description计算两个数字的和, inputSchema{ type: object, properties: { a: {type: number, description: 第一个数字}, b: {type: number, description: 第二个数字} }, required: [a, b] } ), Tool( nameget_current_time, description获取当前系统时间, inputSchema{ type: object, properties: {}, required: [] } ) ] # 实现工具的具体逻辑 app.call_tool() async def call_tool(name: str, arguments: dict): if name add_numbers: result arguments[a] arguments[b] return [TextContent(typetext, textf计算结果是{result})] elif name get_current_time: now datetime.now().strftime(%Y-%m-%d %H:%M:%S) return [TextContent(typetext, textf当前时间是{now})] else: return [TextContent(typetext, textf未知工具{name})] # 启动Server async def main(): async with stdio_server() as (read_stream, write_stream): await app.run(read_stream, write_stream, app.create_initialization_options()) if __name__ __main__: asyncio.run(main())这段代码可以直接跑。保存为server.py然后python server.py启动。启动后它会等待stdin的输入因为用的是stdio传输。3.3 工具声明的关键细节inputSchema用的是JSON Schema格式这个声明非常重要——模型会根据这个schema来决定怎么传参数。几个容易踩坑的地方required字段必须写不写的话模型可能不传某些参数导致你的代码KeyErrordescription要写清楚模型靠这个描述来判断什么时候该调用这个工具。描述写得太模糊模型可能该调的时候不调不该调的时候乱调参数类型要准确写type: number但实际传了字符串SDK层可能不报错但你的业务逻辑会炸我踩过的一个坑工具描述写的是“查询用户信息”结果模型在用户问“帮我看看订单”的时候也调了这个工具。后来把描述改成“根据用户ID查询用户的基本信息包括姓名、邮箱、注册时间不包含订单数据”准确率立刻上来了。3.4 调试MCP Server的实用方法stdio模式下调试不太方便因为stdout被协议通信占用了你print的东西会混进协议数据里导致解析失败。我的做法是用sys.stderr输出调试信息stderr不会被协议占用或者写一个测试脚本直接调用call_tool函数绕过协议层用MCP Inspector这个官方工具可以可视化地测试Server的每个工具import sys print(调试信息, filesys.stderr) # 这样不会干扰协议通信提示如果你在stdio模式下发现Client端报“解析错误”第一件事就是检查代码里有没有不小心用print往stdout写东西。4. 把MCP Server接入实际应用4.1 在支持MCP的客户端中配置Server现在很多AI编程工具和助手应用都支持MCP了。配置方式通常是在一个JSON配置文件里声明Server的启动命令。以常见的配置格式为例{ mcpServers: { demo-server: { command: python, args: [/path/to/server.py], env: {} } } }配置好之后重启客户端它就会自动启动这个Server进程并通过stdio建立连接。你可以在对话中直接让模型使用这些工具比如问“帮我算一下123加456等于多少”模型会自动调用add_numbers工具。4.2 写一个对接内部系统的MCP Server实际工作中最有价值的场景是把自己公司的内部系统通过MCP暴露给AI助手。假设你有一个内部的知识库API想让它能被AI直接查询import httpx app.list_tools() async def list_tools(): return [ Tool( namesearch_knowledge_base, description搜索内部知识库输入关键词返回相关文档摘要, inputSchema{ type: object, properties: { keyword: { type: string, description: 搜索关键词 }, limit: { type: integer, description: 返回结果数量默认5, default: 5 } }, required: [keyword] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name search_knowledge_base: keyword arguments[keyword] limit arguments.get(limit, 5) async with httpx.AsyncClient() as client: resp await client.get( https://internal-api.example.com/search, params{q: keyword, size: limit}, headers{Authorization: Bearer YOUR_TOKEN} ) data resp.json() results \n.join([f- {item[title]}: {item[summary]} for item in data[items]]) return [TextContent(typetext, textf找到以下相关内容\n{results})]这个模式可以套用到任何内部系统上——CRM、工单系统、监控平台只要它有API就能通过MCP暴露给AI。4.3 鉴权信息的安全处理这里有一个很重要的安全问题不要把密钥硬编码在代码里。我见过有人在MCP Server里直接写数据库密码然后提交到了公开仓库这是非常危险的。正确的做法是通过环境变量传入import os API_TOKEN os.environ.get(KB_API_TOKEN) if not API_TOKEN: raise ValueError(缺少环境变量 KB_API_TOKEN)然后在客户端的配置文件里通过env字段传入{ mcpServers: { kb-server: { command: python, args: [/path/to/kb_server.py], env: { KB_API_TOKEN: 实际token值 } } } }注意配置文件本身也要加入.gitignore避免token被提交到版本控制。更安全的做法是使用系统级的密钥管理服务配置文件里只放引用路径。4.4 处理大结果集的分页与截断MCP Tool返回的内容会直接进入模型的上下文窗口。如果你查询数据库返回了1000条记录全部塞进去会直接把上下文撑爆。我的处理策略是Tool层面做默认限制比如最多返回20条返回结果里带上总数和分页信息如果结果太长做摘要截断只返回关键字段MAX_RESULT_LENGTH 4000 def truncate_result(text: str) - str: if len(text) MAX_RESULT_LENGTH: return text return text[:MAX_RESULT_LENGTH] f\n...(结果已截断共{len(text)}字符)这个截断逻辑看起来简单但能避免很多“模型突然变傻”的问题——上下文被无关数据占满了模型自然就没法好好回答你的问题了。5. 实际使用中遇到的坑与排查思路5.1 常见问题速查表问题现象可能原因排查方向Client启动后报连接失败Server进程启动即崩溃手动运行Server命令看stderr输出工具列表为空list_tools未正确注册检查装饰器是否正确使用模型不调用工具工具描述不清晰优化description增加使用场景说明调用工具报参数错误inputSchema定义与实际不符对比schema和实际接收到的arguments返回结果模型看不懂返回格式太原始结构化返回内容加字段说明stdio模式解析错误stdout被污染检查是否有print输出到stdout5.2 工具描述写不好模型就不会用这是最常见也最容易被忽视的问题。工具能不能被正确调用80%取决于description写得好不好。我总结了一个描述模板[做什么] [什么时候用] [返回什么] [有什么限制]举个例子对比一下差的描述“查询订单”好的描述“根据订单号查询订单的详细状态包括支付状态、物流状态、预计送达时间。当用户询问某个具体订单的进展时使用。需要提供完整的订单号不支持模糊查询。”后者明显能让模型更准确地判断调用时机。5.3 错误处理不能省MCP Tool执行失败时如果你直接抛异常Client端可能收到一个不太友好的错误信息。更好的做法是捕获异常返回结构化的错误说明app.call_tool() async def call_tool(name: str, arguments: dict): try: # 业务逻辑 result do_something(arguments) return [TextContent(typetext, textresult)] except KeyError as e: return [TextContent(typetext, textf缺少必要参数{e})] except Exception as e: return [TextContent(typetext, textf执行出错{str(e)}请检查输入参数是否正确)]这样模型收到错误信息后可以自己判断是不是要换个方式重试或者告诉用户哪里出了问题。5.4 性能方面的注意事项MCP Server是常驻进程所有Tool调用都在这个进程里执行。如果你的某个Tool执行时间很长比如超过30秒会阻塞其他请求。解决方案耗时操作放到线程池或异步任务里设置合理的超时时间对于特别耗时的操作考虑返回一个任务ID让模型后续再查询结果我在一个项目里遇到过数据库查询偶尔要十几秒的情况后来加了索引和查询缓存降到毫秒级。MCP Tool的响应时间直接影响用户体验因为模型在等待Tool返回期间是卡住的状态。6. 从能用走向好用进阶实践建议6.1 工具粒度怎么把握工具拆得太细模型要调好几次才能完成一个任务拆得太粗参数复杂模型容易传错。我的经验是一个Tool对应一个完整的原子操作。比如“用户管理”这个场景不要拆成“查用户ID”“查用户姓名”“查用户邮箱”三个Tool也不要合成一个“管理用户”的万能Tool。合理的拆分是“根据条件查询用户列表”“根据ID获取用户详情”“更新用户信息”这样三个。6.2 善用Resources做上下文预加载有些数据是模型每次对话都可能需要的比如系统配置、术语表、常用联系人列表。这些不适合做成Tool让模型每次去调更适合做成Resource由Host端在初始化时加载一次后续对话直接引用。Resources的另一个好处是不消耗模型的决策成本——模型不需要判断“我要不要读这个资源”Host端直接把它放进上下文就行了。6.3 日志与可观测性生产环境用的MCP Server一定要加日志。记录每次Tool调用的入参、出参、耗时、是否成功。这些数据对于排查问题和优化工具有极大帮助。import logging import time logger logging.getLogger(mcp-server) app.call_tool() async def call_tool(name: str, arguments: dict): start time.time() try: result await execute_tool(name, arguments) elapsed time.time() - start logger.info(fTool{name} args{arguments} elapsed{elapsed:.3f}s statusok) return result except Exception as e: elapsed time.time() - start logger.error(fTool{name} args{arguments} elapsed{elapsed:.3f}s statuserror error{e}) raise日志写到文件里不要写到stdout。用Python的logging模块配置FileHandler就行。6.4 版本兼容与协议演进MCP协议本身还在演进中不同版本的SDK可能有细微差异。我的建议是锁定SDK版本不要用latest升级前先在测试环境验证所有Tool的正常性关注协议变更日志特别是破坏性变更实际项目中我一般会在Server启动时打印SDK版本和协议版本方便排查兼容性问题。6.5 什么时候该放弃MCP标题里说“从精通到放弃”虽然是调侃但确实有些场景不适合用MCP工具数量极少且固定直接写Function Calling更简单对延迟极度敏感的场景MCP多了一层进程通信开销团队完全没有异步编程经验维护成本可能超过收益技术选型永远要看具体场景MCP不是银弹。它解决的是“多工具、多客户端、需要标准化”的问题如果你的场景里这个问题不存在那就不需要它。我在实际项目中的体会是MCP最大的价值不在于技术本身有多先进而在于它让工具提供方和使用方解耦了。以前你写一个工具要针对每个AI应用写适配现在写一个MCP Server所有支持MCP的客户端都能用。这个生态效应才是它真正值得投入时间学习的原因。
返回列表