1. 先搞清楚MCP到底在解决什么问题再决定要不要学如果你最近在折腾AI智能体Agent就一定绕不开MCP这个词。我最早接触它是在一次给Agent接入内部服务查询能力的调研里团队当时已经重复写了三套工具封装一套HTTP、一套WebSocket、还有一套直接让模型拼SQL每次接到新接入需求都得从零开始搞鉴权、搞错误码、搞参数格式极其崩溃。后来看到MCPModel Context Protocol模型上下文协议的文档才发现这就是在解决模型要调用工具但不知道该怎么调用这个老问题的标准化方案。简单说MCP是一个开放协议作用是给AI模型提供一套统一的插头接口。模型可以通过它读取外部能力清单、调用具体的函数、拿回结构化结果整个过程对模型来说都是发现式的模型先看到你有什么工具再看每个工具的输入输出长什么样最后决定怎么调用。对服务端开发者来说只需要按协议实现一次就能被各种支持MCP的AI客户端即插即用。这篇文章我想按自己的落地过程来写从协议本身的机制、到手写一个服务端、再讲怎么接进宿主、最后讲我在生产环境里踩过的坑。适合两类人看一是打算给业务系统接入AI能力但不知道从哪下手的后端/算法工程师二是想深入理解Agent工具调用原理的产品或技术负责人。不用把所有代码抄下来重点看它的链路设计和取舍逻辑。1.1 没有MCP之前Agent接入工具的混乱状态我见过太多项目把给模型加工具做成一件别扭的事。最常见的是直接构造一个函数表把所有可用能力的描述塞进System Prompt模型自己拼JSON去调用。这个方案在小规模演示里能跑但一旦能力多起来就会出问题。第一个痛点是格式不统一。十个人的团队能写出十种函数描述方式有的用英文动词短语有的用中文长句参数说明更是随意模型经常在调用时传错字段。第二个痛点是认证逻辑无处安放。函数A要API Key函数B要走内部单点登录函数C直接读数据库每次调用都要在工具里重新写一遍认证分支测试和审计都很难受。第三个痛点更隐蔽——模型工具的发现能力被限制死了。模型只知道你塞给它的那堆函数如果你临时加了一个新API但没更新Prompt模型这一轮对话里永远不会知道它的存在。不是没有人想过做统一有人搞过OpenAPI插件有人搞过函数注册中心但很多方案是中心化的工具能力要统一注册到一个平台上再通过网关暴露。这在大型组织里落地阻力很大因为每个系统的数据可能都不愿意出网。MCP选了另一条路它不是中心化的平台而是一个协议让数据和工具长在各自的系统里AI客户端直接通过协议去访问。这样每个系统只需要实现一次协议接口就能被任意支持方复用。1.2 MCP的核心思路把工具能力驱动程序化MCP最妙的地方是把工具能力做成了类似操作系统中设备驱动的概念。回想一下操作系统不需要知道打印机内部结构打印机厂商只要提供一个符合USB接口协议的驱动系统就能即插即用。MCP也是这个思路只是把USB接口换成了JSON-RPC消息把打印机驱动换成了你的工具服务器。在MCP里有两个关键实体一个是MCP Server可以理解为一个提供单一能力的服务进程比如查天气的能力、读GitHub仓库的能力另一个是MCP Client它是嵌入在宿主里的连接器宿主也就是实际跟你的Agent模型对话的软件。整个过程里还有一层叫Host也就是宿主程序本身它负责给模型提供上下文、展示结果、管理对话。我画过一次三者关系用文字描述一下就是用户问模型帮我查一下这个航班状态模型所在的宿主读到了Server提供的工具列表觉得查航班这个可以用于是让Client向Server发请求Server去航空公司接口取数据返回结构化结果Host把结果整理后回给模型模型再组织成自然语言答复。这里最关键的认知是模型不直接连接任何外部API它只和Protocol打交道。1.3 一次请求的完整旅程跟着一个真实场景走一遍为了把链路说透我拿一个查询国际航班实时状态的场景做过端到端调试。这个场景特别适合理解MCP因为它既涉及外部API调用又有参数鉴权、结果格式化和超时处理。当用户对模型说帮我查一下下周一北京飞东京的航班重点看有没有延误宿主真正做的是先通过Client向Server发起tools/list请求拿到系统里全部工具的描述模型根据描述在内部推理里选中查询航班状态这个函数随后Client再发一个tools/call请求传参包括航班号或出发到达机场与时间。Server收到请求后解析参数、调航司数据接口、把结果整理成JSON返回Client把结果封装成一个工具响应丢回给模型模型基于这个结果继续生成回复。这个过程里有一个容易被忽略的细节模型其实看到了两次信息一次是工具描述input schema一次是工具返回值。这两个信息都伴随着一定的上下文长度开销所以生产上设计工具schema时描述不用过长返回结果也要尽量精简。如果你发现模型总是选错工具或者乱传参数第一时间要检查的不是模型的指令提示词而是你的工具描述是否足够精确、返回值是否足够结构化。这部分后面我会专门讲。2. 协议核心机制拆解JSON-RPC、能力协商与三原语MCP协议层并没有发明一套新的魔法它核心是建立在JSON-RPC 2.0之上。要理解MCP先理解它为什么选这个基础再理解它的生命周期设计。2.1 为什么选JSON-RPC 2.0简洁性带来的生态红利JSON-RPC 2.0是一个非常轻量的远程调用协议请求就是一个带method、params、id的JSON对象响应就是带result或error的JSON对象没有HTTP那样一堆报文头部的开销也没有消息队列那样的复杂路由逻辑。MCP选中它我理解有两大原因。第一它足够通用且无状态。没有状态让Server端实现变得简单不用维护会话对象也没那么多依赖用任何语言写服务端核心逻辑都是几十行的事。第二它的消息格式已经自然映射到JSON模型和程序都能轻易解析。一个直观的对比是如果你用RESTful接口去做Agent工具接入需要为每个工具定义一套独立的路由、错误码和鉴权方式而JSON-RPC把所有能力统一成了一个方法名一段参数这让工具列表的自动发现变得极其自然。这里要顺带提一句MCP底层传输并不强制使用HTTP本地调试时常用stdio标准输入输出通道远程部署才用SSE或流式HTTP。所以不要一听协议就以为必须起个Web服务很多场景下它就是一个子进程通过管道跟宿主对话这反倒解决了本地工具必须走网络的很多安全问题。2.2 初始化握手与能力协商协议里最像开机自检的环节MCP里所有会话都始于一次握手handshake。Client连上Server之后首先发一条initialize请求携带Client的协议版本、自身能力声明和实现信息。Server收到后返回自己的协议版本、能力集和实现信息。这个阶段最关键的两个字段是mcpVersion和capabilities。我当时第一次读规范时对capabilities理解得不够细后来调试过程中才体会到它到底在协调什么。比如Client声明自己支持resources.subscribe它表示我不仅能读取资源还能订阅资源变更通知Server收到后就可能开放这一能力。如果Client没有声明tools相关字段Server在后续连接里就不响应tools/list请求或者只在协议里报错。这就是一种能力协商类似两个人见面先确认你会英语还是只会中文再决定后面用什么语言交流。下面这个是我实际调试中打印出来的握手消息示例脱敏后的真实结构# 客户端发起 initialize 请求 { jsonrpc: 2.0, id: 0, method: initialize, params: { protocolVersion: 2024-11-05, clientInfo: {name: agent-host-demo, version: 1.0.0}, capabilities: { roots: {listChanged: True}, sampling: {}, experimental: {} } } }# 服务端返回 initialize 响应 { jsonrpc: 2.0, id: 0, result: { protocolVersion: 2024-11-05, serverInfo: {name: my-tools-server, version: 0.3.1}, capabilities: { tools: { listChanged: True }, resources: { subscribe: True }, prompts: {} } } }握手完成后双方还要互发一个notifications/initialized通知表示初始化阶段结束可以开始正常业务消息了。我建议初学者调试时一定要打印这个阶段的原始数据因为你后面遇到几乎所有Client连不上Server或者功能拉不出来的问题都出在这个握手阶段版本协商失败、能力字段拼错、JSON里多了个逗号都会让连接静默失效。2.3 Tools、Resources、Prompts协议里的三套原语协议里最常被问到的是三套东西Tools工具、Resources资源、Prompts提示词模板。很多人一开始以为它们只是同一个东西的三种叫法其实定位完全不同需要分开去理解。Tools是用来执行动作的接口。模型调用它是为了产生副作用或获取实时数据比如查询数据库、调外部API、给用户发通知。Tool调用是动态的、有参数的、需要一次往返的。Resources则更像文件是给模型读取的上下文内容比如一个项目里的README、一份运行配置、一张图片的链接。模型通过resources/read读取它们但不产生外部副作用。而Prompts是给用户或模型使用的模板它包含预先编排好的指令和上下文能帮助模型以某种既定方式工作。我记得一个特别贴切的类比Resources像资料库模型想了解背景就翻资料Tools像手和脚模型要改变世界才动手Prompts像标准作业指导书告诉模型遇到某类任务时按什么套路回答。下面这个表格可以作为设计时的参考原语核心关键词交互方式典型例子副作用Tools动作、函数模型发起tools/call查航班、发邮件、执行计算可以有Resources数据、上下文模型发起resources/readREADME、配置文件、日志文件通常无Prompts模板、引导用户或环境获取后注入代码评审Prompt、周报Prompt无分清这三者你在设计Server时就不容易把该给模型读的文件错做成工具调用。我见过有人把数据库表结构塞进工具描述里结果模型每次调用都要传一堆不相关的列上下文开销翻倍响应质量还差。正确的做法是表结构用Resources提供而执行查询并返回结果用Tools提供。3. 手把手实现一个MCP Server从空目录到可用服务理论说多了容易飘下面进入实操环节。我用的是Python生态用的库是社区里常用的FastMCP封装基于官方SDK做了一层简化拿来写Demo特别顺手。它已经把握手、工具注册、传输这些细节封装好了你只需要关心业务函数。3.1 十分钟拉起第一个MCP Server先把环境配好先做准备工作。你只需要Python 3.10以上然后安装依赖# 建议在虚拟环境里操作 mkdir mcp-server-demo cd mcp-server-demo python -m venv .venv source .venv/bin/activate # Windows下用 .venv\Scripts\activate pip install mcp[cli] fastmcp httpx安装完成后创建一个server.py下面是完整的迷你服务端代码from fastmcp import FastMCP mcp FastMCP(demo-server) mcp.tool() def add(a: int, b: int) - int: 把两个整数相加返回结果。 return a b mcp.tool() def current_time(timezone: str Asia/Shanghai) - str: 获取指定时区的当前时间timezone 参数为 IANA 时区名。 from datetime import datetime from zoneinfo import ZoneInfo now datetime.now(ZoneInfo(timezone)) return now.isoformat() if __name__ __main__: mcp.run()跑起来特别简单python server.py默认情况下它会以stdio通道运行也就是不监听端口通过标准输入输出跟父进程通信。你直接在终端里运行时会发现好像什么都没发生这是正常的因为协议等着客户端来通过管道连它。很多初学者在这一步会以为程序卡死了实际不是。3.2 定义带完整Schema的工具让模型精准理解你的函数上面的例子太简单真实场景里工具的参数往往不是两个int而是一个复杂对象。假设我们要做一个查询航班状态工具它需要传入航班号、出发日期、乘客类型等参数。此时就不能靠类型注解硬推了我推荐用Pydantic模型显式定义参数Schema。from pydantic import BaseModel, Field from fastmcp import FastMCP mcp FastMCP(flight-server) class FlightQuery(BaseModel): flight_number: str Field(description航班号如 CA123) departure_date: str Field(description出发日期格式 YYYY-MM-DD) passenger_type: str Field(defaultadult, descriptionadult/carefree/infant) mcp.tool() def query_flight_status(query: FlightQuery) - dict: 根据航班号与出发日期查询航班当前状态包括计划起飞时间、预计到达时间、延误状态与登机口。 # 这里替换成你的真实航班API调用 # 演示用一条模拟数据返回 return { flight_number: query.flight_number, date: query.departure_date, status: on_time, gate: C18, scheduled_departure: 09:30, estimated_arrival: 11:50 } if __name__ __main__: mcp.run()代码里最关键的不是业务逻辑而是Field里的description。这些描述最终会变成工具JSON Schema里的描述直接决定大模型能不能正确填参。我自己调试时做过对比字段描述具体到格式必须是YYYY-MM-DD时模型传错格式的概率能降一半以上什么都没写时模型经常乱发挥甚至会把日期传成下周五这种自然语言。这里有个建议工具的函数名要清晰但不要太长别用一长串动宾结构。模型选择工具主要看函数名和第一个描述句子这个句子要能回答三个问题这个工具处理什么领域能拿到什么类型的结果调用时注意什么前提条件一个反例是process_data这种完全看不出用途模型只能靠猜正例是query_flight_status_by_date光看名字就八九不离十。对于有基础读者我强烈建议你在工具实现里加上一段内部日志打印收到的原始参数这在后续对照模型实际传了啥和服务端打算干啥时非常有用。3.3 错误处理与结构化返回别把崩溃信息直接抛给模型工具函数出异常是必然的问题是怎么返回。如果你在Tool里直接raise一个异常FastMCP会把它包装成协议级错误返回给客户端模型能看到一段堆栈或错误文本。这个行为在调试期没问题但生产中很不友好因为模型经常拿错误堆栈去猜到底要怎么改参数反而越猜越乱。更好的做法是把错误变成结果的一部分返回一个结构化的状态对象。我惯用的返回模板是mcp.tool() def query_flight_status(query: FlightQuery) - dict: try: data real_flight_api.fetch(query) except FlightNotFound: return {success: False, error_code: FLIGHT_NOT_FOUND, message: 没有找到该航班请核对航班号与日期} except ApiTimeout: return {success: False, error_code: UPSTREAM_TIMEOUT, message: 航司接口暂时没有响应请稍后再试} return {success: True, data: data}这样做的好处是模型拿到结果后不用猜测它能直接根据success和error_code决策下一步可以换参数重试、可以告诉用户查询失败也可以转人工。这套逻辑和Web服务的错误码设计是相通的只是注意模型的指令系统对错误码的语义理解能力有限所以message字段一定要写人话而不是0x12ABFCD这种内部编码。另外返回结果里不要塞无关字段。我见过一些后端同事习惯把上游API的原样响应直接返回里面几十个字段模型读起来既费token又容易迷失重点。正确做法是做一层裁剪只返回模型决策和用户展示真正需要的字段。3.4 本机调试用MCP Inspector观察每条消息写完Server怎么验证我强烈推荐直接使用官方配套的调试工具MCP Inspector装SDK时通常会一并带上如果你用的是mcp[cli]它已经在你的虚拟环境里了。它是一个本地Web面板能让你可视化地浏览工具列表、发起调用、查看原始日志。启动方式有两种。一种是在当前目录下执行mcp dev server.py这个命令会以stdio模式启动你的Server并在浏览器里打开调试面板面板上能看到Service正在运行、工具列表、资源列表和实时日志。另一种方式是针对远程服务你可以直接mcp inspector在面板里配置远程连接地址。调试面板里我最常用的是消息日志页它会原样展示每一个Client和Server之间的JSON-RPC消息。我过去排查90%的接入问题都是靠看这几个东西握手阶段有没有成功、工具列表拉取时是否带上了Schema、调用某个工具时模型实际传的参数是什么。看完这些绝大多数为什么模型不选我的工具都能找到答案。一个很多新手的误区是写完工具直接丢给宿主结果一点日志都不看只凭模型答得不好来判断工具坏了。实际问题很可能是工具本身没挂上、参数校验失败或者描述写得让模型误以为这是另一个用途。MCP Inspector的使用习惯应该像写代码时用VS Code断点一样成为调试的默认动作。4. 接进真实宿主从本地配置到远程传输MCP的价值最终体现在接进一个正在跑的Agent宿主里。宿主的选择很多可能是自研的Agent框架也可能是现成的AI客户端应用。这一节我用最常见的两种接入场景来讲配置细节和排查思路。4.1 本地接入理解stdio传输的配置字段如果你的MCP Server是本地脚本通过stdio模式接入宿主客户端通常需要这样一段配置{ mcpServers: { flight-server: { command: python, args: [/absolute/path/to/server.py], env: { FLIGHT_API_KEY: your-key-here, LOG_LEVEL: DEBUG } } } }在接入时最容易被忽视的是command和args的组合方式。如果你用uvx作为启动器command是uvx、args是server.py的路径或包名如果你用python那command就是Python解释器的完整路径。尤其要注意Windows环境command必须指向虚拟环境里的python.exe否则宿主拉起时根本没找到依赖。另一个大坑是环境变量传递。宿主动替你启动子进程时不会自动继承你在终端里export的变量必须显式写在env字段里。我碰到过最典型的一次本地终端测试一切正常接入宿主后工具一直报鉴权失败排查了半天才发现是环境变量没有喂进去。所以凡涉及密钥、Token、隔离的配置全部显式声明在env里不要依赖当前用户的shell环境。4.2 远程接入SSE传输与访问策略如果你的工具能力部署在一台远程服务器上或者多个宿主需要共享同一个工具Server那就要用远程传输。协议客户端通过HTTP与Server建立一个SSEServer-Sent Events连接实现对请求和消息的接收。这种模式在配置上通常是{ mcpServers: { flight-server: { url: https://tools.example.com/mcp, headers: { Authorization: Bearer token } } } }远程模式要注意三点。一是Server端必须实现SSE端点并处理客户端的初始化会话二是在headers里携带认证凭证这需要在Server端实现授权逻辑三是因为走HTTP需要考虑网络超时、流式传输和断线重连。这套在功能层面和stdio没有差异但运维复杂度显著提高你要有日志系统、健康检查和证书管理。我个人的建议是本地优先。如果你的宿主运行在生产环境附近工具进程和数据都被放在相邻部署位置上直接用stdio子进程方式会简单很多而且本地的工具调用天然受到宿主操作系统的权限控制不必为网络攻击面操心。远程SSE适合那种工具能力确实需要单独部署或多个团队共享一套数据源的场景。4.3 连上之后怎么确认真的调通了日志与追踪配置完成后普通用户看不到链路内部到底发生了什么。但作为开发者你必须在第一次接入时把链路验透。我这边的标准流程是四步第一观察宿主启动日志确认有没有出现connection opened或等价字样这表示子进程或远程连接已经建立第二在Server端打开listChanged能力并打印tools/list请求对比模型实际看到的工具列表是不是你想要的那几个第三让模型用一句话触发某个工具比如帮我看看CA123航班然后立刻到Server端日志里看tools/call消息确认入参是否合理第四检查返回结果是否落在模型回答里注意模型可能不会复述原始JSON而是组织成自然语言。如果你在宿主界面上看不到任何工具调用痕迹优先检查的不是模型提示词而是宿主读的MCP配置文件的路径对不对。很多桌面宿主应用会在用户目录下的配置文件里寻找MCP服务器列表而不是你的项目目录路径配错会让一切静默失效。这类问题最折磨人但你只要有一份能实时查看Server进程stdout日志的终端十分钟内就能定位。5. 进阶能力认证鉴权、资源订阅与提示词模板基础跑通之后下一步是让Server能应对更真实的生产约束。这里我把自己实际做过的三个方向提炼出来认证资源订阅提示词模板。5.1 在Server端集成OAuth谁有资格调用我的工具当你的工具Server要暴露给内部或外部多个宿主时必须做认证授权。MCP协议本身允许在握手阶段携带凭证但大多数落地场景用的是标准OAuth授权码模式。举例来说我在内部实现过一个Git工具Server它接公司内部代码库。流程是宿主打开后自动向授权服务器发起跳转用户同意授权后拿到一个短期TokenToken通过协议层传给ServerServer每次调用真实功能时带着Token去请求上游API。实现关键在Server端校验Token的有效性并按用户身份对操作权限做分级控制。比如普通用户只能read资源管理员才能call写类工具。这个设计有一个直接的好处工具能力本身不需要再造一套用户体系直接复用组织的统一身份源。MCP里的认证可以发生在传输层比如HTTP的Authorization头也可以发生在消息层细节取决于你的实现。我的建议是优先放在传输层这样Server代码不用在每个Tool里额外校验身份只需要在网关或入口处统一做。5.2 Resources与订阅让Agent能感知数据变化工具的角色是临时拉数据但有些数据是持续在变的比如某个配置文件、某个服务的最新部署状态。你当然可以每次让模型调用一个get_file_content工具但这绕了远路而且模型不一定会在每次回答前主动去查。MCP的Resources提供了一种更语义化的交互方式把某些数据暴露成资源Agent宿主动态获取到资源列表及内容如果资源还支持subscribe宿主还能在资源变化时收到通知。这一个能力在实际项目中特别适合做文档和配置注入。比如你做了一个项目运营报告生成Agent它需要了解最新的业务指标集群配置这些配置就以资源的形式暴露。指标每更新一次资源Watcher就发一个notifications/resources/updated给宿主宿主在后续对话中就能自动拿到最新值不再依赖旧的快照。这个机制大幅减少了模型无谓的查询动作也避免了模型自以为知道最新数据的幻觉风险。5.3 Prompts模板把常用多步流程固化成套路除了工具和资源Prompts是很多人忽略但很实用的原语。它本质上是预置的提示词模板里面可以包含指令、输入参数定义和少量上下文。宿主的用户可以直接选一个模板填参数后生成一段完整指令。我在实际项目中做过一个新功能评审Prompt的模板模板里要求模型按照架构合理性、安全隐患、性能边界三个维度去阅读代码库里对应的资源再输出结构化评审意见。以前这整套指令每次都要手写在对话里现在直接通过prompts/get获取并注入。这个能力的价值在于把你团队里最优的协作方式沉淀成可复用的协议资源而不是让每个使用者自己临场发挥。用好Prompts要注意一个问题模板不要写得像万能咒语一样冗长。模型指令越具体越好但太长的模板也会占用上下文影响后续工具调用。我习惯控制在400字以内把最关键的要求写清楚然后用结构化输出格式约束结果。6. 真实落地过程中的坑与经验来自生产环境的教训最后这部分是我最想写的。协议本身学起来不难真正的门槛都在那些文档没写但你一定会撞上的细节里。6.1 超时与长耗时任务模型会放弃等待MCP的常规调用是短请求模型发来tools/callServer处理完就返回。但真实业务里很多工具不是瞬时动作比如发邮件、生成一份长报告、训练一个小模型、调用一个慢的第三方接口。如果你在同步函数里阻塞太久宿主端的超时机制会先不耐烦切断连接或直接给用户报工具执行失败。我当时就踩过一次一个工具要调用外部大模型做摘要平均耗时40秒而宿主默认的超时只有30秒导致工具经常失败但日志里又查不到具体错误。这个问题的解法有三种一是升级到支持流式响应的传输方式先把开始执行的响应返回再异步发完成通知二是把长任务拆成提交任务和查询结果两个工具前一个立即返回任务ID后一个轮询或订阅结果三是让宿主放宽对特定工具的超时限制但这要看宿主是否支持。我更推荐第二种方案。它把非确定性的耗时转化成了确定性的轮询问题语义上对模型也很友好模型看到提交任务返回了ID它就知道应该继续调用查询任务状态工具来追踪结果。很多Agent框架内置了这种多步调用能力模型也会自己组织顺序。6.2 工具描述和返回结构决定了模型会不会选择你的工具我在实战里发现一个规律模型对工具的选择率和工具的Schema质量呈强相关。如果你有两个工具都能查天气一个描述是获取天气另一个是获取指定城市未来7天天气预报可选预报开始日期返回数组包含每日天气与穿衣建议模型几乎总是优先尝试后者因为它明确知道适用范围和输出形态。另一个容易翻车的地方是返回值的稳定性。上游API偶尔会缺字段、改字段类型导致返回结果结构不稳定。模型一旦发现这次返回[status: ok]、下次变成{result: {status: ok}}它后续对结果的理解就会混乱。所以我强烈建议在工具内部做一层数据规格化确保从工具返回的任何结果字段名和类型都是一致的宁可多写几个映射函数也不要给模型不稳定输出。6.3 协议版本与能力协商不要假设对方什么都懂MCP协议还在快速演进不同SDK版本、不同宿主之间对协议版本的支持程度并不一致。我在对接一个第三方宿主时就发现它支持的mcpVersion比我当前SDK默认要求的新版本要老结果握手时直接失败。这个问题在协议演进期会非常常见解决方案也很机械在initialize阶段查看宿主声明版本如果失败降级SDK版本或显式在配置里指定兼容版本。能力协商这块容易踩的另一个坑是宿主声明支持resources.subscribe你的Server也在capabilities里声明了但双方处理通知消息的细节不一致导致订阅总是静默断开。排查这类问题最有效的办法还是回到MCP Inspector的消息日志页看到底是哪条消息被静默丢弃或格式不符。不要凭感觉推断协议阶段的问题几乎都能在原始消息里找到答案。6.4 工具粒度一个万能工具不如几个精准工具设计MCP Server时最常见的一种思维惯性是把所有能力揉成一个工具传一个action参数用if-else分支区分查天气查股票查新闻。这个设计看着省事协议层面也能工作但它会直接伤害模型的基础性能。原因是模型的工具选择本质上是一个排序任务给出一堆工具名和描述模型判断选哪个。如果工具数量少而分支多模型就需要在推理时理解action等于什么时才对应什么这比选一个已命名的工具要难得多还容易传错参数。我在项目里做过一次对比把一个大工具拆分成五个小工具后模型在评测集上的工具选择准确率明显提升生成延迟反而没有增加因为每个工具的schema都简单了。我的参考原则是工具粒度以场景动作为边界。比如查询航班状态和查询航班价格是两个工具而不是一个大工具里用typestatus/price区分。如果动作天然分属不同领域比如查天气和发邮件更要明确拆开让模型一眼就能按领域锁定候选集。6.5 权限与审计工具Server不是命令执行器的后门最后说一个容易被忽略的问题。MCP Server往往运行在宿主进程的上下文里它能调外部API、读文件、访问数据库。如果这个能力暴露给不受控的用户相当于把系统的某个操作入口直接开到了AI面前。我见过一个团队为了演示方便把一个执行任意Python代码的工具挂上结果模型被诱导执行了危险命令好在是在沙箱环境里否则后果不敢想。我的实际经验是给每个工具定义明确的能力边界在工具内做参数白名单和输入校验不允许的东西一定不要提供Server进程用独立的低权限账号运行不要继承宿主的高权限身份所有tools/call记录完整的调用参数、调用用户和返回结果便于事后审计。这一点在协议层面没有强制约束完全靠实现者的工程素养。你不想因为图省事把AI变成内网穿透的后门那就要从一开始把权限边界设计清楚。最后分享一个我验证过的组合拳把Server设计成协议薄、业务厚的分层结构协议层只负责解析JSON-RPC和转发业务层全部走统一的服务接口这样即便MCP协议后续升级你只需要替换协议层业务逻辑完全不受影响。我目前维护的几个Server都用了这种结构每次升级SDK几乎没有改过业务代码这比把协议逻辑和业务逻辑揉在一起能省太多事了。