MCP 最佳实践
从减少日志与数据量、写好 Description、区分服务原语到兜底处理,总结开发 MCP Server 时反复踩过的经验。
1. Stdio 模式下减少命令行日志
🖼️ 配图待补:GOdtbbXH5oRjp6xVGrScbueAnyb
2. 减少数据量
Agent 调用 MCP Server 时,Server 返回的所有内容都会被纳入大语言模型(LLM)的上下文,以供后续推理步骤使用。然而,在上下文中包含过多或不相关的信息可能引发几个问题:
- 信息过载:过多的无关数据可能使模型产生混淆,有可能降低其生成回复的质量;
- 上下文长度限制:大量数据可能超过 LLM 所支持的最大上下文长度,导致会话失败或崩溃;
- 性能下降:不必要的信息会进一步加大模型有效处理上下文的难度,从而导致性能欠佳。
为降低这些风险,务必确保 MCP Server 返回的内容简洁且与当前任务直接相关。此外,任何日志或其他输出都应尽量精简,以防止上下文臃肿。
3. 清晰明确的 Description
开发 MCP 时,提供清晰、明确且信息丰富的描述是至关重要的。LLM 调用 MCP 的前提是它必须对 MCP 有深入而充分的理解,包括 MCP 的功能、提供的能力以及支持的参数,而这些信息均通过各个描述节点传递。因此在开发过程中,必须确保配置正确且清晰的描述。例如:
🖼️ 配图待补:RiEPbASqRoV7ZrxpuDdcUOmrnji
目前体验下来,MCP 机制存在一个显著问题:不稳定性——或者说,所有 LLM 应用都必然存在因随机带来的不稳定问题。体感上,即使在相同的上下文和相同的提示下,LLM 也不一定每次都能正确调用 MCP。要提高 MCP 调用的准确性,目前唯一可行的方法是通过不断优化描述来实现。
具体来说,我们可以在 Description 中包含若干关键要素。对于 Server / Tool / Prompt / Resource 级别的描述,建议明确:
- 作用(Purpose):明确说明该组件的功能和用途。例如,"该工具允许 LLM 执行网络搜索,以获取模型训练数据中未包含的最新信息。";
- 调用时机(Invocation Timing):指定在何种情况下应调用该组件。例如,"当用户查询需要实时数据(如股票价格或最新新闻)时,必须调用该工具。";
- 能力(Capabilities):详细描述该组件能够实现的功能。例如,对于工具,可能包括支持的查询类型、返回结果的格式;对于资源,可能包括提供的数据类型;对于提示,可能包括生成内容的类型。
而对于各工具请求参数的 Description,建议明确:
- 含义:解释参数的含义及其在调用中的作用。例如,"query:用户希望搜索的文本内容。";
- 示例(Examples):提供参数的有效值示例,以帮助理解和正确使用。例如,"query:'current stock price of Apple'"。
4. 区分服务类型
MCP 协议定义了三种服务原语,它们在语义与操作层面都有些差异,务必根据具体场景选择合适的原语:
| 原语 | 语义 | 副作用 | 行为 |
|---|---|---|---|
| Resources | 用于读取资源 | 无 | 示例:读文件、读数据库 |
| Prompt | 语义上代表读取一种特殊资源:Prompt | 无 | MCP Client 获取结果后会根据 Prompt 内容进一步执行操作。示例:用于生成 Cursor Rule 的元 Prompt、用于根据上下文规划任务执行路径的元 Prompt |
| Tools | 执行某些可能产生副作用的操作 | 有 | MCP 协议规定,执行 Tool 前需要向用户发出申请,审核通过后才能执行。示例:写文件、写数据库 |
5. 始终处理兜底情况
举个实际例子,假如有下面的 CallToolRequestSchema:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { toolId, arguments: rawArgs = {} } = request.params;
const args = rawArgs as ToolArgs;
printLog("收到工具调用请求:", toolId, args);
// 根据工具ID处理请求
switch (toolId) {
case "add": {
...
}
}
});若此时客户端错误调用了非 add 的 Tool,不会出现报错,但客户端会卡死在请求中直至超时。这是因为 MCP 协议并不能理解程序已经进入 IDLE 状态、迟迟没有发送响应信息,Client 只能空等直至超时。
因此,编写 Tools / Prompts / Resources 时,无论何时都必须返回结果,即使这是一个空结果,例如:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { toolId, arguments: rawArgs = {} } = request.params;
const args = rawArgs as ToolArgs;
printLog("收到工具调用请求:", toolId, args);
// 根据工具ID处理请求
switch (toolId) {
case "add": {
}
default: {
return {
content: [{ type: "text", text: `未找到ID为"${toolId}"的工具` }],
isError: true,
};
}
}
});