MCP协议详解和案例(Resources/Prompts/Tools/Sampling/Roots/Transports)

一:核心架构

1.1 架构概述

  • 主机(Hosts):希望通过 MCP 访问数据的程序,例如 Claude Desktop、IDE 或 AI 工具
    Host 进程充当容器和协调器的角色,主要功能有:
    • 创建和管理多个 MCP 客户端实例
    • 控制 MCP 客户端连接权限和生命周期
    • 执行安全政策和同意要求
    • 处理用户授权决策
    • 协调 AI/LLM 集成和采样
    • 管理跨客户端的上下文聚合
  • 客户端(Clients):与服务器保持 1:1 连接的协议客户端
    每个客户端由 Host 创建并维护一个独立的服务端连接,主要功能有:
    • 与每个服务器建立一个有状态会话
    • 处理协议协商和能力交换
    • 双向路由协议消息
    • 管理订阅和通知
    • 维护服务器之间的安全边界
  • 服务端(Servers):轻量级程序,每个程序都通过标准化的 Model Context Protocol 公开特定功能
    服务端提供专门的上下文和功能,主要功能有:
    • 通过 MCP 原语公开资源(Resources)、工具(Tools)和提示(Prompts)
    • 通过客户端界面请求采样
      特点:
    • 独立运作,专心负责
    • 必须尊重安全约束
    • 可以是本地进程或远程服务
  • 本地数据源:MCP 服务器可以安全访问本地计算机文件、数据库和服务
  • 远程服务:MCP 服务器可通过互联网(例如通过 API)连接到的外部系统

1.2 设计原则

MCP 建立在几个关键的设计原则之上,这些原则决定了其架构和实现。

  1. 易构建:服务端应该非常容易构建
  • 主机应用程序处理复杂的编排职责
  • 服务端专注于特定的、定义明确的功能
  • 简单的接口最大程度地减少实现开销
  • 清晰的分离使代码更易于维护
  1. 模块化:服务端应高度可组合
  • 每个服务端单独提供重点功能
  • 多个服务端可以无缝组合
  • 共享协议实现互操作性
  • 模块化设计支持可扩展性
  1. 隔离性:服务端不应该能够读取整个对话,也不应该"查看"其他服务端
  • 服务端仅接收必要的上下文信息
  • 完整对话历史记录保留在 Host 中
  • 每个服务端连接保持隔离
  • 跨服务端交互由主机控制
  • Host 进程强制执行安全边界
  1. 渐进扩展:可以逐步向服务器和客户端添加功能
  • 核心协议提供所需的最少功能
  • 可以根据需要协商附加功能
  • 服务器和客户端独立发展
  • 为未来扩展而设计的协议
  • 保持向后兼容性

1.3 核心功能

MCP 通过以下原语实现功能:

  1. 资源(Resources):公开数据(如文件、数据库记录),支持文本和二进制格式。
  2. 提示(Prompts):定义可复用的交互模板,驱动工作流。
  3. 工具(Tools):提供可执行功能(如 API 调用、计算),由模型控制。
  4. 采样(Sampling):服务器通过客户端请求 LLM 推理,保持安全隔离。
  5. 根(Roots):定义服务器操作范围(如文件路径、API 端点)。

1.4 协议与传输

MCP 的协议与传输机制是实现客户端-服务器通信的基础,确保 AI 模型与外部资源和工具的高效、安全交互。

1.4.1 协议层 Protocol layer

协议层基于 JSON-RPC 2.0,定义了消息框架、请求/响应机制和通信模式,确保客户端与服务器间的标准化交互。

其主要功能包括:

  • 消息路由:处理请求、响应和通知的传递。
  • 会话管理:维护客户端与服务器的稳定连接。
  • 标准化接口:为客户端(连接服务器)和服务器(提供功能)提供统一接口。
    协议层简化了通信复杂性,体现了 MCP 设计原则中的"易构建"和"模块化",为资源、工具等核心组件提供通信支持。

1.4.2 传输层 Transport layer

传输层负责客户端与服务器间的实际数据交换。MCP 支持两种主要传输机制:

  1. 标准传输 Stdio
  • 使用标准输入/输出进行通信
  • 适合本地场景,如命令行工具和 IDE 插件
  1. HTTP(SSE)
  • 通过 HTTP POST 和 Server-Sent Events(SSE)实现消息传递
  • 适合远程和云服务场景
    两种传输均采用 JSON-RPC 2.0 格式,确保消息一致性。传输层的模块化设计支持 MCP 的扩展性,可适配未来新增的传输方式。

1.4.3 消息类型 Message types

MCP 定义了四种消息类型,支持灵活的通信模式:

  1. 请求(Requests):发起请求操作,期望得到对方响应,如查询资源列表
  2. 结果(Results):返回操作的成功结果
  3. 错误(Errors):指示操作失败,包含错误码和信息
  4. 通知(Notifications):单向消息,无需响应,如资源更新通知
    示例:客户端请求资源列表,服务器返回可用资源
json 复制代码
// 请求
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "resources/list",
  "params": {}
}

// 结果
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [{"uri": "file:///logs/app.log", "name": "应用日志"}]
}

1.5 连接生命周期

1.5.1 初始化 Initialization

  • 步骤一:客户端发送带有协议版本和功能 initialize 请求
  • 步骤二:服务器以其协议版本和功能进行响应
  • 步骤三:客户端发送 initialized 通知作为确认
  • 步骤四:开始正常信息交换

1.5.2 信息交换 Message exchange

初始化后,支持以下模式:

  • 请求-响应:客户端或服务器发送请求,对方响应
  • 通知:任何一方发送单向消息

1.5.3 终止 Termination

任何一方都可以终止连接,错误的传播方式有:请求的错误响应、传输中的错误事件、协议级错误处理。

  • 通过 close() 彻底关闭
  • 传输断开
  • 错误情况
java 复制代码
MCP 定义了标准错误代码
enum ErrorCode {
  // Standard JSON-RPC error codes
  ParseError = -32700,
  InvalidRequest = -32600,
  MethodNotFound = -32601,
  InvalidParams = -32602,
  InternalError = -32603
}

1.6 应用场景

  1. 代码开发与协作
  • GitHub集成:官方MCP服务器支持仓库管理、代码搜索、Issue跟踪、PR管理等完整GitHub工作流
  • Git操作:读取、操作和搜索本地Git仓库
  • IDE集成:JetBrains提供的MCP服务支持在IDE环境中进行代码编写、分析和调试
  • 代码审查:与Cursor、Windsurf等编辑器结合,支持智能代码审查和重构建议
  1. 数据查询与可视化
  • 数据库操作:已有PostgreSQL、MongoDB等数据库的MCP服务器,支持SQL查询和结果分析
  • 数据可视化:Grafana官方MCP服务器支持数据查询和可视化面板创建
  • 本地文件处理:访问、处理和分析本地文件系统中的数据
  1. 第三方API集成
  • 企业服务集成:
    • Slack:发送消息和查询对话历史
    • Atlassian:与Confluence和Jira交互,搜索文档和管理任务
    • Stripe:处理支付和管理客户账户
  • 云服务管理:
    • AWS:操作AWS资源,如S3、EC2、Lambda等
    • Kubernetes:管理容器化应用,监控集群状态
  1. 个人效率工具
  • Google云盘:文件访问和文件搜索
  • Google地图:获取地理位置和导航信息
  • 社交媒体:与Twitter(X)、YouTube等平台交互,发布内容和检索信息
  1. 智能助手应用
  • 浏览器工具:如browsertools插件,在Cursor中自动获取Chrome开发者工具的控制台日志
  • 多源聚合:结合多个MCP服务器构建复杂工作流,如先搜索问题、分析数据,然后提交解决方案

1.7 MCP工具平台

MCP生态正在快速发展,新工具持续涌现。越来越多企业和开发者加入MCP标准,使AI能力边界不断扩展。最新工具可在下列平台上找到:

1.8 Python SDK

Python SDK:https://github.com/modelcontextprotocol/python-sdk

二:核心组件详解

服务端功能:Resources、Prompts、Tools

客户端功能:Sampling、Roots

2.1 Resources

资源(Resources)是 MCP 中的核心原语,它允许服务端公开可供客户端读取并用作 LLM 交互上下文的数据和内容。

资源表示服务端提供的可访问数据,典型类型包括:

  • 文件内容:日志、配置文件、源代码等。
  • 数据库记录:客户数据、交易记录等。
  • API 响应:天气数据、股票价格等。
  • 实时数据:系统指标、传感器读数等。
  • 多媒体:图像、音频、视频等。
    每个资源由唯一的 URI 标识,支持文本或二进制格式。资源的主要功能包括:
  • 发现:客户端通过服务端接口查询可用资源。
  • 读取:客户端按需获取资源内容。
  • 更新:支持资源的动态更新和订阅。

2.1.1 资源URI

资源 URI 由服务端定义协议和路径结构,遵循以下标准格式:

java 复制代码
[protocol]://[host]/[path]
  • 示例
    • 本地文件系统资源:file:///home/user/logs/app.log
    • 数据库:postgres://db.example.com/customers/schema
    • 自定义:screen://localhost/display1
  • 特点
    • URI 唯一标识资源,支持跨系统访问
    • 服务端可定义自定义协议(如 screen://),增强扩展性

2.1.2 资源类型

资源可以包含两种类型的内容:

  1. 文本资源:
  • 格式:UTF-8 编码文本。
  • 适用场景:源代码、日志文件、JSON/XML 数据、配置文件。
  • 示例:text/plain 类型的日志文件。
  1. 二进制资源:
  • 格式:Base64 编码的二进制数据。
  • 适用场景:图像(PNG/JPEG)、PDF、音频、视频。
  • 示例:image/png 类型的截图。

2.1.3 资源发现

客户端可以通过两种主要方法发现可用资源:

  1. 直接资源
    服务器通过 resources/list 端点公开具体资源的列表。每个资源包括:
java 复制代码
{
  uri: string;           // Unique identifier for the resource
  name: string;          // Human-readable name
  description?: string;  // Optional description
  mimeType?: string;     // Optional MIME type
}
  1. 资源模板
    对于动态资源,服务器可以公开 URI 模板 ,客户端可以使用它来构建有效的资源 URI:
java 复制代码
{
  uriTemplate: string;   // URI template following RFC 6570
  name: string;          // Human-readable name for this type
  description?: string;  // Optional description
  mimeType?: string;     // Optional MIME type for all matching resources
}

资源读取

要读取资源,客户端需要使用资源 URI 发出 resources/read 请求。

服务器以资源内容列表进行响应,比如:

java 复制代码
{
  contents: [
    {
      uri: string;        // The URI of the resource
      mimeType?: string;  // Optional MIME type

      // One of:
      text?: string;      // For text resources
      blob?: string;      // For binary resources (base64 encoded)
    }
  ]
}

服务器可能会响应一个 resources/read 请求而返回多个资源。例如,这可用于在读取目录时返回目录内的文件列表。

2.1.4 资源更新

MCP 通过两种机制支持资源的实时更新:

  1. 列表更改
  • 资源列表发生变化时,服务器可以通过 notifications/resources/list_changed 通知客户端
  • 示例:新日志文件添加时通知
  1. 内容变更
    客户可以订阅特定资源的更新:
  • 客户端通过 resources/subscribe 订阅特定资源
  • 当资源发生变化时,服务器发送 notifications/resources/updated
  • 客户端可以使用 resources/read 获取最新内容
  • 客户端也可以使用 resources/unsubscribe 取消订阅

2.1.5 示例代码

resource_server.py

py 复制代码
"""
MCP 服务端(resource_server.py)
"""

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
from anyio import open_file
from typing import AnyUrl

app = Server("example-server")

@app.list_resources()
async def list_resources() -> list[types.Resource]:
    """返回可用资源列表"""
    return [
        types.Resource(
            uri="file:///logs/app.log",
            name="应用日志",
            description="系统运行日志",
            mimeType="text/plain"
        )
    ]

@app.read_resource()
async def read_resource(uri: AnyUrl) -> list[types.Content]:
    """读取资源内容"""
    if str(uri) == "file:///logs/app.log":
        async with await open_file("/logs/app.log", "r") as f:
            content = await f.read()
        return [types.Content(uri=str(uri), mimeType="text/plain", text=content)]
    raise ValueError("资源不存在")

@app.subscribe_resource()
async def subscribe_resource(uri: AnyUrl) -> None:
    """订阅资源更新(模拟实现)"""
    if str(uri) != "file:///logs/app.log":
        raise ValueError("不支持的资源")
    # 模拟文件变更通知(实际实现需监控文件变化)
    await app.notify("resources/updated", {"uri": str(uri)})

async def main():
    async with stdio_server() as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())

if __name__ == "__main__":
    import anyio
    anyio.run(main)

resource_client.py

py 复制代码
"""
MCP 客户端(resource_client.py)
"""

from mcp.client import stdio_client, ClientSession
import asyncio

async def main():
    async with stdio_client(command="python resource_server.py") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            # 初始化会话
            await session.initialize()

            # 发现资源
            request = {"method": "resources/list", "params": {}}
            resources = await session.send_request(request, list)
            print("可用资源:", resources)

            # 读取资源
            read_request = {"method": "resources/read", "params": {"uri": "file:///logs/app.log"}}
            content = await session.send_request(read_request, dict)
            print("资源内容:", content)

            # 订阅资源更新
            subscribe_request = {"method": "resources/subscribe", "params": {"uri": "file:///logs/app.log"}}
            await session.send_request(subscribe_request, None)

            # 监听通知(模拟)
            async for notification in session.notifications():
                if notification["method"] == "resources/updated":
                    print("收到更新通知:", notification["params"])
                    break

if __name__ == "__main__":
    asyncio.run(main)

2.2 Prompts

提示(Prompts)是 MCP 中用于定义可重复使用的模板和工作流程的核心组件,服务端通过提示公开交互模式,客户端将其呈现给用户以指导特定任务。

MCP 中的 Prompts 是预定义的模板,支持以下功能:

  • 动态参数:接受用户输入或上下文变量。
  • 资源集成:嵌入资源内容(如日志、代码文件)作为上下文。
  • 多步骤工作流:引导复杂的交互过程,如调试或报告生成。
  • 用户界面:作为 UI 元素(如斜线命令 /commit),提升交互效率。

2.2.1 Prompts结构

每个 Prompts 的定义如下:

json 复制代码
{
  name: string;              // Unique identifier for the prompt
  description?: string;      // Human-readable description
  arguments?: [              // Optional list of arguments
    {
      name: string;          // Argument identifier
      description?: string;  // Argument description
      required?: boolean;    // Whether argument is required
    }
  ]
}

2.2.2 Prompts发现

客户端可以通过 prompts/list 端点发现可用的提示,服务端返回prompts列表:

json 复制代码
// Request
{
  method: "prompts/list"
}

// Response
{
  prompts: [
    {
      name: "analyze-code",
      description: "Analyze code for potential improvements",
      arguments: [
        {
          name: "language",
          description: "Programming language",
          required: true
        }
      ]
    }
  ]
}

2.2.3 Prompts使用

客户端通过 prompts/get 端口获取prompts的执行结果,需指定prompts名称和参数:

json 复制代码
// Request
{
  method: "prompts/get",
  params: {
    name: "analyze-code",
    arguments: {
      language: "python"
    }
  }
}

// Response
{
  description: "Analyze Python code for potential improvements",
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: "Please analyze the following Python code for potential improvements:\n\n```python\ndef calculate_sum(numbers):\n    total = 0\n    for num in numbers:\n        total = total + num\n    return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n```"
      }
    }
  ]
}

特点:

  • 响应包含一组消息(messages),可直接用于 LLM 交互。
  • 支持动态内容,如嵌入参数或资源。

2.2.4 动态Prompts

Prompts 支持动态生成内容,涵盖以下场景:

  1. 嵌入资源上下文
    Prompts 可引用资源(如日志文件、代码),服务端在响应中嵌入资源内容。
    如:分析项目日志和代码的提示
json 复制代码
// Request
{
  "name": "analyze-project",
  "description": "Analyze project logs and code",
  "arguments": [
    {
      "name": "timeframe",
      "description": "Time period to analyze logs",
      "required": true
    },
    {
      "name": "fileUri",
      "description": "URI of code file to review",
      "required": true
    }
  ]
}

处理 prompts/get 请求时,响应如下:

javascript 复制代码
// Response

{
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Analyze these system logs and the code file for any issues:"
      }
    },
    {
      "role": "user",
      "content": {
        "type": "resource",
        "resource": {
          "uri": "logs://recent?timeframe=1h",
          "text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded",
          "mimeType": "text/plain"
        }
      }
    },
    {
      "role": "user",
      "content": {
        "type": "resource",
        "resource": {
          "uri": "file:///path/to/code.py",
          "text": "def connect_to_service(timeout=30):\n    retries = 3\n    for attempt in range(retries):\n        try:\n            return establish_connection(timeout)\n        except TimeoutError:\n            if attempt == retries - 1:\n                raise\n            time.sleep(5)\n\ndef establish_connection(timeout):\n    # Connection implementation\n    pass",
          "mimeType": "text/x-python"
        }
      }
    }
  ]
}
  1. 多步骤工作流
    Prompts 可定义多轮交互,指导用户完成复杂任务。
    如:调试错误的工作流
javascript 复制代码
// Request

{
  "method": "prompts/get",
  "params": {
    "name": "debug-error",
    "arguments": {
      "error": "Connection timeout"
    }
  }
}

// Response
{
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Here's an error I'm seeing: Connection timeout"
      }
    },
    {
      "role": "assistant",
      "content": {
        "type": "text",
        "text": "I'll help analyze this error. What have you tried so far?"
      }
    },
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "I've tried restarting the service, but the error persists."
      }
    }
  ]
}

2.2.5 示例代码

以下是一个完整的 MCP 提示服务端和客户端实现,展示 Prompts 发现、获取和动态资源集成的功能。

prompt_server.py

py 复制代码
"""
MCP 服务端(prompt_server.py)
"""

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
from typing import Dict, Optional

# 定义提示模板
PROMPTS = {
    "git-commit": types.Prompt(
        name="git-commit",
        description="生成 Git 提交信息",
        arguments=[
            types.PromptArgument(
                name="changes",
                description="变更的差异或描述",
                required=True
            )
        ]
    ),
    "analyze-project": types.Prompt(
        name="analyze-project",
        description="分析项目日志和代码",
        arguments=[
            types.PromptArgument(
                name="timeframe",
                description="日志分析的时间范围",
                required=True
            ),
            types.PromptArgument(
                name="fileUri",
                description="代码文件的 URI",
                required=True
            )
        ]
    )
}

app = Server("prompt-server")

@app.list_prompts()
async def list_prompts() -> list[types.Prompt]:
    """返回可用提示列表"""
    return list(PROMPTS.values())

@app.get_prompt()
async def get_prompt(name: str, arguments: Optional[Dict[str, str]] = None) -> types.GetPromptResult:
    """获取提示的执行结果"""
    if name not in PROMPTS:
        raise ValueError(f"提示不存在: {name}")

    if name == "git-commit":
        if not arguments or "changes" not in arguments:
            raise ValueError("缺少必要参数: changes")
        changes = arguments["changes"]
        return types.GetPromptResult(
            description="生成 Git 提交信息",
            messages=[
                types.PromptMessage(
                    role="user",
                    content=types.TextContent(
                        type="text",
                        text=f"为以下变更生成简洁的提交信息:\n\n{changes}"
                    )
                )
            ]
        )

    if name == "analyze-project":
        if not arguments or "timeframe" not in arguments or "fileUri" not in arguments:
            raise ValueError("缺少必要参数: timeframe 或 fileUri")
        timeframe = arguments["timeframe"]
        file_uri = arguments["fileUri"]
        # 模拟日志和代码内容
        log_content = "[2024-03-14 15:32:11] ERROR: Connection timeout"
        code_content = "def connect_to_service():\n    pass"
        return types.GetPromptResult(
            description="分析项目日志和代码",
            messages=[
                types.PromptMessage(
                    role="user",
                    content=types.TextContent(
                        type="text",
                        text="分析以下系统日志和代码文件,找出潜在问题:"
                    )
                ),
                types.PromptMessage(
                    role="user",
                    content=types.ResourceContent(
                        type="resource",
                        resource=types.Content(
                            uri=f"logs://recent?timeframe={timeframe}",
                            text=log_content,
                            mimeType="text/plain"
                        )
                    )
                ),
                types.PromptMessage(
                    role="user",
                    content=types.ResourceContent(
                        type="resource",
                        resource=types.Content(
                            uri=file_uri,
                            text=code_content,
                            mimeType="text/x-python"
                        )
                    )
                )
            ]
        )

    raise ValueError("提示实现未找到")

async def main():
    async with stdio_server() as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())

if __name__ == "__main__":
    import anyio
    anyio.run(main)

prompt_client.py

py 复制代码
"""
MCP 客户端(prompt_client.py)
"""

from mcp.client import stdio_client, ClientSession
import asyncio

async def main():
    async with stdio_client(command="python prompt_server.py") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            # 初始化会话
            await session.initialize()

            # 发现提示
            request = {"method": "prompts/list", "params": {}}
            prompts = await session.send_request(request, dict)
            print("可用提示:", prompts["prompts"])

            # 获取 Git 提交提示
            git_request = {
                "method": "prompts/get",
                "params": {
                    "name": "git-commit",
                    "arguments": {"changes": "添加用户认证功能"}
                }
            }
            git_result = await session.send_request(git_request, dict)
            print("Git 提交提示:", git_result)

            # 获取项目分析提示
            project_request = {
                "method": "prompts/get",
                "params": {
                    "name": "analyze-project",
                    "arguments": {
                        "timeframe": "1h",
                        "fileUri": "file:///code/network.py"
                    }
                }
            }
            project_result = await session.send_request(project_request, dict)
            print("项目分析提示:", project_result)

if __name__ == "__main__":
    asyncio.run(main)

2.3 Tools

工具(Tools)是 MCP 中用于公开可执行功能的核心组件,允许服务端向客户端提供与外部系统交互、执行计算或修改状态的能力。

MCP 中的工具允许服务器公开可由客户端调用并用于执行操作的可执行函数。工具的主要能力包括:

  • Discovery:客户端可以通过 tools/list 端点列出可用的工具
  • Invocation:使用 tools/call 端点调用工具,服务端执行工具并返回结果
  • Flexibility:工具的实现可以从简单的计算到复杂的 API 交互
    与 Resources 一样,工具也由唯一名称标识,并可以包含说明来指导其使用。不同的是,工具可以修改状态或与外部系统动态交互操作。

2.3.1 工具结构

每个工具都具有以下结构定义:

javascript 复制代码
{
  name: string;          // Unique identifier for the tool
  description?: string;  // Human-readable description
  inputSchema: {         // JSON Schema for the tool's parameters
    type: "object",
    properties: { ... }  // Tool-specific parameters
  }
}

2.3.2 工具发现

客户端通过 tools/list 端口查询可用工具,服务端返回工具列表:

javascript 复制代码
// Request
{
  "method": "tools/list",
  "params": {}
}

// Response
{
  "tools": [
    {
      "name": "calculate_sum",
      "description": "计算两个数的和",
      "inputSchema": {
        "type": "object",
        "properties": {
          "a": {"type": "number"},
          "b": {"type": "number"}
        },
        "required": ["a", "b"]
      }
    },
    {
      "name": "github_create_issue",
      "description": "创建 GitHub 问题",
      "inputSchema": {
        "type": "object",
        "properties": {
          "title": {"type": "string"},
          "body": {"type": "string"}
        },
        "required": ["title"]
      }
    }
  ]
}

2.3.3 工具执行

客户端通过 tools/call 接口调用工具,需指定工具名称和参数:

javascript 复制代码
// Request
{
  "method": "tools/call",
  "params": {
    "name": "calculate_sum",
    "arguments": {
      "a": 5,
      "b": 3
    }
  }
}

// Response
{
  "contents": [
    {
      "type": "text",
      "text": "8"
    }
  ]
}

2.3.4 工具实现示例

工具支持多种实现类型:

  1. 系统操作
    示例:执行 shell 命令,与本地系统交互的工具
javascript 复制代码
{
  name: "execute_command",
  description: "Run a shell command",
  inputSchema: {
    type: "object",
    properties: {
      command: { type: "string" },
      args: { type: "array", items: { type: "string" } }
    },
    required: ["command"]
  }
}
  1. API集成
    示例:调用 GitHub API,创建 Issue
javascript 复制代码
{
  name: "github_create_issue",
  description: "Create a GitHub issue",
  inputSchema: {
    type: "object",
    properties: {
      title: { type: "string" },
      body: { type: "string" },
      labels: { type: "array", items: { type: "string" } }
    },
    required: ["title"]
  }
}
  1. 数据处理
    示例:分析 CSV 文件
javascript 复制代码
{
  name: "analyze_csv",
  description: "Analyze a CSV file",
  inputSchema: {
    type: "object",
    properties: {
      filepath: { type: "string" },
      operations: {
        type: "array",
        items: {
          enum: ["sum", "average", "count"]
        }
      }
    },
    "required": ["filepath", "operation"]
  }
}

【注意】工具调用可能涉及敏感操作(如文件写入、API 请求),服务端需实现:

  • 参数验证:确保输入符合 inputSchema
  • 权限检查:限制工具访问敏感资源
  • 用户确认:关键操作需用户批准,符合模型控制设计

2.3.5 示例代码

以下是一个完整的 MCP 工具服务端和客户端实现,展示工具发现、调用和复杂 API 集成。

tool_server.py

py 复制代码
"""
MCP 服务端(tool_server.py)
"""

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
from typing import Dict, List, Union
import aiohttp

app = Server("tool-server")

@app.list_tools()
async def list_tools() -> List[types.Tool]:
    """返回可用工具列表"""
    return [
        types.Tool(
            name="calculate_sum",
            description="计算两个数的和",
            inputSchema={
                "type": "object",
                "properties": {
                    "a": {"type": "number", "description": "第一个数"},
                    "b": {"type": "number", "description": "第二个数"}
                },
                "required": ["a", "b"]
            }
        ),
        types.Tool(
            name="github_create_issue",
            description="创建 GitHub 问题",
            inputSchema={
                "type": "object",
                "properties": {
                    "title": {"type": "string", "description": "问题标题"},
                    "body": {"type": "string", "description": "问题描述"}
                },
                "required": ["title"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: Dict) -> List[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:
    """执行工具调用"""
    if name == "calculate_sum":
        if "a" not in arguments or "b" not in arguments:
            raise ValueError("缺少必要参数: a, b")
        a, b = arguments["a"], arguments["b"]
        if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
            raise ValueError("参数 a, b 必须为数字")
        result = a + b
        return [types.TextContent(type="text", text=str(result))]

    if name == "github_create_issue":
        if "title" not in arguments:
            raise ValueError("缺少必要参数: title")
        title = arguments["title"]
        body = arguments.get("body", "")
        # 模拟 GitHub API 调用
        async with aiohttp.ClientSession() as session:
            # 实际场景需配置 token 和 URL
            mock_response = {"html_url": f"https://github.com/mock/repo/issues/123"}
            return [types.TextContent(type="text", text=f"问题已创建: {mock_response['html_url']}")]

    raise ValueError(f"工具不存在: {name}")

async def main():
    async with stdio_server() as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())

if __name__ == "__main__":
    import anyio
    anyio.run(main)

tool_client.py

py 复制代码
"""
MCP 客户端(tool_client.py)
"""

from mcp.client import stdio_client, ClientSession
import asyncio

async def main():
    async with stdio_client(command="python tool_server.py") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            # 初始化会话
            await session.initialize()

            # 发现工具
            request = {"method": "tools/list", "params": {}}
            tools = await session.send_request(request, dict)
            print("可用工具:", tools["tools"])

            # 调用计算工具
            calc_request = {
                "method": "tools/call",
                "params": {
                    "name": "calculate_sum",
                    "arguments": {"a": 5, "b": 3}
                }
            }
            calc_result = await session.send_request(calc_request, dict)
            print("计算结果:", calc_result)

            # 调用 GitHub 工具
            github_request = {
                "method": "tools/call",
                "params": {
                    "name": "github_create_issue",
                    "arguments": {
                        "title": "测试问题",
                        "body": "这是一个测试问题描述"
                    }
                }
            }
            github_result = await session.send_request(github_request, dict)
            print("GitHub 问题结果:", github_result)

if __name__ == "__main__":
    asyncio.run(main)

2.4 Sampling

采样(Sampling)是 MCP 中用于请求客户端调用 LLM 的核心功能,允许服务端通过客户端与 LLM 交互生成复杂内容,服务端无需直接访问模型,保障了安全性和隐私。

采样的主要功能包括:

  • 内容生成:生成文本、图像或其他 LLM 输出
  • 上下文集成:结合资源、提示或工具的上下文
  • 用户控制:客户端审核和修改请求,确保透明性
  • 灵活配置:支持模型选择、采样参数和上下文范围

2.4.1 采样流程

采样过程遵循以下步骤:

  1. 服务端向客户端发送 sampling/createMessage 请求,包含消息和参数
  2. 客户端审核该请求,并可以修改内容或参数
  3. 客户端调用 LLM 进行采样
  4. 客户端检查生成结果,确保符合要求
  5. 客户端将结果返回给服务器
    这种人机交互设计可确保用户能够控制所看到和生成的内容,保障隐私和安全性。

消息格式

采样请求使用标准化的消息格式:

javascript 复制代码
{
  messages: [
    {
      role: "user" | "assistant",
      content: {
        type: "text" | "image",

        // For text:
        text?: string,

        // For images:
        data?: string,             // base64 encoded
        mimeType?: string
      }
    }
  ],
  modelPreferences?: {
    hints?: [{
      name?: string                // Suggested model name/family
    }],
    costPriority?: number,         // 0-1, importance of minimizing cost
    speedPriority?: number,        // 0-1, importance of low latency
    intelligencePriority?: number  // 0-1, importance of capabilities
  },
  systemPrompt?: string,
  includeContext?: "none" | "thisServer" | "allServers",
  temperature?: number,
  maxTokens: number,
  stopSequences?: string[],
  metadata?: Record<string, unknown>
}
  • messages
    数组类型,包含要发送给 LLM 的对话历史记录。每条消息均包含以下内容:
    • role: 角色,可选值有 user、assistant
    • content:消息内容,可以是
      • 带有 text 字段的文本内容
      • 带有 data (base64)和 mimeType 字段的图像内容
  • modelPreferences
    允许服务器指定其模型选择偏好,客户根据这些偏好和可用的模型做出最终的模型选择。包括:
    • hints:模型名称列表,客户端可以使用它来选择合适的模型
      • name:可以匹配完整或部分模型名称的字符串(例如"claude-3"、"sonnet")
    • Priority values(0-1 标准化):
      • costPriority:最小化成本的重要性
      • speedPriority:低延迟响应的重要性
      • intelligencePriority:高级模型能力的重要性
  • systemPrompt
    可选的字段,允许服务端请求特定的系统提示。客户端可以修改或忽略此字段。
  • includeContext
    指定要包含的 MCP 上下文,可选值有:
    • none:没有其他上下文
    • thisServer:包含来自请求服务器的上下文
    • allServers:包含所有已连接 MCP 服务器的上下文
  • 采样参数
    使用以下命令对采样进行微调:
    • temperature:控制随机性(0.0 到 1.0)
    • maxTokens: 最大令牌数
    • stopSequences:停止生成的序列数组
    • metadata:其他特定于提供商的参数

2.4.2 响应格式

客户端返回完成结果:

javascript 复制代码
{
  model: string,  // Name of the model used
  stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string,
  role: "user" | "assistant",
  content: {
    type: "text" | "image",
    text?: string,
    data?: string,
    mimeType?: string
  }
}

2.4.3 示例代码

以下是一个完整的 MCP 采样服务端和客户端实现,展示采样请求、上下文集成和结果处理

sampling_server.py

py 复制代码
"""
MCP 服务端(sampling_server.py)
"""

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
from typing import Dict, List, Optional, Union

app = Server("sampling-server")

@app.list_resources()
async def list_resources() -> List[types.Resource]:
    """返回资源列表,供采样上下文使用"""
    return [
        types.Resource(
            uri="file:///logs/app.log",
            name="应用日志",
            description="系统运行日志",
            mimeType="text/plain"
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: Dict) -> List[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:
    """模拟工具调用,供采样上下文使用"""
    if name == "check_status":
        return [types.TextContent(type="text", text="系统状态:正常")]
    raise ValueError(f"工具不存在: {name}")

async def send_sampling_request(session):
    """发送采样请求"""
    request = {
        "method": "sampling/createMessage",
        "params": {
            "messages": [
                types.PromptMessage(
                    role="user",
                    content=types.ResourceContent(
                        type="resource",
                        resource=types.Content(
                            uri="file:///logs/app.log",
                            text="[2024-03-14 15:32:11] ERROR: Connection timeout",
                            mimeType="text/plain"
                        )
                    )
                ).to_dict(),
                types.PromptMessage(
                    role="user",
                    content=types.TextContent(
                        type="text",
                        text="分析日志并建议修复方案"
                    )
                ).to_dict()
            ],
            "systemPrompt": "你是一个系统诊断助手,提供详细的错误分析。",
            "includeContext": "thisServer",
            "modelPreferences": {
                "hints": [{"name": "claude"}],
                "intelligencePriority": 0.8
            },
            "temperature": 0.7,
            "maxTokens": 150,
            "stopSequences": ["\n\n"]
        }
    }
    result = await session.send_request(request, dict)
    return result

async def main():
    async with stdio_server() as streams:
        async with app.session(streams[0], streams[1]) as session:
            # 运行服务器并发送采样请求
            result = await send_sampling_request(session)
            print("采样结果:", result)

if __name__ == "__main__":
    import anyio
    anyio.run(main)

sampling_client.py

py 复制代码
"""
MCP 客户端(sampling_client.py)
"""

from mcp.client import stdio_client, ClientSession
import asyncio

async def mock_llm_call(messages: List[Dict], params: Dict) -> Dict:
    """模拟 LLM 调用,返回采样结果"""
    # 模拟处理日志分析请求
    if any("日志" in msg.get("content", {}).get("text", "") for msg in messages):
        return {
            "model": "claude3-7",
            "stopReason": "maxTokens",
            "role": "assistant",
            "content": {
                "type": "text",
                "text": "日志显示连接超时,建议:\n1. 检查网络稳定性\n2. 增加超时时间"
            }
        }
    return {
        "model": "claude3-7",
        "stopReason": "endTurn",
        "role": "assistant",
        "content": {
            "type": "text",
            "text": "无法识别请求"
        }
    }

async def main():
    async with stdio_client(command="python sampling_server.py") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            # 初始化会话
            await session.initialize()

            # 模拟客户端处理采样请求
            async for request in session.requests():
                if request["method"] == "sampling/createMessage":
                    params = request["params"]
                    result = await mock_llm_call(params["messages"], params)
                    await session.send_response(request["id"], result)
                    print("客户端返回结果:", result)

if __name__ == "__main__":
    asyncio.run(main)

2.5 Roots

根(Roots)是 MCP 中定义服务端操作边界的概念,客户端通过根向服务端建议关注的资源位置和范围。根以 URI 形式表示,支持文件系统路径、HTTP 接口或其他自定义资源,帮助服务端高效定位和管理上下文。

根是客户端推荐的服务端工作范围,通常表示项目目录、API 端点或数据源:

javascript 复制代码
file:///home/user/projects/myapp
https://api.example.com/v1

2.5.1 根的作用

根的主要功能包括:

  • 资源指引:告知服务端相关资源的 URI 和逻辑分组
  • 工作空间隔离:明确区分用户的工作环境(如代码库、数据库)
  • 动态管理:多个根可让用户同时使用不同的资源
  • 灵活扩展:支持任意 URI 格式,适配多种场景

2.5.2 根的结构

javascript 复制代码
{
  "roots": [
    {
      "uri": "file:///home/user/projects/frontend",
      "name": "Frontend Repository",
      "description": "Front-end project directory"
    },
    {
      "uri": "https://api.example.com/v1",
      "name": "API Endpoint",
      "description": "External service interface"
    }
  ]
}
  • uri:根的唯一标识,支持 file://、https:// 或自定义协议
  • name:人类可读的名称,用于标识根
  • description:可选的描述,说明根的用途

2.5.3 工作原理

当客户端支持根时,其交互流程如下:

  1. 声明能力:客户端在连接时通过初始化消息声明支持根
  2. 提供根列表:客户端发送 roots/set 请求,向服务端提供建议的根列表
  3. 动态更新:根变更时,客户端发送 notifications/roots/changed 通知
  4. 服务端处理:服务端根据根定位资源,优先操作根内的数据

特点:

  • 根是建议性而非强制性,服务端可选择忽略
  • 服务端应验证根 URI 的有效性,防止安全风险
  • 支持多根并存,适合复杂项目

2.5.4 示例代码

以下是一个完整的 MCP 根服务端和客户端实现,展示根的声明、更新和资源定位。

root_server.py

py 复制代码
"""
MCP 服务端(root_server.py)
"""

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
from typing import List, Dict, Optional
from urllib.parse import urlparse

app = Server("root-server")

class RootManager:
    """管理根的类"""
    def __init__(self):
        self.roots: List[Dict] = []

    def set_roots(self, roots: List[Dict]):
        """设置根列表"""
        self.roots = roots

    def get_resources(self) -> List[types.Resource]:
        """根据根生成资源列表"""
        resources = []
        for root in self.roots:
            uri = root["uri"]
            name = root["name"]
            # 模拟根据根生成资源
            if uri.startswith("file://"):
                resources.append(
                    types.Resource(
                        uri=f"{uri}/app.log",
                        name=f"{name} - 日志",
                        description=f"来自 {name} 的日志文件",
                        mimeType="text/plain"
                    )
                )
            elif uri.startswith("https://"):
                resources.append(
                    types.Resource(
                        uri=uri,
                        name=f"{name} - 接口",
                        description=f"来自 {name} 的 API 数据",
                        mimeType="application/json"
                    )
                )
        return resources

root_manager = RootManager()

@app.set_roots()
async def set_roots(roots: List[Dict]) -> None:
    """接收客户端提供的根"""
    root_manager.set_roots(roots)
    print("收到根:", roots)

@app.list_resources()
async def list_resources() -> List[types.Resource]:
    """返回基于根的资源列表"""
    return root_manager.get_resources()

@app.on_notification("roots/changed")
async def on_roots_changed(params: Dict):
    """处理根变更通知"""
    roots = params.get("roots", [])
    root_manager.set_roots(roots)
    print("根已更新:", roots)

async def main():
    async with stdio_server() as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())

if __name__ == "__main__":
    import anyio
    anyio.run(main)

root_client.py

py 复制代码
"""
MCP 客户端(root_client.py)
"""

from mcp.client import stdio_client, ClientSession
import asyncio

async def main():
    async with stdio_client(command="python root_server.py") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            # 初始化会话,声明根支持
            await session.initialize(capabilities={"roots": True})

            # 设置初始根
            initial_roots = [
                {
                    "uri": "file:///home/user/projects/frontend",
                    "name": "前端代码库",
                    "description": "前端项目目录"
                },
                {
                    "uri": "https://api.example.com/v1",
                    "name": "API 端点",
                    "description": "外部服务接口"
                }
            ]
            set_roots_request = {
                "method": "roots/set",
                "params": {"roots": initial_roots}
            }
            await session.send_request(set_roots_request, None)
            print("已发送根:", initial_roots)

            # 查询资源
            list_resources_request = {"method": "resources/list", "params": {}}
            resources = await session.send_request(list_resources_request, dict)
            print("可用资源:", resources)

            # 模拟根变更
            updated_roots = [
                {
                    "uri": "file:///home/user/projects/backend",
                    "name": "后端代码库",
                    "description": "后端项目目录"
                }
            ]
            await session.notify("roots/changed", {"roots": updated_roots})
            print("已通知根变更:", updated_roots)

            # 再次查询资源
            resources = await session.send_request(list_resources_request, dict)
            print("更新后的资源:", resources)

if __name__ == "__main__":
    asyncio.run(main)

2.6 Transports

传输(Transports)是 MCP 中负责客户端与服务器消息交换的核心组件,为资源、提示、工具等功能提供底层通信支持。

2.6.1 消息格式

MCP 使用 JSON-RPC 2.0 作为其传输格式。传输层负责将 MCP 协议消息转换为 JSON-RPC 格式进行传输,并将收到的 JSON-RPC 消息转换回 MCP 协议消息。

主要消息类型包括:

  1. 请求(Request)
    请求从客户端发送到服务器或反之亦然,以启动操作。
javascript 复制代码
{
  jsonrpc: "2.0",
  id: number | string,
  method: string,
  params?: object
}
  • 请求必须包含字符串或整数 ID
  • 与基本 JSON-RPC 不同,ID 不能为 null
  • 请求 ID 一定不能被请求者在同一个会话中使用过
  1. 响应(Response)
    响应是为了回复请求而发送的,其中包含操作的结果(Results)或错误(Errors)。
javascript 复制代码
{
  jsonrpc: "2.0",
  id: number | string,
  result?: object,
  error?: {
    code: number,
    message: string,
    data?: unknown
  }
}
  • 响应必须包含与其对应的请求相同的 ID
  • 响应进一步细分为成功结果或错误。必须设置 result 或 error,且响应不得将两者同时设置
  • result 可以遵循任何 JSON 对象结构,而 error 必须至少包含错误代码和消息
  • 错误代码必须是整数
  1. 通知(Notifications)
    通知以单向消息的形式从客户端发送到服务器或从服务器发送到客户端,接收方不得发送响应。
javascript 复制代码
{
  jsonrpc: "2.0";
  method: string;
  params?: {
    [key: string]: unknown;
  };
}
  • 通知中不得包含 ID
  • 这些格式支持 MCP 组件的通信需求,如工具调用结果返回或资源变更通知。
  • JSON-RPC 的结构兼顾同步(请求-响应)和异步(通知)交互。

2.6.2 内置传输类型

MCP 为客户端-服务端通信定义了两种标准传输机制:

  • stdio:通过标准输入和标准输出进行通信
  • Streamable HTTP:可流式传输的 HTTP
2.6.2.1 标准输入/输出 (stdio)
  1. 传输机制

    1. 客户端将 MCP 服务器作为子进程启动
    2. 服务端从其标准输入(stdin)读取 JSON-RPC 消息并将消息发送到其标准输出(stdout)
    3. 消息可能是JSON-RPC请求、通知、响应,或包含一个或多个请求或通知的批量 JSON-RPC
    4. 消息由换行符分隔,并且不得包含嵌入的换行符
    5. 服务器可以将 UTF-8 字符串写入其标准错误(stderr)以用于日志记录。客户端可以捕获、转发或忽略此日志记录
    6. 服务器不得将任何非有效 MCP 消息写入其 stdout
    7. 客户端不得向服务器的 stdin 写入任何无效的 MCP 消息
  2. 适用场景

    • 构建命令行工具,如访问本地文件系统
    • 使用 Shell 脚本
    • 实施本地整合,如 IDE 插件
    • 需要简单的流程沟通,低延迟、轻量级集成
  3. 实现示例

    服务器通过 Stdio 提供资源列表,客户端获取并打印

file-server.py

py 复制代码
"""
MCP 服务端
"""
from mcp.server import Server, stdio_server

app = Server("file-server")

@app.list_resources()
async def list_resources():
    """返回可用资源列表"""
    return [{"uri": "file:///logs/app.log", "name": "应用日志"}]

async with stdio_server() as streams:
    await app.run(streams[0], streams[1])

file-client.py

py 复制代码
"""
MCP 客户端
"""
from mcp.client import stdio_client, ClientSession

async with stdio_client(command="./file-server") as streams:
    async with ClientSession(streams[0], streams[1]) as session:
        resources = await session.send_request({"method": "resources/list"}, list)
        print(resources)  # [{"uri": "file:///logs/app.log", "name": "应用日志"}]
2.6.2.2 可流式传输的 HTTP

在 Streamable HTTP 传输中,服务器作为一个独立的进程运行,可以处理多个客户端连接。此传输使用 HTTP POST 和 GET 请求,因此服务端必须提供单个 HTTP 端点路径(MCP 端点)支持 POST 和 GET 方法。

  1. 传输机制
  2. 发送消息到服务器
    从客户端发送的每个 JSON-RPC 消息都必须是向 MCP 端点发出的新 HTTP POST 请求。
  • 客户端必须使用 HTTP POST 将 JSON-RPC 消息发送到 MCP 端点
  • 客户端必须包含一个 Accept 标头,列出 application/json 和 text/event-stream 作为支持的内容类型
  • POST 请求的主体必须是下列之一:
    • 单个 JSON-RPC 请求 、 通知或响应
    • 一个或多个请求、通知或响应(批处理)
  • 输入仅由(任意数量的)JSON-RPC 响应或通知组成:
    • 如果服务端接受输入,则服务器必须返回 HTTP 状态代码 202 Accepted,且不返回任何正文
    • 如果服务端无法接受输入,它必须返回 HTTP 错误状态代码(例如 400 Bad Request)。HTTP 响应主体可以包含没有 id 的 JSON-RPC 错误响应
  • 输入包含任意数量的 JSON-RPC 请求 ,则服务端必须返回 Content-Type: text/event-stream ,以启动 SSE 流,或者 Content-Type: application/json ,返回一个 JSON 对象。客户端必须支持这两种情况
  • 如果服务端启动 SSE 流:
    • SSE 流最终应包括 POST 正文中发送的每个 JSON-RPC 请求的一个 JSON-RPC 响应
    • 服务器可以在发送 JSON-RPC 响应之前发送 JSON-RPC 请求和通知
    • 服务器不应该在发送 JSON-RPC 响应之前关闭 SSE 流,除非会话到期
    • 发送所有 JSON-RPC 响应后,服务器应该关闭 SSE 流
    • 断开连接可能随时发生,因此:
      • 断开连接不应被解释为客户端取消其请求
      • 如需取消,客户端应明确发送 MCP CancelledNotification
      • 为了避免因断开连接而丢失消息,服务器应使流可恢復
  1. 监听来自服务器的消息
    客户端可以向 MCP 端点发出 HTTP GET。这可用于打开 SSE 流,从而允许服务端与客户端通信,而无需客户端先通过 HTTP POST 发送数据。
  • 客户端必须包含一个 Accept 标头,将 text/event-stream 列为支持的内容类型
  • 服务端必须返回 Content-Type: text/event-stream 来响应此 HTTP GET,或者返回 HTTP 405 方法不允许,表明服务端在此端点不提供 SSE 流
  • 如果服务端启动 SSE 流
    • 服务端可以在流上发送 JSON-RPC 请求和通知
    • 这些消息应与来自客户端的任何并发运行的 JSON-RPC 请求无关
    • 除非恢复与先前客户端请求相关的流,否则服务端不得在流上发送 JSON-RPC 响应
    • 服务端可以随时关闭 SSE 流
    • 客户端可以随时关闭 SSE 流
  1. 多个连接
  • 客户端可以同时保持与多个 SSE 流的连接
  • 服务端必须仅在一个连接的流上发送其每个 JSON-RPC 消息;即不能在多个流上广播相同的消息
  1. 可恢复性和重新传输
    支持恢复断开的连接并重新传递可能丢失的消息:
  • 服务端可以将 id 字段附加到其 SSE 事件中(ID必须在该会话中的所有流中唯一)
  • 客户端希望在连接中断后恢复连接,它应该发出 HTTP GET 到 MCP 端点,并包括 Last-Event-ID 标头来指示其收到的最后一个事件 ID
    • 服务器可以使用此标头来重播在断开连接的流上的最后一个事件 ID 之后发送的消息,并从该点恢复流
    • 服务器不得重播已在不同流上传递的消息
      换句话说,这些事件 ID 由服务器根据每个流分配,以充当该特定流内的光标。
  1. 会话管理
    MCP "会话" 由客户端和服务端之间逻辑相关的交互组成。
  • 使用 Streamable HTTP 传输的服务端可以在初始化时分配会话 ID,通过将其包含在 InitializeResult HTTP 响应的 Mcp-Session-Id 标头中
    • 会话 ID 应该是全局唯一的并且加密安全的(UUID、JWT、加密哈希)
    • 会话 ID 必须仅包含可见的 ASCII 字符(范围从 0x21 到 0x7E)
  • 如果服务端在初始化期间返回了 Mcp-Session-Id,则使用 Streamable HTTP 传输的客户端必须在所有后续 HTTP 请求中携带 Mcp-Session-Id
    • 需要会话 ID 的服务端应该响应没有 Mcp-Session-Id 标头(初始化除外)的请求 400 错误
  • 服务端可以随时终止会话,之后它必须使用 HTTP 404 Not Found 响应包含该会话 ID 的请求
  • 当客户端收到包含 HTTP 404 响应时,它必须通过发送新的 InitializeRequest 来开始一个新的会话
  • 不再需要特定会话的客户端应向 MCP 端点发送 HTTP DELETE,其中包含 Mcp-Session-Id 标头,明确终止会话
  1. 适用场景
  • 仅需要服务端到客户端的流式传输
  • 云服务集成,如 AWS 资源管理
  • 实时应用,如数据库监控
  1. 实现示例
    服务器通过 HTTP SSE 提供工具列表,客户端通过 POST 请求查询
    cloud-server.py
py 复制代码
"""
MCP 服务端
"""

from mcp.server import Server
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route
import mcp.types as types

# 初始化 MCP 服务器
app = Server("cloud-server")
sse = SseServerTransport("/messages")

# 定义工具列表
@app.list_tools()
async def list_tools() -> list[types.Tool]:
    """返回可用工具列表"""
    return [
        types.Tool(
            name="check_aws_status",
            description="检查 AWS 资源状态",
            inputSchema={
                "type": "object",
                "properties": {"resource_id": {"type": "string"}},
                "required": ["resource_id"]
            }
        )
    ]

# 处理 SSE 连接
async def handle_sse(scope, receive, send):
    async with sse.connect_sse(scope, receive, send) as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())

# 处理 POST 消息
async def handle_messages(scope, receive, send):
    await sse.handle_post_message(scope, receive, send)

# 配置 Starlette 路由
starlette_app = Starlette(
    routes=[
        Route("/messages", endpoint=handle_sse, methods=["GET"]),  # SSE 流
        Route("/messages", endpoint=handle_messages, methods=["POST"])  # POST 请求
    ]
)

cloud-client.py

py 复制代码
"""
MCP 客户端
"""
from mcp.client import sse_client, ClientSession
import asyncio

async def main():
    # 连接到服务器
    async with sse_client("http://localhost:8000/messages") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            # 初始化会话
            await session.initialize()
            # 发送工具列表请求
            request = {"method": "tools/list", "params": {}}
            tools = await session.send_request(request, list)
            print("可用工具:", tools)

if __name__ == "__main__":
    asyncio.run(main())

JSON 请求与响应:

javascript 复制代码
// 客户端请求
{
    "jsonrpc": "2.0",
    "id": "req-002",
    "method": "tools/list",
    "params": {}
}

// 服务器 SSE 响应(流式)
data: {"jsonrpc": "2.0", "id": "req-002", "result": [{"name": "check_aws_status", "description": "检查 AWS 资源状态"}]}
2.6.2.3 自定义传输

MCP 的传输层支持扩展,开发者可实现自定义传输以满足特定需求,只需遵循 Transport 接口。

  • 接口定义
    • TypeScript 中 Transport 接口,定义了初始化、消息发送和关闭的抽象方法
javascript 复制代码
interface Transport {
  // Start processing messages
  start(): Promise<void>;

  // Send a JSON-RPC message
  send(message: JSONRPCMessage): Promise<void>;

  // Close the connection
  close(): Promise<void>;

  // Callbacks
  onclose?: () => void;
  onerror?: (error: Error) => void;
  onmessage?: (message: JSONRPCMessage) => void;
}
  • Python 基于传输上下文管理器和 anyio 库实现
py 复制代码
from contextlib import contextmanager 
import anyio 
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream

@contextmanager
async def create_transport(
    read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
    write_stream: MemoryObjectSendStream[JSONRPCMessage]
):
    """
    Transport interface for MCP.

    Args:
        read_stream: Stream to read incoming messages from
        write_stream: Stream to write outgoing messages to
    """
    # 创建一个异步任务组,用于并发执行消息处理任务
    async with anyio.create_task_group() as tg:
        try:
            # Start processing messages
            tg.start_soon(lambda: process_messages(read_stream))

            # Send messages
            async with write_stream:
                yield write_stream

        except Exception as exc:
            # Handle errors
            raise exc
        finally:
            # Clean up
            tg.cancel_scope.cancel()
            await write_stream.aclose()
            await read_stream.aclose()
  • 适用场景
    • 自定义网络协议,如 WebSocket 用于低延迟应用
    • 与现有系统集成,如专有消息队列
    • 性能优化,如二进制协议支持高吞吐量

2.6.3 错误处理

传输层需处理多种错误场景,确保通信的鲁棒性:

  1. 连接错误(Connection errors):处理网络中断或服务器不可用
  2. 消息解析错误(Message parsing errors):验证 JSON-RPC 格式,拒绝无效消息
  3. 协议错误(Protocol errors):处理无效方法或参数(如 -32601 表示方法不存在)
  4. 网络超时(Network timeouts):实现重试或优雅降级
  5. 资源清理(Resource cleanup):确保流或进程的正确关闭

示例:服务器捕获连接和消息错误,通过 stderr 输出日志

py 复制代码
import anyio
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from contextlib import asynccontextmanager
import logging
from typing import Dict, Union, AsyncIterator
import json

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@asynccontextmanager
async def custom_mcp_transport(
    scope: dict = None,  # 模拟 Starlette 风格的 scope
    receive: callable = None,  # 模拟接收消息的函数
    send: callable = None  # 模拟发送消息的函数
) -> AsyncIterator[tuple[MemoryObjectReceiveStream[Dict], MemoryObjectSendStream[Dict]]]:
    """
    自定义 MCP 传输,基于 anyio 内存流实现双向通信。

    Args:
        scope: 传输上下文(如 HTTP 请求元数据),此处模拟使用
        receive: 接收消息的异步函数(模拟输入)
        send: 发送消息的异步函数(模拟输出)

    Yields:
        tuple: (read_stream, write_stream),用于接收和发送 JSON-RPC 消息
    """
    try:
        # 创建内存流用于双向通信
        read_stream_writer, read_stream = anyio.create_memory_object_stream[Dict](max_buffer_size=10)
        write_stream, write_stream_reader = anyio.create_memory_object_stream[Dict](max_buffer_size=10)

        async def message_handler():
            """处理接收到的消息"""
            try:
                async with read_stream_writer:
                    # 模拟从输入通道接收消息
                    if receive:
                        async for message in receive():
                            try:
                                # 验证 JSON-RPC 消息格式
                                if not isinstance(message, dict) or "jsonrpc" not in message:
                                    raise ValueError("无效的 JSON-RPC 消息")
                                await read_stream_writer.send(message)
                                logger.info(f"接收到消息: {message}")
                            except Exception as e:
                                logger.error(f"消息处理失败: {e}")
                                await read_stream_writer.send(e)
            except Exception as e:
                logger.error(f"消息处理循环中断: {e}")
                raise

        async def message_sender():
            """处理发送消息"""
            try:
                async with write_stream_reader:
                    async for message in write_stream_reader:
                        if send:
                            # 模拟发送消息到外部
                            await send({"type": "jsonrpc", "body": message})
                            logger.info(f"发送消息: {message}")
            except Exception as e:
                logger.error(f"消息发送失败: {e}")
                raise

        async with anyio.create_task_group() as tg:
            # 启动消息处理和发送任务
            tg.start_soon(message_handler)
            tg.start_soon(message_sender)
            try:
                # 提供流给调用者
                yield read_stream, write_stream
            except Exception as e:
                logger.error(f"传输运行时错误: {e}")
                raise
            finally:
                # 清理资源
                tg.cancel_scope.cancel()
                await write_stream.aclose()
                await read_stream.aclose()
                await read_stream_writer.aclose()
                await write_stream_reader.aclose()
                logger.info("传输已关闭")
    except Exception as e:
        logger.error(f"传输初始化失败: {e}")
        raise

# 示例使用
async def mock_receive():
    """模拟外部消息输入"""
    messages = [
        {"jsonrpc": "2.0", "method": "resources/list", "params": {}, "id": "req-001"},
        {"jsonrpc": "2.0", "method": "invalid", "params": {}}  # 无效消息
    ]
    for msg in messages:
        yield msg
        await anyio.sleep(1)

async def mock_send(message: dict):
    """模拟外部消息输出"""
    print(f"模拟发送: {message}")

async def main():
    """使用自定义传输"""
    async with custom_mcp_transport(
        scope={"type": "mcp"},
        receive=mock_receive,
        send=mock_send
    ) as (read_stream, write_stream):
        # 发送测试消息
        test_message = {"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": "req-002"}
        await write_stream.send(test_message)

        # 接收并处理消息
        async for message in read_stream:
            if isinstance(message, Exception):
                print(f"接收到错误: {message}")
            else:
                print(f"接收到消息: {message}")
                # 模拟响应
                response = {"jsonrpc": "2.0", "id": message.get("id"), "result": "ok"}
                await write_stream.send(response)

if __name__ == "__main__":
    anyio.run(main)


# 在 MCP 服务器中,将 custom_mcp_transport 作为传输层
from mcp.server import Server

app = Server("test-server")
async with custom_mcp_transport() as (read_stream, write_stream):
    await app.run(read_stream, write_stream)
相关推荐
路边的阿不15 小时前
自己写一个智能体-使用MCP服务
agent·mcp
带刺的坐椅16 小时前
从 Chat 到 Agent:Solon AI 带你进入“行动派”大模型时代
java·ai·agent·solon·mcp·java25
薛定谔的猫22 天前
Cursor 系列(3):关于MCP
前端·cursor·mcp
王国强20092 天前
Unla MCP 网关代理配置教程
mcp
kagg8862 天前
mcp-gateway —— 隐藏mcp上下文以避免不必要的tokens开销
llm·mcp
AAA阿giao2 天前
qoder-cli:下一代命令行 AI 编程代理——全面解析与深度实践指南
开发语言·前端·人工智能·ai编程·mcp·context7·qoder-cli
饭勺oO3 天前
AI 编程配置太头疼?ACP 帮你一键搞定,再也不用反复折腾!
ai·prompt·agent·acp·mcp·skills·agent skill
AlienZHOU3 天前
MCP 是最大骗局?Skills 才是救星?
agent·mcp·vibecoding
Linux内核拾遗3 天前
人人都在聊 MCP,它到底解决了什么?
aigc·ai编程·mcp