AI TOOLS

EP01. "MCP 入门:用 Python SDK 打造你的第一个 MCP Server"

首页 › AI 工具 › Claude · Developer and Platform · MCP Intro · EP01
约 29 分钟· #EP01#Claude#MCP Intro
🔒 登录后可标记已读
  • 接 GitHub 这类外部服务,如果不用 MCP,光是 repos、PR、issues、projects 每一类都要自己写 tool schema——Introduction to Model Context Protocol 这门官方课(academy.claude.com,10 节课 + 测验,约 1 小时)就是教怎么用 MCP 省掉这一大堆重复劳动
  • MCP(Model Context Protocol)是一层通信协议:Claude 要用工具、要读数据、要跑预建流程,都不用你手写整合代码,交给专门的 MCP Server 处理
  • 课程用 Python 官方 SDK,带你从零搭一个「文件管理系统」的 MCP Server + Client,依序涵盖三大组件——Tools(模型自己决定要不要调用)、Resources(App 决定何时抓取)、Prompts(用户主动触发)
  • 前置知识:基础 Python、了解 async/await、知道 API 是什么概念

重点内容


MCP 解决的问题

设想一个场景:做一个聊天界面,用户想问 Claude 关于 GitHub 仓库的问题。

  • 不用 MCP 的做法:GitHub 功能太多(repos、pull requests、issues、projects……),每一项都要自己写 tool schema、写执行函数、测试、维护,工作量随功能数量线性爆炸
  • MCP 的做法:把「定义 tool + 执行 tool」这件事的责任,从你的应用服务器转移给专门的 MCP Server。你不用自己写 GitHub 的 tools,直接用别人(通常是服务商自己)写好的 GitHub MCP Server,里面已经封装好 get_repos() 这类标准化工具,直接连 GitHub API

📌 MCP Server 是谁写的?通常服务商会官方发布(例如 AWS 就发布了自己的官方 MCP Server)。它跟直接调用 API 的差别在于:MCP Server 把 tool schema 和执行函数都预先定义好了,你不用自己动手写。它跟「tool use」也不是一回事——MCP Server 负责提供已经定义好的工具,tool use 讲的是 Claude 具体怎么去调用这些工具,两者是互补关系。


架构总览:一次调用怎么在 Client 和 Server 之间跑

MCP Client(在你的应用里)和 MCP Server 之间靠传输层无关的方式通信——本地场景最常见的是标准输入输出(stdio),也支持 HTTP、WebSocket 等网络协议。

两种核心消息往返:

消息作用
ListToolsRequest / ListToolsResultClient 问 Server「你有哪些工具」,拿到工具清单
CallToolRequest / CallToolResultClient 请 Server 执行某个工具(带参数),拿到执行结果

以「用户问 GitHub 仓库问题」为例,完整走一遍:

  1. 用户把问题发给你的服务器
  2. 服务器要先知道有哪些工具可用
  3. 服务器通过 MCP Client 去问
  4. MCP Client 发 ListToolsRequest 给 MCP Server
  5. Claude 收到用户问题 + 可用工具清单
  6. Claude 决定要用哪个工具
  7. 服务器请 MCP Client 执行这个工具
  8. MCP Client 发 CallToolRequest 给 MCP Server(Server 内部去调 GitHub API)
  9. GitHub 把数据回给 Server
  10. 结果包成 CallToolResult 往回传
  11. Claude 根据结果组织最终答案
  12. 用户收到回复

📌 MCP Client 把这整条链路的复杂度都包起来了——写应用的人只需要专心处理业务逻辑,不用管服务器通信细节。


动手写 Tools:MCP Server 端

课程用 Python 官方 SDK 的 FastMCP 类,靠装饰器 + Python 类型提示定义工具,不用手写 JSON schema:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("DocumentMCP", log_level="ERROR")

示范用的文档存储(一个简单字典):

docs = {
    "deposition.md": "This deposition covers the testimony of Angela Smith, P.E.",
    "report.pdf": "The report details the state of a 20m condenser tower.",
    "financials.docx": "These financials outline the project's budget and expenditures",
    "outlook.pdf": "This document presents the projected future performance of the system",
    "plan.md": "The plan outlines the steps for the project's implementation.",
    "spec.txt": "These specifications define the technical requirements for the equipment"
}

读文档的工具:

@mcp.tool(
    name="read_doc_contents",
    description="Read the contents of a document and return it as a string."
)
def read_document(
    doc_id: str = Field(description="Id of the document to read")
):
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")

    return docs[doc_id]

编辑文档的工具:

@mcp.tool(
    name="edit_document",
    description="Edit a document by replacing a string in the documents content with a new string."
)
def edit_document(
    doc_id: str = Field(description="Id of the document that will be edited"),
    old_str: str = Field(description="The text to replace. Must match exactly, including whitespace."),
    new_str: str = Field(description="The new text to insert in place of the old text.")
):
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")

    docs[doc_id] = docs[doc_id].replace(old_str, new_str)

用这套 SDK 的好处:不用手写 JSON schema;类型提示自动做参数校验;Field(description=...) 里的说明文字帮 Claude 理解每个参数该怎么填;Python 的 raise/ValueError 直接就是错误处理;装饰器一贴,工具自动注册,不用另外维护清单。


用 Inspector 测试 Server(不用先接完整 App)

MCP Inspector 是 Python SDK 自带的浏览器端调试工具,不用搭好整个应用就能单独测 Server。

  1. 启动:mcp dev mcp_server.py,本地跑在 http://127.0.0.1:6274
  2. 点 Connect 按钮,状态从 disconnected 变成可用
  3. 切到 Tools 分页,点 List Tools 看到所有已注册工具
  4. 选一个工具,填测试参数(比如文档 id),点 Run Tool
  5. 面板直接显示执行结果和成功与否

💡 Inspector 会保留 Server 状态,可以连续测(比如先编辑文档、再读一次确认改动生效),适合边写边验证复杂的工具串联逻辑。


把 Server 接进 Client

Client 端由两部分组成:自己写的 MCP Client 类 + SDK 提供的 ClientSession(负责管理连接,要留意资源要正确释放)。

Client 要做两件事:把工具清单转给 Claude、Claude 要用工具时负责去执行。

async def list_tools(self) -> list[types.Tool]:
    result = await self.session().list_tools()
    return result.tools
async def call_tool(self, tool_name: str, tool_input: dict) -> types.CallToolResult | None:
    return await self.session().call_tool(tool_name, tool_input)

测试:uv run mcp_client.py 先确认连接和工具清单正常,再跑 uv run main.py 测完整流程(Client 接上应用逻辑,Claude 能实际调用工具)。


Resources:只读数据怎么定义

Resources 的作用类似 HTTP 的 GET 请求——暴露只读数据,不像 Tools 要 Claude 自己判断该不该调用。

典型场景:让用户打 @文档名 引用一份文档。这需要两个操作:列出可选文档(给自动补全用)、拿到某份文档的具体内容。用户一旦 @ 了某份文档,内容直接塞进发给 Claude 的 prompt 里,Claude 不用再另外调工具去拿。

请求流程:Client 发 ReadResourceRequest(带一个 URI 表示要哪份资源)→ Server 处理这个 URI、跑对应函数 → Server 用 ReadResourceResult 把结果传回来。

Direct Resources(静态 URI,不带参数):

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

Templated Resources(URI 里带参数,SDK 自动解析成关键字参数传进函数):

@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]

📌 mime_type 一定要设对(application/json、text/plain、application/pdf 等),告诉 Client 该怎么解析这份数据;SDK 会自动帮你序列化返回值,不用手动转 JSON。

测试同样用 uv run mcp dev mcp_server.py 打开 Inspector,这次看 Resources(静态资源)和 Resource Templates(带参数的资源)两个分区,Templated 的要先填参数才能测。


Client 端怎么读 Resource

import json
from pydantic import AnyUrl

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

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

    return resource.text

Server 回来的 contents 是个列表,第一项包含实际内容、MIME type、和一些元数据。逻辑很简单:MIME type 是 application/json 就解析成 Python 对象,其他情况直接当纯文字返回。

用户端体验:在 CLI 打 @,自动列出可用资源,方向键选、空格确认,内容直接塞进 prompt,一次性连同问题发给 Claude——中间不用再额外跑一次 tool call。


Prompts:预建的专家级指令

用户当然可以直接跟 Claude 描述想做什么,但如果 MCP Server 的作者已经把某个任务的 prompt 反复测试、打磨过,效果会比用户现场现写好很多——这是 Prompts 这个组件存在的理由。

范例场景:用户打 /format doc_id,触发一个预建好的「重新排版成 Markdown」流程:

@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:
<document_id>
{doc_id}
</document_id>

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

    return [
        base.UserMessage(prompt)
    ]

函数返回的是一串 message 列表,会直接发给 Claude——可以塞多条 user/assistant 消息,做更复杂的多轮铺垫。同样可以先用 Inspector 测试,它会显示变量替换后完整的 prompt 内容,上线前就能确认没写错。

好处:结果稳定一致;把专业经验/边界情况处理编码进 prompt 里;多个地方都能重复用同一份;要改进只需要改 Server 这一处,所有用它的 Client 都同步受益。


Client 端怎么用 Prompts

async def list_prompts(self) -> list[types.Prompt]:
    result = await self.session().list_prompts()
    return result.prompts
async def get_prompt(self, prompt_name, args: dict[str, str]):
    result = await self.session().get_prompt(prompt_name, args)
    return result.messages

例如 format_document 这个 prompt 需要 doc_id 参数,Client 传 {"doc_id": "plan.md"} 进去,服务器把这个值插进模板里再回传。用户端打 / 就能看到所有可用 prompt 当作指令来选,选中后系统还会引导补上需要的参数(比如选哪份文档),组装完整后一次发给 Claude。


三大组件怎么选

组件谁决定要不要用适合的场景
ToolsClaude 自己(模型判断这个任务需不需要调用)扩展 Claude 能做的事——比如帮它接一个能实际算平方根的 JS 执行器
Resources你的应用代码(App 决定什么时候抓、怎么用)填充 UI 里的自动补全、给对话补充背景资料
Prompts用户(点按钮、选菜单、打 slash command 主动触发)让用户一键跑一套已经调好、可重复用的高质量流程

学完你会

  • ✅ 讲得清 MCP 为什么存在、它怎么省掉手写整合代码的功夫
  • ✅ 用 Python SDK 的装饰器写出 Tools、Resources(含 Direct 和 Templated 两种)、Prompts 三种组件
  • ✅ 用 MCP Inspector(mcp dev)独立调试 Server,不用先搭好整个 App
  • ✅ 在 Client 端接好 list_tools/call_tool/read_resource/list_prompts/get_prompt,让 Claude 真正用上这些工具
  • ✅ 遇到新需求,能判断该做成 Tool、Resource 还是 Prompt

常见错误

  • ❌ 沿用旧习惯手写 JSON schema 定义工具——Python SDK 的装饰器 + 类型提示 + Field(description=...) 已经帮你自动生成,不用再手刻一遍
  • ❌ 把该由 App 主动抓取的数据(比如自动补全清单)做成 Tool,指望 Claude 自己想到要调用——这种场景应该用 Resource,由你的代码决定什么时候取用,不是丢给模型猜
  • ❌ 定义 Resource 时漏填或填错 mime_type——Client 靠这个字段判断内容该当 JSON 解析还是当纯文字处理,填错会导致 Client 端解析出错
  • ❌ 把 Prompt 当成「省事的固定模板」随便写写——它的价值在于经过反复测试、覆盖边界情况,写得越粗糙,跟用户自己现场描述需求的差距就越小,等于白做
  • 💡 还没搭好完整应用之前,先用 mcp dev mcp_server.py 开 Inspector 单独测 Server 端的 Tools/Resources/Prompts,比每次都跑一整套 Client 快很多

Sources

官方文档:

  1. Introduction to Model Context Protocol(课程总览)— https://academy.claude.com/courses/introduction-to-model-context-protocol
  2. Introducing MCP — https://academy.claude.com/courses/introduction-to-model-context-protocol/introducing-mcp
  3. MCP clients — https://academy.claude.com/courses/introduction-to-model-context-protocol/mcp-clients
  4. Defining tools with MCP — https://academy.claude.com/courses/introduction-to-model-context-protocol/defining-tools-with-mcp
  5. The server inspector — https://academy.claude.com/courses/introduction-to-model-context-protocol/the-server-inspector
  6. Implementing a client — https://academy.claude.com/courses/introduction-to-model-context-protocol/implementing-a-client
  7. Defining resources — https://academy.claude.com/courses/introduction-to-model-context-protocol/defining-resources
  8. Accessing resources — https://academy.claude.com/courses/introduction-to-model-context-protocol/accessing-resources
  9. Defining prompts — https://academy.claude.com/courses/introduction-to-model-context-protocol/defining-prompts
  10. Prompts in the client — https://academy.claude.com/courses/introduction-to-model-context-protocol/prompts-in-the-client
  11. MCP review — https://academy.claude.com/courses/introduction-to-model-context-protocol/mcp-review