🎓 Anthropic 学院 · 中文学习版 · 完全免费

MCP 进阶专题

MCP: Advanced Topics

高级4 个模块14 节课全中文讲解

这门进阶课假设你已经会构建基本的 MCP 服务器,深入生产级主题:采样、通知系统、Roots 文件访问,以及不同的传输机制与部署考量。

在互动学院中学习(含测验与进度保存)→

模块一 · 介绍 Introduction

开始吧

这门进阶课假设你已经会构建基本的 MCP 服务器。我们将深入生产级主题:采样(sampling)、通知、Roots 文件访问,以及不同的传输机制。

你将深入的主题

  • Sampling:让服务器借用客户端的模型能力,把 AI 成本和复杂度留在客户端
  • 日志与进度通知:为长任务提供实时反馈
  • Roots:受控的文件系统访问,划定安全边界
  • 传输:STDIO 与 StreamableHTTP 两种机制
  • 生产部署:有状态 vs 无状态、横向扩展的取舍
🧠 自测:这门进阶课最适合谁?
  1. 完全没写过代码的人
  2. 已能构建基本 MCP 服务器、想上生产环境的开发者 ✓
  3. 只用聊天的普通用户
  4. 产品经理

进阶课面向已掌握 MCP 基础、需要处理采样、通知、传输和部署等生产级问题的开发者。

模块二 · 核心特性 Core MCP features

Sampling

Sampling 是 MCP 中最独特的机制之一:它让服务器可以反向请求客户端去调用 LLM。理解这个设计,是掌握进阶 MCP 的关键一步。

什么是 Sampling

在常规流程中,客户端调用服务器工具。而 Sampling 反转了这个方向:服务器在执行工具时,可以向客户端发送 sampling/createMessage 请求,让客户端代它调用 LLM。

  • 服务器不需要持有 API Key,模型访问权限集中在客户端
  • 模型调用的费用由客户端(用户)承担,服务器无需管理账单
  • 服务器可以在工具逻辑中借助模型能力处理复杂任务
  • 人机回路(Human-in-the-loop):客户端可以在转发请求前让用户审核

为什么这个设计很重要

这个设计解决了一个现实问题:如果每个 MCP 服务器都需要自己的 API Key,部署和费用管理会非常复杂。Sampling 让服务器保持无状态,所有 LLM 访问都通过客户端统一管控。

🧠 自测:Sampling 机制中,谁负责持有 LLM API Key 并实际调用模型?
  1. MCP 服务器
  2. MCP 客户端 ✓
  3. MCP Inspector
  4. FastMCP 框架自动管理

Sampling 的设计初衷就是让 API Key 和模型访问权限留在客户端。服务器只发出 sampling/createMessage 请求,客户端决定如何处理(包括是否让用户审核)。

Sampling 实战

理论讲完,现在写代码。本节你将实现一个使用 Sampling 的工具,以及对应的客户端处理逻辑,完整跑通整个流程。

服务器端:发起 Sampling 请求

示例 Prompt
from mcp.server.fastmcp import FastMCP, Context
from mcp.types import SamplingMessage, TextContent

mcp = FastMCP('sampling-demo')

@mcp.tool()
async def summarize_text(text: str, ctx: Context) -> str:
    '''使用 LLM 对给定文本生成摘要(通过 Sampling 机制)'''
    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role='user',
                content=TextContent(
                    type='text',
                    text=f'请用两句话概括以下内容:\n\n{text}'
                )
            )
        ],
        max_tokens=200
    )
    return result.content.text

if __name__ == '__main__':
    mcp.run()

客户端:处理 Sampling 请求

客户端需要注册一个 sampling_handler 来处理服务器发来的 Sampling 请求。这个处理器负责调用实际的 LLM API 并返回结果。

🧠 自测:在服务器端工具中发起 Sampling 请求,需要通过什么对象调用 create_message()?
  1. 直接调用 mcp.create_message()
  2. 通过 Context 参数的 session 属性调用 ✓
  3. 通过导入 anthropic 库直接调用
  4. 通过全局变量 SESSION 调用

工具函数需要声明 `ctx: Context` 参数,FastMCP 会自动注入。然后通过 `ctx.session.create_message()` 向客户端发起 Sampling 请求。

日志与进度通知

生产级的 MCP 服务器需要可观测性。日志通知和进度通知是 MCP 内置的两种机制,让客户端实时了解服务器工具的执行状态。

两种通知原语

  • 日志通知(Log Notification):发送结构化日志消息,支持 debug / info / warning / error 四个级别
  • 进度通知(Progress Notification):发送当前进度和总量,适合有明确完成标准的长任务
  • 两者都是单向推送(服务器 → 客户端),不需要客户端响应
  • 帮助开发者调试问题,帮助用户了解任务状态

日志通知

通过 `ctx.session.send_log_message()` 发送日志。客户端可以选择展示给用户、写入日志文件、或两者都做。

🧠 自测:以下哪个级别的日志通知最适合记录工具成功完成一个主要步骤?
  1. debug
  2. info ✓
  3. warning
  4. error

info 级别用于正常流程中的重要节点,如文件读取完成、API 调用成功。debug 用于更详细的调试信息,warning 和 error 用于异常情况。

通知实战

现在把日志和进度通知用到实际代码中。本节包含完整的 Python 示例,展示如何在长任务工具里添加通知,以及客户端如何接收它们。

服务器:发送日志和进度

示例 Prompt
from mcp.server.fastmcp import FastMCP, Context
import asyncio

mcp = FastMCP('notification-demo')

@mcp.tool()
async def process_files(file_count: int, ctx: Context) -> str:
    '''模拟批量处理文件,展示日志和进度通知'''
    await ctx.session.send_log_message(level='info', data=f'开始处理 {file_count} 个文件')

    for i in range(file_count):
        await asyncio.sleep(0.5)
        await ctx.session.send_log_message(level='debug', data=f'正在处理文件 {i + 1}/{file_count}')

        if ctx.meta and ctx.meta.progress_token:
            await ctx.session.send_progress(
                progress_token=ctx.meta.progress_token,
                progress=i + 1,
                total=file_count
            )

    await ctx.session.send_log_message(level='info', data=f'所有 {file_count} 个文件处理完成')
    return f'成功处理 {file_count} 个文件'

if __name__ == '__main__':
    mcp.run()

客户端:接收通知

客户端可以通过注册回调来处理服务器推送的日志和进度通知:

🧠 自测:服务器发送进度通知时,progress_token 参数的来源是?
  1. FastMCP 框架自动生成
  2. 服务器工具函数自己生成
  3. 由客户端在发起工具调用时提供 ✓
  4. 从环境变量中读取

progress_token 由客户端在调用工具时通过请求的 meta 字段传递。服务器发送进度通知时引用这个 token,让客户端能将进度更新与对应的调用关联起来。

Roots

Roots 是 MCP 的安全机制:客户端告诉服务器它被允许访问哪些文件路径,防止服务器越界读写文件系统。这对生产环境的安全至关重要。

为什么需要 Roots

没有 Roots 时,一个提供读取文件功能的 MCP 服务器理论上可以访问用户电脑上的任何文件。Roots 机制让用户能够明确声明服务器的访问边界。

  • 客户端(代表用户)声明允许访问的目录列表(Roots)
  • 服务器可以通过 roots/list 请求获知这些边界
  • 服务器有责任将所有文件操作限制在这些目录范围内
  • 典型场景:用户打开一个项目文件夹,将该文件夹作为 Root 授权给服务器
🧠 自测:MCP Roots 机制中,谁负责声明允许访问的文件路径列表?
  1. MCP 服务器
  2. MCP 客户端(代表用户) ✓
  3. FastMCP 框架自动检测
  4. 操作系统文件权限系统

Roots 由客户端声明,代表用户的意图。客户端在初始化或运行时向服务器提供允许访问的目录列表,服务器需要在自己的逻辑中遵守这些边界。

Roots 实战

把 Roots 概念落地到代码中。本节展示服务器如何请求 Roots 列表,以及如何用这些路径安全地约束文件访问。

服务器请求 Roots 并验证路径

示例 Prompt
from mcp.server.fastmcp import FastMCP, Context
from pathlib import Path

mcp = FastMCP('roots-demo')

def is_path_within_roots(file_path: str, roots: list) -> bool:
    '''检查文件路径是否在允许的 Roots 范围内'''
    target = Path(file_path).resolve()
    for root in roots:
        root_path = Path(root.uri.replace('file://', '')).resolve()
        try:
            target.relative_to(root_path)
            return True
        except ValueError:
            continue
    return False

@mcp.tool()
async def safe_read_file(file_path: str, ctx: Context) -> str:
    '''安全地读取文件,只允许访问客户端声明的 Roots 范围内的路径'''
    roots_result = await ctx.session.list_roots()
    roots = roots_result.roots

    if not roots:
        return '错误:客户端未声明任何 Root,无法访问文件'

    if not is_path_within_roots(file_path, roots):
        return f'错误:路径 {file_path} 不在允许的范围内。'

    with open(file_path, 'r', encoding='utf-8') as f:
        return f.read()

if __name__ == '__main__':
    mcp.run()

客户端声明 Roots

客户端在创建 Session 时,通过 roots 参数声明允许服务器访问的目录列表。Root 的 URI 使用 file:// 前缀的文件系统路径格式。

🧠 自测:在服务器代码中,通过什么方法获取客户端声明的 Roots 列表?
  1. mcp.get_roots()
  2. ctx.session.list_roots() ✓
  3. ctx.roots
  4. session.request_roots()

通过 `ctx.session.list_roots()` 发起 roots/list 请求,客户端返回其声明的 Root 列表。这需要工具函数声明 `ctx: Context` 参数。

模块三 · 传输与通信 Transports & communication

JSON 消息类型

MCP 底层使用 JSON-RPC 2.0 作为消息格式。理解四种消息类型,是真正掌握 MCP 通信机制的基础。

四种消息类型

  • Request(请求):带 id + method + params,期待对方响应
  • Response(响应):带相同的 id + result 或 error,是对 Request 的回答
  • Notification(通知):只有 method,没有 id,单向推送,不需要响应
  • Error(错误响应):带 id + error 对象(含 code、message、data),请求处理失败时返回
示例 Prompt
// Request — 客户端调用工具
{
  "jsonrpc": "2.0",
  "id": "req-001",
  "method": "tools/call",
  "params": { "name": "add_numbers", "arguments": { "a": 3, "b": 5 } }
}

// Response — 服务器成功响应
{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": { "content": [{ "type": "text", "text": "8" }] }
}

// Notification — 服务器推送日志(无 id)
{
  "jsonrpc": "2.0",
  "method": "notifications/message",
  "params": { "level": "info", "data": "处理完成" }
}

// Error Response — 请求处理失败
{
  "jsonrpc": "2.0",
  "id": "req-001",
  "error": { "code": -32602, "message": "参数类型错误" }
}

消息流向

消息并非只从客户端流向服务器。Sampling 请求是服务器向客户端发送的 Request;日志通知是服务器向客户端发送的 Notification;Roots 请求也是服务器向客户端发的 Request。

🧠 自测:服务器发送进度通知时,应该使用哪种 JSON-RPC 消息类型?
  1. Request(因为需要客户端确认收到)
  2. Response(作为工具调用的中间响应)
  3. Notification(单向推送,不需要响应) ✓
  4. Error(进度更新属于特殊错误类型)

进度通知是单向推送,服务器不需要等待客户端确认。Notification 类型没有 id,客户端收到后处理即可,无需回复。

STDIO 传输

STDIO 是最简单的 MCP 传输方式,也是本课程一直在用的方式。本节深入了解它的工作原理、适用场景和局限性。

STDIO 传输的工作原理

STDIO(Standard Input/Output)传输通过进程的标准输入(stdin)和标准输出(stdout)交换 JSON-RPC 消息。客户端启动服务器子进程,通过管道(pipe)通信。

  • 客户端 fork 出服务器子进程
  • 客户端向服务器的 stdin 写入 JSON 消息
  • 服务器向 stdout 写入响应消息
  • 每条消息以换行符分隔
  • 服务器的 stderr 可用于调试输出(不影响协议)

STDIO 的适用场景和局限

  • ✅ 适合:本地工具、命令行服务器、Claude Desktop 集成
  • ✅ 适合:开发和测试阶段,简单直接
  • ✅ 优点:无需网络配置,天然安全(进程间通信)
  • ❌ 局限:只支持单个客户端连接
  • ❌ 局限:不支持跨网络访问,不能部署为远程服务
🧠 自测:STDIO 传输最大的局限性是什么?
  1. 消息格式不是标准 JSON,难以调试
  2. 只支持单个客户端连接,无法跨网络部署 ✓
  3. 需要配置复杂的网络防火墙规则
  4. 只能在 Windows 系统上运行

STDIO 通过进程间通信工作,天然只支持一对一的客户端-服务器关系,且服务器进程必须在客户端机器上本地运行,无法作为远程服务部署。

StreamableHTTP 传输

StreamableHTTP 是 MCP 的网络传输方式,支持多客户端并发连接和跨网络部署。它结合了 HTTP 的通用性和 SSE 的实时推送能力。

StreamableHTTP 如何工作

  • HTTP POST:客户端向固定端点发送 JSON-RPC 请求
  • SSE(Server-Sent Events):服务器通过长连接持续推送消息给客户端
  • 支持多个客户端同时连接(不同于 STDIO 的单客户端限制)
  • 适合部署为云服务或远程 API,客户端通过 URL 连接

STDIO vs StreamableHTTP 对比

  • STDIO:本地进程通信,单客户端,简单,适合 Claude Desktop
  • StreamableHTTP:网络通信,多客户端,需要 HTTP 服务器,适合云部署
  • StreamableHTTP 支持认证(通过 HTTP 头),STDIO 无需认证(进程隔离保证安全)
  • StreamableHTTP 服务器可以水平扩展,STDIO 不行
🧠 自测:StreamableHTTP 传输使用什么机制来实现服务器向客户端的实时推送?
  1. WebSocket 双向连接
  2. HTTP 长轮询(Long Polling)
  3. SSE(Server-Sent Events)单向推送 ✓
  4. gRPC 流式传输

StreamableHTTP 使用 SSE 实现服务器到客户端的实时推送。SSE 是基于 HTTP 的单向流,服务器可以持续发送事件,无需客户端反复请求。

StreamableHTTP 深入

理论够了,来写代码。本节展示如何用 FastMCP 搭建 StreamableHTTP 服务器,以及客户端如何通过 URL 连接它。

搭建 StreamableHTTP 服务器

示例 Prompt
from mcp.server.fastmcp import FastMCP, Context
import asyncio

mcp = FastMCP('http-server')

@mcp.tool()
async def long_task(steps: int, ctx: Context) -> str:
    '''模拟长时间运行的任务,通过 SSE 推送进度'''
    for i in range(steps):
        await asyncio.sleep(1)
        await ctx.session.send_log_message(level='info', data=f'步骤 {i + 1}/{steps} 完成')
    return f'任务完成,共执行 {steps} 步'

if __name__ == '__main__':
    mcp.run(
        transport='streamable-http',
        host='0.0.0.0',
        port=8000
    )

# 启动后访问:http://localhost:8000/mcp

编写 StreamableHTTP 客户端

示例 Prompt
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    server_url = 'http://localhost:8000/mcp'

    async with streamablehttp_client(server_url) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            session.on_log_message = lambda msg: print(f'[服务器日志] {msg.data}')
            result = await session.call_tool('long_task', {'steps': 3})
            print(f'结果: {result.content[0].text}')

if __name__ == '__main__':
    asyncio.run(main())
🧠 自测:启动 StreamableHTTP 服务器后,客户端应该连接到哪个 URL 路径?
  1. /api/v1/mcp
  2. /tools
  3. /mcp(FastMCP 默认路径) ✓
  4. /rpc

FastMCP 的 StreamableHTTP 模式默认将 MCP 端点挂载在 /mcp 路径下。完整 URL 格式为 http://host:port/mcp。

状态与 StreamableHTTP

使用 StreamableHTTP 部署服务器时,你必须决定:有状态还是无状态?这个选择对系统的可扩展性和复杂度有深远影响。

有状态服务器(Stateful)

  • 优点:实现简单,状态管理由框架处理,支持复杂的多轮交互
  • 缺点:每个请求必须路由到同一台服务器实例(黏性会话)
  • 缺点:难以水平扩展——增加服务器实例不会自动分担现有会话
  • 适合场景:单机部署、开发环境、用户量小的服务

无状态服务器(Stateless)

  • 优点:可以水平扩展,负载均衡器可以将请求分发到任意实例
  • 优点:任意实例宕机不影响其他实例的正常服务
  • 缺点:需要外部状态存储(Redis、PostgreSQL 等),架构复杂
  • 适合场景:云端部署、高并发服务、需要高可用的生产环境
🧠 自测:在需要支持大量并发用户的生产环境中,StreamableHTTP 服务器推荐使用哪种模式?
  1. 有状态模式,更容易实现
  2. 无状态模式,便于水平扩展 ✓
  3. 混合模式,奇数请求有状态,偶数请求无状态
  4. STDIO 模式,性能更高

无状态模式下,负载均衡器可以将请求分发到任意服务器实例,只要外部存储(如 Redis)保存了会话状态。这使得服务可以根据负载水平扩展。

模块四 · 评估与下一步 Assessment & next steps

MCP 概念测评

这是 MCP 进阶专题的综合测评。这些问题覆盖了课程的全部核心概念,认真回答,看看你对进阶特性的掌握程度。

🧠 自测:Sampling 机制的核心目的是什么?
  1. 让服务器能够直接访问 LLM,绕过客户端
  2. 让服务器可以请求客户端代其调用 LLM,API Key 留在客户端 ✓
  3. 提高 LLM 响应的采样随机性
  4. 让多个服务器共享同一个模型连接

Sampling 的设计让服务器无需持有 LLM API Key。服务器发送 sampling/createMessage 请求给客户端,客户端负责实际调用 LLM,费用和访问控制权在用户侧。

🧠 自测:MCP Roots 机制的主要安全目标是什么?
  1. 防止客户端读取服务器的配置文件
  2. 让客户端声明服务器允许访问的文件路径范围,防止越界访问 ✓
  3. 加密客户端和服务器之间的所有通信
  4. 限制每个工具的最大执行时间

Roots 是客户端声明的文件访问边界。服务器通过 roots/list 请求获取这些边界,然后在自己的代码中验证文件路径,确保不访问用户未授权的目录。

🧠 自测:STDIO 传输和 StreamableHTTP 传输最关键的区别是什么?
  1. STDIO 使用 JSON,StreamableHTTP 使用 XML
  2. STDIO 只支持单客户端本地连接,StreamableHTTP 支持多客户端网络连接 ✓
  3. STDIO 更安全,StreamableHTTP 存在安全风险
  4. StreamableHTTP 不支持工具调用,只支持资源访问

STDIO 通过进程间通信工作,天然单客户端,适合本地工具。StreamableHTTP 基于 HTTP/SSE,支持多客户端并发连接,适合云端部署和远程访问。

🧠 自测:send_log_message 和 send_progress 两种通知的主要区别是什么?
  1. log 是双向的,progress 是单向的
  2. log 发送结构化日志(带级别),progress 发送数值进度(当前值/总量) ✓
  3. log 只能在工具中使用,progress 可以在任何地方使用
  4. 两者完全相同,只是命名不同

send_log_message 发送带 level(debug/info/warning/error)的日志消息,适合记录事件;send_progress 发送 progress/total 数值,适合展示长任务的完成百分比。

🧠 自测:有状态(Stateful)StreamableHTTP 服务器在生产环境中的主要挑战是什么?
  1. 无法支持工具调用,只支持资源读取
  2. 必须配置黏性会话,同一客户端请求必须路由到同一服务器实例 ✓
  3. 不支持 SSE,只能使用 HTTP 轮询
  4. 每次请求都需要重新连接,性能很差

有状态服务器将会话状态保存在内存中,因此来自同一客户端的所有请求必须路由到同一台服务器。在有负载均衡的环境中,需要配置黏性会话(Sticky Session)才能正确工作。

🧠 自测:JSON-RPC 中,Notification 消息和 Request 消息的关键区别是什么?
  1. Notification 使用 XML 格式,Request 使用 JSON 格式
  2. Notification 没有 id 字段,不需要响应;Request 有 id,需要对应的 Response ✓
  3. Notification 只能从客户端发送给服务器,Request 方向相反
  4. Notification 只包含错误信息,Request 包含正常请求

id 是关键区别:Request 有 id,接收方必须用相同 id 返回 Response 或 Error;Notification 无 id,是单向推送,接收方处理后不需要回复。

课程总结

恭喜你完成了 MCP 进阶专题的全部内容!你现在掌握的技能,足以构建真正生产级的 MCP 服务器。

我们学了什么

  • Sampling:服务器反向请求客户端调用 LLM,API Key 和费用控制权在用户侧
  • 日志通知:send_log_message() 提供结构化日志(debug/info/warning/error)
  • 进度通知:send_progress() 为长任务提供实时进度更新
  • Roots:客户端声明文件访问边界,服务器验证路径合法性
  • JSON-RPC 消息类型:Request、Response、Notification、Error 四种类型
  • STDIO 传输:本地单客户端,简单直接,适合 Claude Desktop 集成
  • StreamableHTTP 传输:网络多客户端,SSE 推送,适合云端部署
  • 有状态 vs 无状态:理解权衡,根据规模选择合适的架构

生产环境的真实考量

  • 错误处理:工具内部的异常必须被捕获,返回有意义的错误消息给客户端
  • 认证授权:StreamableHTTP 部署时,通过 HTTP 头实现 API Key 或 OAuth 认证
  • 监控告警:将日志通知接入 Datadog、CloudWatch 等监控系统
  • 速率限制:防止客户端过度调用工具,保护后端资源
🧠 自测:在 StreamableHTTP 生产部署中,最推荐的认证方式是?
  1. 将 API Key 硬编码在服务器配置文件中
  2. 通过 HTTP 请求头传递认证信息(如 Authorization: Bearer token) ✓
  3. 使用 IP 白名单替代任何认证
  4. 在 URL 路径中包含密钥

HTTP 头(如 Authorization: Bearer token)是 Web API 认证的标准做法,安全且灵活。硬编码密钥和 URL 中的密钥都存在安全风险,IP 白名单在云环境中难以维护。

想要测验互动、进度自动保存的完整体验?

进入 AI 学院互动版 →

继续学习其他课程

Claude 101用 Claude 处理日常工作:写作、总结、头脑风暴、整理资料。零基础友好。 Claude Code 101在终端里用 AI 编程代理:读写代码、运行命令、定制工作流。 Model Context Protocol 入门用 Python 从零构建 MCP 服务器与客户端,连接 Claude 与外部服务。 Agent Skills 入门把任务的标准做法打包成可复用的 Skill,让 Claude 稳定专业地完成。 Subagents 入门用子代理拆分与委派任务,保持主上下文干净。 AI 基础搞懂生成式 AI 到底是什么、怎么工作,建立可靠的心智模型。 应用 AI 基础把 AI 真正用进日常工作:找到高价值场景,养成可靠习惯。 Agents 与工作流让 AI 不止于回答,而是自己分步完成任务、自动化你的工作流。