AI TOOLS

EP10. "MCP 客户端实现 + Resources 与 Prompts"

首页 › AI 工具 › Claude · Developer and Platform · Claude API · EP10
约 17 分钟· #EP10#Claude#Claude API
🔒 登录后可标记已读
  • MCP client 由两部分组成:自己写的 MCPClient 类(负责业务逻辑)+ SDK 提供的 Client Session(负责真正管连接、要做好资源清理)
  • MCP 除了「工具」(Tool,做动作)还有另外两种能力:Resources(暴露数据,像 HTTP 的 GET)和 Prompts(预先写好、调好的指令模板)——三者用途不一样,不要混着用
  • Resources 分两种:Direct(固定 URI,比如 docs://documents)跟 Templated(带参数的 URI,比如 docs://documents/{doc_id}),后者 SDK 会自动帮你解析参数
  • Prompts 让 MCP server 作者把"调好的高质量指令模板"直接交给 client 用,用户不用自己从零现写 prompt,官方说法是这样"效果更一致、质量更高"
  • 前置知识:读过 [EP09](./EP09_mcp服务端开发基础.md)(MCP 基本概念、FastMCP、@mcp.tool、Inspector),这篇笔记接着讲 client 端实现 + Resources/Prompts 这两个额外能力

重点内容


实现 MCP Client

MCP client 由两个部分组成:

  • 自己写的 MCPClient 类,包着业务逻辑
  • SDK 提供的 Client Session,真正负责管理跟 server 的连接,用完要做好资源清理(一般靠 async with 处理)

📌 再次强调这条架构原则:"实际项目里,通常只会实现 MCP client 或 MCP server 其中一个,不会两个都做。"——课程两个都写只是为了完整演示互动关系。

Client 承担两件核心工作:

  1. 拿到可用工具列表,传给 Claude
  2. Claude 要求调用某个工具时,实际去执行

list_tools 方法:直接呼叫 session 内建方法,把结果里的 tools 数组返回:

async def list_tools(self) -> list[types.Tool]:
    result = await self.session().list_tools()
    return result.tools

call_tool 方法:接收工具名称和参数字典(来自 Claude 的请求),返回 server 端的执行结果:

async def call_tool(
    self, tool_name: str, tool_input: dict
) -> types.CallToolResult | None:
    return await self.session().call_tool(tool_name, tool_input)

测试写法:用 async context manager 连上 server,直接呼叫 client 方法验证:

async with MCPClient(
    command="uv", args=["run", "mcp_server.py"]
) as client:
    result = await client.list_tools()
    print(result)

完整应用流程:client 拿到工具定义 → 传给 Claude → Claude 要求调用时,client 透过 server 真正执行 → 把结果传回给 Claude 继续处理。


Resources:暴露数据,不是做动作

Resources 是 MCP server 用「请求-响应」模式,把数据暴露给 client 的另一种机制——概念上更接近 HTTP server 里的 GET handler,专门用来拿信息,不是用来执行动作(执行动作是 Tool 的职责)。

两种 Resource 类型:

类型URI 写法特点
Direct Resourcedocs://documents固定 URI,不带参数
Templated Resourcedocs://documents/{doc_id}URI 里带参数,Python SDK 自动解析、当函数参数传进去

Direct Resource 示范:

@mcp.resource(
    "docs://documents",
    mime_type="application/json"
)
def list_docs() -> list[str]:
    return list(docs.keys())

Templated Resource 示范:

@mcp.resource(
    "docs://documents/{doc_id}",
    mime_type="text/plain"
)
def fetch_doc(doc_id: str) -> str:
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

几个技术细节:

  • @mcp.resource() 装饰器同时定义 URI 和 MIME type
  • 常见 MIME type 有 application/json、text/plain,也能用其他合法格式
  • SDK 会自动处理序列化,不用自己手动转 JSON
  • 一样可以用 uv run mcp dev mcp_server.py 起 MCP Inspector 来测试

💡 实用场景:文档「@ 提及」功能——用户打 @ 时列出可用文档(对应 Direct Resource),选中某份文档后,把它的内容注入 prompt(对应 Templated Resource),两种 Resource 配合着用。


Client 端读取 Resource

Resource 的价值在于:server 能把数据直接塞进 prompt 里,不用像 Tool 那样绕一圈"调用-返回"的流程,给 Claude 提供上下文更直接。

Client 在中间做的事:处理跟 MCP server 的通讯、自动解析数据。用户在输入框打出 "@report.pdf" 这类引用时,应用会透过 MCP client 抓这份 Resource,把内容塞进要发给 Claude 的 prompt 里。

核心函数 read_resource:

async def read_resource(self, uri: str) -> Any:
    result = await self.session().read_resource(AnyUrl(uri))
    resource = result.contents[0]

回应里的 contents 是个列表,一般只需要取第一个元素。

根据 MIME type 处理不同内容类型:

if isinstance(resource, types.TextResourceContents):
    if resource.mimeType == "application/json":
        return json.loads(resource.text)

    return resource.text

Resource 可能返回不同格式的内容,靠 MIME type 判断该用什么方式 parse。

要用到的 import:

import json
from pydantic import AnyUrl

测试方式:用户输入类似 "What's in the @report.pdf document?" 这种带 @ 引用的问句,触发自动补全 → 选中对应 Resource → 自动抓取内容 → 塞进 prompt——整个过程完全不需要走 Tool Call。


Prompts:预先写好、调好的指令模板

MCP server 上的 Prompts,让 server 作者能定义"预先写好、经过打磨的高质量指令模板",client 直接拿来用,不用自己从零现写 prompt。

📌 官方说法:"用户理论上自己也能完成这些任务,但用 MCP server 作者精心开发、测试过的 prompt,通常能拿到更一致、更高质量的结果。"

Prompt 的运作方式:函数返回一份消息列表,直接供 Claude 使用。要用到:

  • 引入基础消息类型:from mcp.server.fastmcp import base
  • 用 @mcp.prompt() 装饰器,带上 name 和 description
  • 函数返回一份组成完整 prompt 的消息列表

示范:文档格式化 Prompt:

@mcp.prompt(
    name="format",
    description="Rewrites the contents of the document in Markdown format."
)
def format_document(
    doc_id: str = Field(description="Id of the document to format")
) -> list[base.Message]:
    prompt = f"""
Your goal is to reformat a document to be written with markdown syntax.

The id of the document you need to reformat is:

{doc_id}


Add in headers, bullet points, tables, etc as necessary. Feel free to add in extra formatting.
Use the 'edit_document' tool to edit the document. After the document has been reformatted...
"""

    return [
        base.UserMessage(prompt)
    ]

写 Prompt 的最佳实践:

  • 聚焦在跟这个 server 核心用途相关的任务上
  • 指令要写得详细、具体
  • 用各种不同输入充分测试
  • 附上清楚的、给用户看的说明(description)
  • 考虑跟 server 自己的 tools、resources 怎么配合使用(比如上面这个例子最后一步就叫 Claude 用 edit_document 这个 tool)

Client 端使用 Prompts

列出所有可用 Prompt:

async def list_prompts(self) -> list[types.Prompt]:
    result = await self.session().list_prompts()
    return result.prompts

取出某个具体 Prompt(带参数内插):

async def get_prompt(self, prompt_name, args: dict[str, str]):
    result = await self.session().get_prompt(prompt_name, args)
    return result.messages

参数怎么传:Server 端的 prompt 函数本身可以接参数——比如上面那个文档格式化 prompt 就要一个 doc_id。Client 调 get_prompt 时,args 字典里要带上对应的 key,MCP server 会把这些值当关键字参数传给 prompt 函数。

CLI 测试流程,三步:

  1. 用户选一个 prompt(比如 "format")
  2. 系统询问这个 prompt 需要的参数
  3. 把内插好参数的完整 prompt 发给 Claude

最佳实践总结:Prompt 要跟 server 本身的用途相关、要充分测试过、写清楚、围绕 server 已有的 tools 设计、同时要考虑用户需要填哪些参数。

常见错误

  • ❌ 把 Resource 当 Tool 用——要"拿数据"该用 Resource(像 GET),需要"执行动作/产生副作用"才该用 Tool;用错类型会让架构变得混乱
  • ❌ Client Session 用完不做资源清理——用 async with 管好生命周期,避免连接泄漏
  • ❌ 处理 Resource 内容时没检查 MIME type 就直接当纯文字处理——application/json 类型的内容要先 json.loads(),不能直接当字符串用
  • ❌ Prompt 函数需要的参数,client 端 get_prompt 没有对应传全——参数名对不上,server 端拿不到预期的值,模板内插会出错
  • ❌ 以为一个项目要同时把 client 和 server 都做全——真实项目通常只做一边,另一边用现成/第三方的实现就够了

Sources

官方文档:

  1. Building with the Claude API(Claude Academy 课程)— https://academy.claude.com/courses/building-with-the-claude-api