关于我
MCP 全解:从理解到深度开发

MCP 最佳实践

从减少日志与数据量、写好 Description、区分服务原语到兜底处理,总结开发 MCP Server 时反复踩过的经验。

1. Stdio 模式下减少命令行日志

🖼️ 配图待补:GOdtbbXH5oRjp6xVGrScbueAnyb

2. 减少数据量

Agent 调用 MCP Server 时,Server 返回的所有内容都会被纳入大语言模型(LLM)的上下文,以供后续推理步骤使用。然而,在上下文中包含过多或不相关的信息可能引发几个问题:

  1. 信息过载:过多的无关数据可能使模型产生混淆,有可能降低其生成回复的质量;
  2. 上下文长度限制:大量数据可能超过 LLM 所支持的最大上下文长度,导致会话失败或崩溃;
  3. 性能下降:不必要的信息会进一步加大模型有效处理上下文的难度,从而导致性能欠佳。

为降低这些风险,务必确保 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语义上代表读取一种特殊资源:PromptMCP 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,
      };
    }
  }
});

On this page