模型上下文协议(MCP)的浪潮大约在一年前席卷而来,开发者们蜂拥而上构建服务器,希望借此释放大语言模型的全部潜能。
现实却不尽如人意,许多MCP服务器的表现令人失望,社交媒体上甚至出现了协议已死的论调。
开发者们普遍的抱怨指向了协议本身,尤其是其带来的高昂的Token消耗。
但公司正在成功部署MCP服务器,集成也已上线,只是结果并未达到预期。
问题究竟出在哪里。
开发者们习惯性地将MCP服务器视为传统具象状态传输(REST)应用程序接口(API)的一层简单封装,这正是症结所在。
谷歌DeepMind工程师发文开怼:你的MCP服务器正在扼杀你的AI代理,不是MCP协议本身。
MCP的本质是为AI代理设计的用户界面(UI),一个全新的用户群体需要一套截然不同的设计哲学。
当前,Skill的轻量级代理能力扩展方式正悄然兴起,它通过自然语言指令和脚本教会代理新能力,备受社区热捧。
这引发了新的讨论,Skill会取代MCP吗。
谷歌工程师带来了构建高效MCP服务器的核心最佳实践,并厘清MCP与Skill之间的真实关系,为你提供一套清晰、可操作的指南,帮助你构建真正为AI代理服务的用户界面,让你的AI不再迷路。
为AI代理设计用户界面
将MCP服务器视为AI代理的用户界面,是构建高效能代理系统的关键思想转变。
AI代理是一种非人类用户,它们的需求与人类开发者有着本质的区别。
我们不能想当然地认为,大型语言模型足够聪明,能够像人类一样理解和使用为开发者设计的API。
许多开发者陷入的第一个误区,便是将现有的REST API进行一对一的直接封装,变成MCP工具。
一个设计精良的REST API,并不等同于一个好的MCP服务器。
REST API的设计原则,如可组合性、可发现性、灵活性和稳定性,都是为人类开发者量身定做的。
开发者可以阅读一次文档,然后灵活地组合各种小型的API端点来完成复杂任务。
这种模式对于人类来说是高效的。
但对于AI代理,这些优点却可能成为沉重的负担。
代理在每一次与外界交互时,都需要在它的上下文窗口中加载工具的完整定义,也就是API的模式(Schema)。
如果我们将REST API直接搬过来,代理的上下文窗口就会被大量为人类设计的、细粒度的API定义所填满。
这导致了高昂的Token成本,也是MCP备受诟病的一点。
代理的发现成本变得极其昂贵,它不得不在每次请求中都阅读一遍冗长的文档。
让我们通过一个表格来更直观地对比REST API设计原则对开发者和AI代理的不同影响。
人类开发者钟爱的可组合性,对于代理来说意味着缓慢的多步工具调用和漫长的迭代过程。
代理需要自行编排调用顺序,处理中间结果,每一步都是一次独立的网络往返,效率低下。
而人类开发者追求的灵活性,即在一个API中提供纷繁复杂的选项,对代理而言则是一场灾难。
过度的复杂性会极大地增加代理产生幻觉的风险,它可能会错误地猜测参数的用法,或者在众多选项中迷失方向。
更糟糕的是,很多MCP服务器变成了数据倾倒服务,一次性向代理返回海量的原始数据。
这会迅速撑爆代理本就有限的上下文窗口,导致关键信息丢失,甚至使整个任务失败。
因此,我们需要为AI代理这个特殊的用户建立一个清晰的画像。
它们没有人类的直觉、常识或模糊处理能力。
它们完全依赖你所提供的界面,即工具的名称、描述、参数和返回结果。
它们在严格的上下文窗口限制下工作,每一次交互、每一次Token的消耗,都弥足珍贵。
我们必须像产品经理一样,为这个非人类用户,精心设计它们的用户体验。
一个典型的例子是订单追踪。
假设你正在构建一个帮助用户追踪订单的代理。作为一名人类开发者,你会先阅读API文档,然后编写一个脚本,依次调用三个API端点,例如获取用户信息(GET /users),获取该用户的订单列表(GET /orders),最后获取特定订单的物流状态(GET /shipments)。
你调试脚本,然后部署它,整个过程一气呵成。
一个糟糕的MCP服务器会怎么做呢。
它会把这三个API端点原封不动地暴露给代理,变成三个独立的工具。
这意味着代理为了完成追踪订单这个简单的任务,必须在其上下文中加载所有三个工具的描述,然后像人类开发者一样,进行三次独立的网络往返调用,并将所有的中间结果存储在对话历史中。
这个过程效率低下,而且极易出错。
一个优秀的MCP服务器则会提供一个名为track_order的工具,它只接受一个参数,比如用户的电子邮箱地址。
这个工具在服务器内部完成对上述三个API的依次调用和数据整合,最后只返回一个简洁明了的结果,例如订单 #12345 已通过联邦快递(FedEx)发货,预计将于周四送达。
同样的结果,一次调用,结果导向。这才是为代理设计的界面。
记住,MCP是AI代理的用户界面。
你需要运用相同的产品思维,只是服务的用户不再是人类。
这个思维的转变,是构建高效MCP服务器的基石。
构建优秀MCP服务器的六项实践
遵循六项核心实践,可以系统性地构建出易于AI代理理解和使用的高质量MCP服务器。
这套方法论将帮助你从根本上转变观念,从为开发者提供API转向为代理设计UI。
每一项实践都将遵循陷阱、修复、详解的模式,并辅以通俗易懂的类比,让你彻底掌握其精髓。
实践一,结果导向,而非操作导向。
常见的陷阱是将REST API的端点一对一地转换为MCP的工具。
这种做法看似省力,实则将本应由服务器承担的编排工作,甩给了本就不擅长此道的AI代理。
正确的修复方式是围绕用户或代理真正想要达成的结果来设计工具。
这好比你去餐厅点餐,你只需要告诉服务员我想要一份宫保鸡丁。
你关心的是最终上桌的那盘菜,一个完整的结果。你不会,也不需要去指挥后厨的厨师先去切二两鸡丁,再准备一把花生米,然后调一碗糖醋酱汁。
AI代理就是那个饥肠辘轆的食客,它只关心结果,对过程毫无兴趣。
而一个糟糕的MCP服务器,却在强迫代理成为那个指手画脚的总厨。
正如前面提到的订单追踪案例,不要暴露三个独立的操作工具get_user_by_email()、list_orders(user_id)和get_order_status(order_id)。
这种设计迫使代理必须自行进行三轮独立的网络请求和响应,极大地增加了它的推理负担和出错概率。
你应该提供一个更高层次、面向结果的工具,例如track_latest_order(email)。
将所有的业务逻辑和API调用编排都封装在你的服务器代码中,而不是推给大型语言模型的上下文窗口去处理。
这能极大提升代理的执行效率,还能显著降低Token的消耗。
实践二,扁平化参数。
另一个常见的陷阱是在工具的参数中,使用复杂的、层层嵌套的字典(dictionaries)或配置对象(configuration objects)。
开发者或许习惯了这种灵活的数据结构,但对于AI代理来说,这就像是让它去填写一份需要在一堆嵌套文件夹里寻找不同表格的复杂申请表,极易迷路。
修复这个问题的关键在于,尽可能使用顶层的、扁平化的基本数据类型(primitives)和受到严格约束的类型作为参数。
一张所有问题都清晰罗列的单页表格,远比一堆复杂的嵌套表格要友好得多。
扁平化的参数就是这张清晰的单页表格。
让我们来看一个具体的对比。
在糟糕的设计中,代理面对一个模糊的filters: dict参数,它不得不去猜这个字典里应该包含哪些键,比如是user_email还是customer_email,是order_status还是status。
这种猜测极易出错。
而在优秀的设计中,email、status和limit这三个参数被清晰地定义在顶层。
更妙的是,status参数通过Literal类型被严格限制在pending、shipped、delivered这三个合法的选项之中,代理无法提供任何其他值。
同时,为status和limit提供了合理的默认值,这意味着代理在最简单的情况下,甚至只需要提供一个email参数就可以成功调用这个工具,极大地降低了使用门槛。
实践三,指令即上下文。
很多开发者会忽略工具的文档字符串(docstrings)和错误信息,认为它们只是写给人看的注释。
这是一个巨大的陷阱。
对于AI代理来说,你提供的每一个字,都是它赖以生存的上下文。
修复方法是,将你写的每一段文本,都视为代理上下文的一部分,精心雕琢。
工具的文档字符串和错误信息,就像是产品的使用说明书和故障排查手册。
没有它们,代理只能在黑暗中胡乱摸索。
一个优秀的文档字符串,不应该只是简单描述功能,它更是一份清晰的操作指南。
它必须明确告诉代理何时使用这个工具,例如当用户询问订单状态时使用此工具。
它必须说明如何格式化参数,例如电子邮件地址必须为小写格式。
它还必须描述可以期待什么样的返回结果,例如将返回订单ID和当前的物流状态。
同样,错误信息也是至关重要的上下文。
当一次工具调用失败时,不要直接向代理抛出一个冷冰冰的Python异常堆栈。那对它毫无帮助。你应该返回一个充满信息的、友好的字符串。
例如,返回未找到该用户,请尝试使用用户的电子邮件地址进行搜索。
代理在它的观察(observation)中看到这条信息后,就能理解失败的原因,并根据你的提示在下一步中自我修正。你正在通过错误信息,教会代理如何解决问题。
实践四,无情精简。
暴露你的API所能做的一切,返回你的API所能返回的一切,这是一个极具诱惑力的陷阱。
开发者往往认为多多益善,提供更多功能和数据总不会错。
对于在有限上下文窗口中挣扎的AI代理来说,这恰恰是致命的。
正确的做法是为发现而设计,而不是为了穷尽而暴露。
你需要无情地进行精简。
想象一下,一个只有几个核心功能按钮的简洁遥控器,远比一个布满上百个按钮的复杂遥控器要好用得多。
你的工具集,就是AI代理的遥控器。
代理的上下文窗口是一种极其宝贵的稀缺资源。
每一个多余的工具定义,每一个多余的返回字段,每一个无关紧要的错误信息,都在无情地挤占这个有限的空间,干扰代理的判断。
因此,你需要像一个严苛的产品经理一样,对你的MCP服务器进行精简。
每个服务器只保留5到15个核心工具。
如果工具太多,就说明服务器的职责划分可能过于宽泛。
一个服务器,一个核心职责。
不要试图构建一个无所不能的瑞士军刀式服务器。
让每个服务器都聚焦于一个明确的业务领域,比如订单管理或用户通知。
果断删除未被使用的工具。
定期审查工具的使用情况,移除那些冗余的或极少被调用的工具。
根据用户角色进行拆分。
例如,为普通用户和管理员用户提供两套不同的MCP服务器。
管理员服务器可以包含一些高权限的操作,而普通用户服务器则只提供他们需要的功能。
你的目标是构建一个易于发现的环境。
代理应该能够快速地在有限的几个选项中,找到那个最正确的工具,并从工具的返回中,得到一个简洁、可直接用于行动的响应。
实践五,为发现命名工具。
给工具起一些像create_issue或send_message这样的通用名称,是另一个常见的陷阱。
在一个大型的图书馆里,如果所有的书都叫书,你将永远找不到你想要的那一本。
清晰的命名规则,就是图书馆的索引系统。
修复这个问题的方法是,使用带有服务前缀的、面向动作的命名方式。
你的MCP服务器通常会和其他许多服务器一起运行。
如果代码托管平台GitHub和项目管理工具Jira都提供了一个名为create_issue的工具,当用户说创建一个问题时,代理就只能去猜测应该调用哪一个。这会带来不确定性。
一个健壮的命名模式是{service}_{action}_{resource}。
遵循这个模式,你的工具名称会变得清晰且无歧义。
例如slack_send_message表示在Slack中发送消息,linear_list_issues表示列出Linear中的问题,sentry_get_error_details表示从Sentry获取错误详情。
这种命名方式让代理可以轻易地将用户的意图映射到正确的工具上。
值得注意的是,一些现代的MCP客户端会自动为工具名称加上服务器的前缀,但这依然不能取代你在定义工具时就采用清晰命名的好习惯。
实践六,分页大结果。
最后一个常见的陷阱是一次性向代理返回数百甚至数千条记录。
这就像是试图一次性给你的朋友发送一部一万页的小说,结果只会是对方的设备崩溃。代理的上下文窗口同样会因此而崩溃。
正确的做法是,使用带有元数据的分页机制来处理大量的返回结果。
与其一次性给代理一整本书,不如把它分成章节,并告诉它总共有多少章,以及当前在第几章。这样代理就能轻松地阅读和处理。
一个健壮的分页设计应该包含以下几点。
你的工具应该接受一个limit参数,让调用者可以指定希望返回的记录数量。
为这个参数设置一个合理的默认值,比如20或50。
在你的返回结果中,除了数据本身,还必须包含分页的元数据。
这通常包括has_more(一个布尔值,表示是否还有更多数据),next_offset或next_page_token(用于获取下一页数据的标识符),以及total_count(总记录数)。
在服务器的实现层面,你应该直接在数据库查询时就使用LIMIT和OFFSET,而不是将所有数据查询出来后再在内存中进行切片。
这对于保持服务器的性能和稳定性至关重要。
通过这种方式,代理可以根据需要,一页一页地获取信息,而不会因为一次性的数据洪流而淹死。
Gmail MCP服务器的实战演练
让我们将前面讨论的六大最佳实践应用到一个具体的场景中。
我们将以构建一个与谷歌邮箱(Gmail)交互的MCP服务器为例,通过前后对比,直观地展示为代理设计的思想如何将一个笨拙难用的工具集,转变为一个优雅高效的代理界面。
假设我们的目标是让AI代理能够帮助用户读取和发送电子邮件。
一个典型的、未经深思熟虑的实现方式,通常是直接将Gmail官方API的功能进行一对一的映射。
下面是一个遵循传统API封装思路构建的MCP服务器,我们称之为改造前的版本。
这个实现存在着严重的问题,它完全没有考虑到AI代理这个用户的特殊性,几乎违背了我们前面提到的所有最佳实践。
它违背了结果导向原则。
仅仅是读取一封邮件这个看似简单的任务,代理就需要进行两次调用。
它必须先调用messages_list来根据查询条件搜索出邮件列表,从中提取出message_id,然后再用这个ID去调用messages_get来获取邮件的具体内容。
这是典型的操作导向设计,将本应在服务器内部完成的逻辑编排工作,完全推给了代理。
它违背了扁平化参数和精简返回原则。
messages_send和drafts_create工具都接受一个名为message的复杂嵌套对象作为参数。代理必须自行构造一个包含{"raw": ...}结构的字典。
更糟糕的是,这个raw字段的值还需要是经过特殊编码(base64url)的MIME格式字符串。
这对代理来说是一个巨大的认知负担,它需要理解MIME格式,知道如何进行Base64编码,这极大地增加了它产生幻觉和出错的概率。
同样,messages_get的返回结果也是一个复杂的嵌套对象,代理需要从payload.body.data这样深层的路径中去解析邮件正文,非常不便。
它违背了为发现命名原则。
这些工具的名称如messages_list、messages_get、messages_send都非常通用。
如果环境中还存在其他消息服务(比如Slack或WhatsApp)的MCP服务器,代理将很难区分它们,从而导致混淆。
总而言之,这个改造前的服务器对AI代理极不友好。
它要求代理像一个经验丰富的软件工程师一样去理解API的细节,处理复杂的数据结构和编码,这完全违背了构建MCP服务器的初衷。
现在,让我们运用六大最佳实践,对这个服务器进行彻底的重新设计。
下面是改造后的版本。
这个改造后的版本看起来简洁了许多,但其背后蕴含着深刻的设计思想,完美地应用了我们的六大实践。
它体现了结果导向与精简返回。
我们将读取邮件的流程拆分成了两个清晰的步骤,分别对应两个不同的结果。
gmail_search工具负责根据查询返回一个简洁的邮件列表,这个列表只包含代理最关心的核心信息:ID、主题、发件人、日期和摘要。
所有不必要的嵌套和字段都被移除了。
当代理需要阅读某封邮件的全文时,它再使用gmail_read工具,传入message_id,直接获取到同样经过精心处理的邮件正文和附件列表。
整个过程清晰明了,返回的数据干净利落。
它体现了扁平化参数。
gmail_send工具的改变是革命性的。它不再需要代理去构造任何复杂的对象。发送一封邮件所需要的所有信息,收件人(to)、主题(subject)和正文(body),都作为顶层的、扁平化的参数直接提供。
代理只需要传入简单的字符串和列表,就可以轻松地发送邮件。
所有关于MIME格式和Base64编码的复杂性,都被优雅地封装在了服务器的内部实现中。
我们甚至还提供了一个可选的reply_to_id参数,让代理可以方便地回复邮件。
它体现了无情精简。
在gmail_send的设计中,我们刻意省略了抄送(cc)和密送(bcc)等不常用的功能。
我们优先满足80%最常见的用例,而不是试图构建一个大而全的、包含所有边缘功能的复杂工具。
这让工具本身变得更加简单,易于代理理解和使用。
它体现了为发现命名。
所有工具都加上了gmail_的前缀,这使得它们在多服务器环境中具有了唯一的标识性,代理可以毫不费力地将它们与其他工具区分开来。
它体现了指令即上下文和分页。
虽然在代码片段中没有直接展示,但我们可以想象,gmail_search的文档字符串会清晰地说明它的用途和query参数的格式,并且它的实现会严格遵守limit参数进行分页返回。
同样,gmail_send的返回结果中包含一个success布尔值,这为代理提供了明确的成功或失败信号,是一种有效的错误信息上下文。
通过这个Gmail的实战演练,我们可以清晰地看到,遵循最佳实践所带来的巨大差异。
改造后的服务器不再是一个笨拙的API搬运工,而是一个为AI代理量身定制的、优雅、高效的用户界面。
它让代理的工作变得简单,也让最终的用户体验变得更加流畅和智能。
作为开发者,当你开始构建自己的MCP服务器时,请时刻将这个Gmail的例子放在心中。
不断地问自己:我是在为另一个开发者构建API,还是在为一个AI代理设计UI?
这个简单的思维转变,将决定你的项目最终的成败。
Skills与MCP是互补而非竞争
在探讨如何构建优秀的MCP服务器时,我们无法回避一个近期在AI代理领域备受关注的新概念:Skills。
随着社区对轻量级、更加灵活的代理能力扩展方式的追求,Skills迅速走红。
这不禁让许多开发者产生疑问:Skills的出现,是否意味着MCP的过时?
它们之间是相互替代的竞争关系吗?
答案是否定的。
Skills和MCP并非相互排斥,而是在不同层面解决问题的互补方案,二者联手,才能构建出最强大、最灵活的AI代理生态。
要理解这一点,我们需要了解Skills是什么。
从本质上讲,Skills是一种打包了指令、元数据和资源的知识包,代理可以在需要时加载和使用它们。
它更像是一本操作手册或一段教学代码,通过自然语言描述和可执行脚本,来教会代理如何完成一项特定的任务。
这种方式是基于文件系统的,并且采用了渐进式披露(progressive disclosure)的策略,即只在需要时才将相关信息加载到代理的上下文中,从而有效地管理了宝贵的Token资源。
让我们通过一个表格,来更清晰了解Skills的三层加载机制和对应的Token成本。
从这个表格中,我们可以看出Skills和MCP的根本区别。
MCP提供的是一种API式的硬连接。
它通过严格的、结构化的工具定义(schema),为代理提供了一个稳定、可靠、类型安全的接口。
代理通过MCP调用工具,就像程序调用一个函数一样,参数和返回值都有明确的契约。
这使得交互过程高度可预测,不易出错,特别适合于将企业内部成熟、稳定的服务暴露给代理使用。
而Skills提供的则是一种文档式的软指导。
它不直接定义新的工具,而是教会代理如何使用现有的、更通用的工具(比如shell或python解释器)去完成任务。
它更侧重于工作流的编排和知识的传递。
这使得Skills非常灵活,任何人都可以轻松地创建一个新的Skill来封装一个特定的工作流程,而无需去部署和维护一个专门的服务器。
因此,两者并非竞争关系,而是完美的互补。
在一个成熟的AI代理生态中,MCP服务器和Skills各司其职,相得益彰。
各个业务团队,比如人力资源团队、IT团队、财务团队,可以各自负责构建和维护自己领域的MCP服务器,将他们稳定可靠的核心业务能力(如创建员工档案、分配设备、查询报销状态等)以结构化的方式暴露出来。
这些MCP服务器是整个生态的基石,提供了坚实的原子能力。
而Skills则扮演着流程编排者和知识胶水的角色。
业务分析师、产品经理,甚至普通用户,都可以创建Skills来编排和组合来自不同MCP服务器的原子能力,形成面向具体应用场景的、端到端的复杂工作流。
例如,一个名为新员工入职流程的Skill,它的说明书可能会这样指导代理:调用HR系统的MCP工具hr_create_employee_profile来创建员工档案,调用IT系统的MCP工具it_assign_new_laptop来分配一台新笔记本电脑,调用办公软件的MCP工具slack_send_welcome_message来向新员工发送欢迎消息和入职指引。
在这个过程中,Skill本身没有提供任何新的硬功能,但它将多个原子能力串联起来,形成了一个有价值的、完整的业务流程。
它教会了代理如何做,而MCP服务器则为代理提供了能做什么的基础。
因此,正确的策略是拥抱两者,让MCP服务器提供稳定可靠的底层能力,让Skills负责灵活多变的上层编排。
构建MCP服务器的本质,是为AI代理设计一个清晰、高效的用户界面。
你的思维模式,从根本上决定了你的AI代理的智能水平。
当我们不再将代理视为一个简单的API消费者,而是将其看作一个独特的、需要被精心服务的非人类用户时,我们才能真正释放它的潜力。
这六项最佳实践,结果导向、扁平化参数、指令即上下文、无情精简、为发现命名、以及分页大结果,共同构成了一套完整的设计哲学。
它指引我们从代理的视角出发,去构建一个让它们感到舒适、易于理解和高效工作的环境。
而通过将MCP的结构化能力与Skills的灵活动态编排相结合,我们将能够构建出前所未有的、真正智能和强大的AI应用生态。
参考资料:
https://www.philschmid.de/mcp-best-practices
https://www.philschmid.de/philipp-schmid