029、MCP Resources 与 Prompts 开发实战

029、MCP Resources 与 Prompts 开发实战

上周五凌晨两点,我在调试一个Claude Code的MCP Server时,发现Resources接口返回的数据总是被截断。日志里看到response body长度只有4096字节,但我的数据库查询结果明明有2万行。翻遍MCP协议文档,才发现Resources的uri参数有个隐藏的size字段控制分页——这个坑让我折腾了三个小时。今天就把Resources和Prompts这两个MCP核心原型的实战经验掰开揉碎讲清楚。

Resources:不只是文件系统

很多人把MCP Resources理解成“读取文件”,这格局小了。Resources本质是结构化数据源的抽象层,可以是数据库表、API响应、甚至是实时计算的中间结果。Claude Code通过resources/readresources/list两个方法交互,但真正决定开发效率的是URI设计。

URI命名规范(踩过坑才懂)

别学官方示例那样用file:///logs/app.log这种扁平URI。生产环境我推荐三级命名法:

mcp://{provider}/{resource_type}/{identifier}

比如我负责的监控系统:

mcp://prometheus/metrics/cpu_usage
mcp://prometheus/alerts/high_memory
mcp://postgres/tables/orders

这样设计的好处是:resources/list返回的树形结构天然支持按provider和type过滤。Claude Code的自动补全会根据前缀提示可用资源,用户体验直接拉满。

分页与增量更新(血泪教训)

Resources的read方法返回contents数组,但每个content有大小限制。我踩过的坑是:默认不启用分页。必须显式在read请求的params里传sizecursor

// 别这样写——直接返回全部数据
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const data = await fetchAllFromDB(); // 2万行数据直接炸
  return { contents: [{ uri: request.params.uri, text: JSON.stringify(data) }] };
});

// 正确姿势:支持游标分页
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri, size = 1000, cursor } = request.params;
  const { rows, nextCursor } = await fetchPaginated(uri, size, cursor);
  return {
    contents: [{ uri, text: JSON.stringify(rows) }],
    nextCursor // 告诉Claude Code还有更多数据
  };
});

这里有个细节:nextCursor必须是字符串,不能是数字。我一开始传了数字索引,结果Claude Code解析报错,排查半天才发现协议要求string类型。

订阅机制:实时数据的关键

Resources支持subscribeunsubscribe,但很多开发者忽略了这个能力。如果你的数据源是实时变化的(比如日志流、监控指标),一定要实现订阅。

// 订阅处理逻辑
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
  const { uri } = request.params;
  // 这里踩过坑:必须返回空对象,不能返回undefined
  // 否则Claude Code会认为订阅失败
  startPolling(uri, (newData) => {
    // 通过notification通知客户端
    server.sendNotification({
      method: "notifications/resources/updated",
      params: { uri }
    });
  });
  return {}; // 必须显式返回空对象
});

注意:notifications/resources/updated通知只传URI,不传数据。Claude Code收到通知后会主动调用read获取最新数据。这个设计是为了避免通知体过大,但代价是增加了一次网络往返。

Prompts:让Claude Code学会提问

Prompts是MCP里最容易被低估的能力。它定义了Claude Code可以“主动问”用户什么信息。比如一个代码审查工具,需要用户选择审查级别(严格/宽松),或者一个部署工具,需要用户确认环境。

Prompt模板设计原则

Prompts的核心是prompts/get方法,返回一个messages数组。每个message可以是userassistantsystem角色。但实战中我发现:不要把所有逻辑塞进一个prompt

// 反面教材:一个prompt干所有事
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  return {
    messages: [
      { role: "system", content: { type: "text", text: "你是代码审查助手..." } },
      { role: "user", content: { type: "text", text: "请审查以下代码..." } }
    ]
  };
});

// 正确做法:拆分成多个prompt,每个专注一个场景
const prompts = {
  "review-code": {
    name: "审查代码",
    description: "对指定代码文件进行审查",
    arguments: [
      { name: "filePath", description: "文件路径", required: true },
      { name: "severity", description: "审查严格程度", required: false }
    ]
  },
  "deploy-service": {
    name: "部署服务",
    description: "部署服务到指定环境",
    arguments: [
      { name: "env", description: "环境名称", required: true },
      { name: "version", description: "版本号", required: true }
    ]
  }
};

每个prompt的arguments字段是关键。Claude Code会根据这些参数自动生成交互式表单,用户填写后触发prompts/get请求。参数类型目前只支持string,但可以通过description暗示格式(比如“格式:YYYY-MM-DD”)。

动态prompt:根据上下文调整

静态prompt模板不够灵活。我常用的模式是:在prompts/get处理函数里根据参数动态构建prompt内容。

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  
  if (name === "review-code") {
    const fileContent = await readFile(args.filePath);
    const severity = args.severity || "normal";
    
    // 根据文件类型调整审查重点
    const language = detectLanguage(args.filePath);
    const focusAreas = getFocusAreas(language, severity);
    
    return {
      messages: [
        {
          role: "system",
          content: {
            type: "text",
            text: `你正在审查${language}代码。重点关注:${focusAreas.join("、")}。严格程度:${severity}`
          }
        },
        {
          role: "user", 
          content: {
            type: "text",
            text: `请审查以下代码:\n\`\`\`${language}\n${fileContent}\n\`\`\``
          }
        }
      ]
    };
  }
});

这里有个坑:arguments参数名必须和prompts/list里定义的一致,大小写敏感。我因为把filePath写成filepath,导致Claude Code传参时一直报错。

Prompt与Resources的联动

高级玩法是把Resources和Prompts结合起来。比如一个prompt需要读取某个资源的数据作为上下文:

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  if (request.params.name === "analyze-metrics") {
    // 先读取资源数据
    const metricsData = await server.handleRequest({
      method: "resources/read",
      params: { uri: "mcp://prometheus/metrics/cpu_usage", size: 100 }
    });
    
    return {
      messages: [
        {
          role: "system",
          content: { type: "text", text: "你是一个性能分析专家" }
        },
        {
          role: "user",
          content: { type: "text", text: `分析以下CPU指标:\n${metricsData.contents[0].text}` }
        }
      ]
    };
  }
});

但注意:不要在prompt处理函数里直接调用resources/read,这会导致循环依赖。正确做法是通过server实例的handleRequest方法,或者把资源读取逻辑抽成独立函数。

实战经验总结

  1. Resources的URI设计决定可扩展性:用三级命名法,别偷懒。我见过一个项目把所有资源都叫data,后来维护成本爆炸。

  2. 分页是必选项而非可选项:即使你的数据现在很小,也要实现分页。Claude Code的上下文窗口有限,一次性返回太多数据会导致对话卡死。

  3. Prompts的arguments要精简:每个prompt的参数不要超过3个,否则用户填写体验很差。复杂场景拆成多个prompt串联。

  4. 错误处理要细致:Resources和Prompts的请求都可能失败。返回错误时用MCP定义的错误码,比如-32602表示参数无效,-32603表示内部错误。别自己发明错误码。

  5. 日志记录要包含URI:调试时最痛苦的是不知道哪个资源出了问题。每个请求都打日志,带上uriname字段。

  6. 测试要覆盖边界情况:空数据、超大响应、参数缺失、并发请求——这些场景都要写测试。我吃过亏:一个prompt在参数为空时返回了undefined,导致Claude Code整个会话崩溃。

最后说个个人习惯:我会在MCP Server启动时打印所有注册的Resources和Prompts列表,方便快速验证。这个简单的做法帮我省了无数排查时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值