MCP 完全指南:从零开始掌握模型上下文协议

引言:LLM 应用面临的集成困境

大语言模型的能力边界正在被不断拓宽------从单纯的文本生成,到能够调用工具、读取数据源、执行操作。但一个根本性问题始终存在:每个 AI 应用与每个外部系统之间,都需要一套独立的集成代码。

假设你有 5 个内部系统(GitHub、Jira、PostgreSQL、文件系统、Slack)和 3 个 AI 客户端(Claude Desktop、Cursor、OpenAI 助手)。为了让每个 AI 都能访问每个系统,你需要开发 15 套不同的集成逻辑。这就是经典的 N×M 集成问题:N 个客户端 × M 个数据源,每一对都需要单独的适配器。

MCP(Model Context Protocol,模型上下文协议)正是为解决这一问题而诞生的。

第一章:MCP 的定义与技术分层

1.1 核心定义

MCP(Model Context Protocol) 是由 Anthropic 发起、现由 Linux Foundation Agentic AI Foundation 治理的开放标准协议。它定义了一套标准化接口,让 LLM 应用能够以统一的方式连接外部工具、数据源和提示模板。

MCP 借鉴了开发工具中 LSP(Language Server Protocol)的成功经验------LSP 统一了编辑器与语言服务器之间的通信,MCP 则统一了 AI 应用与外部系统之间的通信。

1.2 MCP 在技术栈中的精确分层:它定义了哪一层?

理解 MCP 最关键的认知门槛在于:MCP 不定义 LLM 如何被调用,而是定义 LLM 周围的环境如何被接入。

我们可以用经典的三层架构来精确定位 MCP 的职责范围:

层级 组件 交互协议 职责
应用表现层 Host(Claude Desktop / Cursor) 自定义 UI 逻辑 渲染界面,管理对话历史,用户授权
模型推理层 Host ↔ LLM API HTTP REST(OpenAI / Anthropic 规范) 构造 Prompt,调用推理,解析 Function Call 意图
上下文接入层 Host ↔ MCP Server MCP(JSON-RPC) 将"LLM 想要执行的动作"翻译为"系统底层操作"
基础设施层 MCP Server ↔ 外部系统 任意(SQL、文件 I/O、HTTP API) 实际执行读写、计算、网络请求

MCP 锚定的是上下文接入层(南向接口) :

  • 它不关心 Host 调用 LLM 时用的是 OpenAI 的 tools 参数还是 Anthropic 的 functions 参数。那是模型推理层的差异。
  • 它只定义 Host 与 Server 之间如何发现能力(tools/list)、如何执行操作(tools/call)、如何读取内容(resources/read)。
  • 这意味着:即使未来出现全新的 LLM 推理协议,只要 Host 内部适配了新的映射逻辑,已有的 MCP Server 无需修改一行代码依然可以工作。

关键点:MCP 定义的是 LLM 周围的可插拔生态环境,而不是 LLM 内部的推理逻辑。如果你要调优 LLM 的吐字风格,修改的是 Host 调用 API 的 System Prompt;如果你要新增一个计算器或连接一个新数据库,你编写的是 MCP Server。两者互不干扰。

1.3 MCP 的设计目标

MCP 的设计围绕四个核心原则展开:

  • 服务器应极易构建:Host 负责复杂的编排工作,服务器只聚焦于明确定义的能力。
  • 服务器应高度可组合:每个服务器提供独立功能,多个服务器可以无缝组合。
  • 服务器不应能读取完整对话:服务器只接收必要的上下文信息,完整对话历史保留在 Host 端。这一设计从根本上限制了 Server 的权限边界。
  • 功能可渐进式添加:核心协议提供最小功能集,额外能力可通过协商按需启用,避免协议膨胀。

第二章:架构三要素------Host、Client、Server

MCP 采用 客户端-宿主-服务器 架构,包含三个核心角色。

2.1 Host(宿主)

Host 是运行 AI 模型的应用,充当容器和协调者:

  • 创建并管理多个 Client 实例
  • 控制 Client 连接权限和生命周期
  • 执行安全策略和用户授权决策
  • 协调 AI/LLM 集成和采样
  • 跨 Client 聚合上下文

常见 Host:Claude Desktop、Cursor、Windsurf、VS Code。

2.2 Client(客户端)

Client 是运行在 Host 内部的协议层组件:

  • 与单个 Server 建立一对一的持久连接
  • 处理协议协商和能力交换
  • 双向路由协议消息
  • 管理订阅和通知
  • 维护 Server 之间的安全边界

关键约束:每个 Client 只连接一个 Server;每个 Server 同时只服务一个 Client;一个 Host 可以运行多个 Client 并行工作。

2.3 Server(服务器)

Server 是暴露能力的独立进程:

  • 通过 MCP 原语暴露 Resources、Tools 和 Prompts
  • 独立运行,聚焦特定职责
  • 可通过 Client 接口请求采样(Server-Side Sampling)
  • 可以是本地进程或远程服务

常见 Server:filesystem(文件系统访问)、postgres(数据库查询)、slack(团队协作)、github(代码管理)。

2.4 架构关系图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                        Host 主机应用                        │
│              (Claude Desktop / Cursor / IDE)               │
│                                                             │
│    ┌──────────────┐              ┌──────────────┐          │
│    │ MCP Client   │              │ MCP Client   │          │
│    │   (1:1)      │              │   (1:1)      │          │
│    └──────┬───────┘              └──────┬───────┘          │
└───────────┼──────────────────────────────┼─────────────────┘
            │         传输层                │
            │   (stdio / HTTP+SSE)         │
            ▼                              ▼
    ┌───────────────┐             ┌───────────────┐
    │  MCP Server   │             │  MCP Server   │
    │  (独立进程)   │             │  (独立进程)   │
    │  文件系统     │             │  GitHub API   │
    └───────────────┘             └───────────────┘

架构的四个核心约束:

  1. Client 与 Server 是 1:1 关系,一个 Client 实例只服务于一个 Server
  2. Host 与 Client 是 1:N 关系,一个 Host 可管理多个 Client
  3. Server 不感知其他 Server 的存在,保持隔离
  4. 所有协议消息都通过传输层(stdio 或 HTTP+SSE)以 JSON-RPC 格式传递

第三章:协议基础------从一次真实对话理解 JSON-RPC

MCP 的所有消息都必须遵循 JSON-RPC 2.0 规范。与其枯燥地罗列字段,不如直接看一段 MCP Client 与 Server 之间的真实通信日志。

3.1 一次完整的协议握手(抓包视图)

以下是 Client(Host)与 Server 建立连接时的完整对话。我们逐行拆解:

text 复制代码
[1]  Client → Server   (请求)
    {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"0.1.0","capabilities":{}}}
    
[2]  Server → Client   (响应)
    {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"0.1.0","capabilities":{"tools":{}}}}
    
[3]  Client → Server   (通知)
    {"jsonrpc":"2.0","method":"notifications/initialized"}

[4]  Client → Server   (请求:列出工具)
    {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
    
[5]  Server → Client   (响应:返回工具列表)
    {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"add","description":"加法","inputSchema":{...}}]}}

[6]  Client → Server   (请求:调用工具)
    {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":3,"b":5}}}
    
[7]  Server → Client   (响应:返回执行结果)
    {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"8"}]}}

补充:stdio 传输中的消息分帧

在上述 stdio 传输模式中,每条 JSON-RPC 消息在底层是以换行符 \n 作为分隔符的。Server 通过按行读取 stdin,将每行解析为一条独立的 JSON-RPC 消息。因此,每条消息必须且只能占一行,不能出现跨行的 JSON 格式化(即紧凑格式,无多余空白字符)。这也是为什么抓包日志中的 JSON 看起来是压缩成一行展示的。

3.2 三种消息类型的区分

从上面的对话中,你可以看到三种截然不同的消息形态:

① Request(请求)------ 带 ID,期待响应

第 1、4、6 行属于 Request。特征:

  • 包含 "id" 字段(数字或字符串)
  • 包含 "method" 字段(要执行的操作名)
  • 包含 "params" 字段(操作参数)
  • 发送方期待接收方返回一个同 ID 的 Response

② Response(响应)------ 带相同 ID,返回结果或错误

第 2、5、7 行属于 Response。特征:

  • 包含与 Request 相同的 "id"
  • 包含 "result" 或 "error" 字段(二选一)
  • 不包含 "method" 字段

③ Notification(通知)------ 无 ID,不期待响应

第 3 行属于 Notification。特征:

  • 没有 "id" 字段
  • 包含 "method" 字段
  • 发送方不期待任何响应

关键点:Request 与 Notification 的唯一区别就是有没有 id 。有 id 意味着对方必须回复;没有 id 意味着发完即止,对方无需应答。

3.3 协议中的方法名约定

MCP 规范定义了标准的方法名,采用命名空间前缀来组织:

命名空间 方法示例 方向 说明
initialize initialize C→S 协议握手(唯一不带命名空间前缀的方法)
tools/ tools/list, tools/call C→S 工具发现与调用
resources/ resources/list, resources/read C→S 资源发现与读取
prompts/ prompts/list, prompts/get C→S 提示模板发现与获取
sampling/ sampling/createMessage S→C Server 请求 LLM 采样
notifications/ notifications/initialized C→S 初始化完成通知

3.4 错误响应的标准格式

当 Server 无法处理某个请求时,必须返回标准格式的错误响应:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32601,
    "message": "Method not found"
  }
}

MCP 沿用了 JSON-RPC 2.0 的标准错误码,并扩展了自定义错误码(从 -32000 到 -32099):

错误码 含义
-32700 解析错误(JSON 格式无效)
-32600 无效请求(不符合 JSON-RPC 规范)
-32601 方法不存在
-32602 参数无效
-32603 内部错误
-32000 ~ -32099 服务器自定义错误

第四章:能力协商------协议握手阶段

MCP 使用基于能力的协商系统,Client 和 Server 在初始化阶段明确声明各自支持的特性。

4.1 为什么需要能力协商?

协议版本和能力声明解决了三个问题:

  1. 向后兼容:新版本的 Host 可以连接旧版本的 Server,反之亦然
  2. 功能发现:双方无需预设对方支持什么,通过声明即可获知
  3. 按需启用:Server 不需要实现全部可选功能,只需声明实现了的部分

4.2 协商流程(重温第三章的抓包)

复制代码
Client                                          Server
   |                                               |
   |  --- 1. initialize (协议版本 + Client能力) --> |
   |       {"id":1,"method":"initialize",...}      |
   |                                               |
   |  <-- 2. 响应 (协议版本 + Server能力) --------- |
   |       {"id":1,"result":{...}}                 |
   |                                               |
   |  --- 3. initialized (通知, 无响应) ----------> |
   |       {"method":"notifications/initialized"}  |
   |                                               |
   |  === 进入正常运行阶段 ===                       |

第一步:Client 发送 initialize 请求

Client 在请求中声明:

  • protocolVersion:支持的协议版本(如 "0.1.0")
  • capabilities:Client 具备的能力
    • sampling:是否支持 Server 发起的 LLM 采样请求
    • roots:是否支持提供根目录列表
    • experimental:实验性能力

第二步:Server 响应 initialize

Server 在响应中返回:

  • protocolVersion:选择使用的协议版本
  • capabilities:Server 具备的能力
    • tools:是否提供工具调用能力
    • resources:是否提供资源读取能力
    • prompts:是否提供提示模板能力
    • logging:是否支持日志输出

第三步:Client 发送 initialized 通知

Client 发送此通知后,双方进入正常运行阶段。任何在 initialized 之前发送的请求(除 initialize 外)都会被拒绝。

4.3 能力决定可用方法

Server 能力 可用方法
tools tools/list, tools/call
resources resources/list, resources/read, resources/subscribe
prompts prompts/list, prompts/get
logging logging/setLevel
Client 能力 可用方法
sampling sampling/createMessage
roots roots/list

补充说明:roots 能力

roots 允许 Client 告知 Server 当前用户授权访问的文件系统根目录列表(如 VS Code 当前打开的工作区路径)。文件系统类的 Server 可利用此信息进行权限边界控制------Server 只能访问 roots 声明的目录范围内的文件,超出则拒绝访问。这相当于给 Server 划定了一个"可操作的安全范围"。

第五章:生命周期------会话的状态转换

每个 MCP 会话遵循标准的生命周期阶段。

5.1 状态定义

状态 描述 允许的操作
未初始化 连接已建立,但尚未发送 initialize 仅 initialize
初始化中 已发送 initialize,等待响应 仅 initialize 相关消息
已初始化 initialize 已响应,等待 initialized 通知 无(等待通知)
运行中 initialized 已发送,正常操作 所有协议方法
关闭中 正在关闭连接 仅清理操作

5.2 各阶段详解

阶段一:建立传输连接

  • Stdio 模式:Client 通过 fork+exec 启动 Server 子进程,管道建立
  • HTTP 模式:Client 建立 SSE 连接和 HTTP POST 通道

阶段二:初始化握手

  • Client → Server: initialize
  • Server → Client: initialize 响应
  • Client → Server: initialized 通知

注意:Server 在收到 initialize 之前,不能响应任何其他请求。Server 在收到 initialized 通知之前,虽然可以处理请求,但不应该发送需要 Client 处理的通知。

阶段三:正常运行

  • 双方可自由发送 Request/Response/Notification
  • Server 可以随时发送通知(如 tools/list_changed)

阶段四:终止

  • Stdio 模式:Client 关闭 stdin 管道,Server 检测到 EOF 后自行退出
  • Server 应优雅地释放资源

第六章:三大核心原语------Resources、Tools、Prompts

MCP 定义了三种核心原语,Server 通过它们向 Client 暴露能力。

6.1 Tools(工具)

Tools 是可执行的函数,AI 可以调用它们来执行操作。

特征:

  • 有副作用(side effects),可以修改外部状态
  • 接受结构化参数输入,返回结构化结果
  • 由 AI 根据上下文决定是否调用
  • 适合:API 调用、文件系统操作、数据库查询、数学计算

Tool 的结构:

字段 类型 说明
name string 唯一标识符,用于 tools/call 路由
description string 工具功能描述,LLM 据此决定何时调用
inputSchema JSON Schema 定义参数的 JSON Schema 对象

协议方法:

  • tools/list:获取所有可用工具
  • tools/call:执行指定工具

6.2 Resources(资源)

Resources 是只读的内容数据,可以通过 URI 定位。

特征:

  • 只读,无副作用
  • Client 可以将其内容注入 LLM 上下文
  • 无需通过工具调用即可获取,降低了 LLM 的决策负担

适用内容:配置文件、日志文件、数据库记录、文档内容。

协议方法:

  • resources/list:获取所有可用资源
  • resources/read:读取指定 URI 的内容
  • resources/subscribe:订阅资源变更(可选)

6.3 Prompts(提示)

Prompts 是可复用的提示模板,用于指导 LLM 交互。

特征:

  • 用户控制,通过用户选择触发(如斜杠命令)
  • 可以包含参数化模板
  • 适合:预定义工作流、标准化指令

协议方法:

  • prompts/list:获取所有可用提示模板
  • prompts/get:获取指定提示模板的内容

6.4 三者对比

维度 Tools Resources Prompts
调用方 AI/模型自主决策 应用/Client 主动读取 用户主动选择
是否有副作用 是 否 否
典型用途 执行操作 提供上下文 标准化交互模板
获取方式 tools/call resources/read prompts/get

第七章:MCP Server 的运行模型------独立执行与 Host 托管

7.1 Server 的本质:一个读写 stdio 的普通进程

无论以何种方式运行,任何一个 MCP Server 在操作系统层面都只是一个独立的进程。它不关心启动者是谁,只遵循一个铁律:

  • 从 标准输入(stdin) 读取 JSON-RPC 请求
  • 向 标准输出(stdout) 写入 JSON-RPC 响应
  • 向 标准错误(stderr) 写入日志(供调试)

这意味着,在终端直接执行启动命令 与由 Host 启动子进程,对于 Server 进程本身而言,完全没有区别。

7.2 模式一:直接在命令行执行(独立模式)

bash 复制代码
# 直接启动 Server(会卡住等待 stdin)
uvx mcp-server-fetch

Server 启动后阻塞在 read(stdin),等待输入。你可以手动模拟 Host:

bash 复制代码
# 发送一个 tools/list 请求
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | uvx mcp-server-fetch

用途:开发调试、单元测试、验证 Server 能否正常启动。

关键点:在这种模式下,你是临时 Host。管道关闭后 Server 进程随之退出。

7.3 模式二:由 MCP Host 托管

在 Host 配置文件中定义:

json 复制代码
{
  "mcpServers": {
    "fetch": {
      "command": "uvx",
      "args": ["mcp-server-fetch"]
    }
  }
}

Host 的行为:

  1. 执行 fork+exec 启动子进程
  2. 通过管道连接子进程的 stdin 和 stdout
  3. 自动完成完整的握手协商
  4. 管理进程生命周期

两种模式对比:

维度 命令行独立执行 Host 托管执行
输入源 终端管道 Host 内部的 stdin 管道
生命周期 随管道关闭终止 随 Host 生命周期
协议协商 需手动发送 initialize 自动完成
消息路由 无 Host 将 LLM 请求路由至此

7.4 解码 mcpServers 配置语义

字段 说明
mcpServers Host 保留字段,声明需启动的 Server 列表
"fetch" 逻辑别名,用于日志和路由
"command" 可执行文件(必须在 $PATH 中或为绝对路径)
"args" 透传参数数组
"env"(可选) 注入环境变量,隔离敏感凭证
"cwd"(可选) 设置子进程工作目录

7.5 安装机制:运行器 vs 手动安装

uvx / npx(运行器) :

  1. 检查缓存中是否存在包
  2. 若不存在,从 PyPI/NPM 下载到临时缓存
  3. 创建隔离虚拟环境,执行入口脚本

手动安装(生产推荐) :

bash 复制代码
python -m venv mcp_venv
source mcp_venv/bin/activate
pip install mcp-server-fetch==1.2.0
which mcp-server-fetch  # 获取绝对路径

配置改为:

json 复制代码
{
  "mcpServers": {
    "fetch": {
      "command": "/path/to/mcp_venv/bin/mcp-server-fetch"
    }
  }
}

关键点:无论使用运行器还是绝对路径,最终产生的进程完全一致。运行器适合快速体验,绝对路径适合生产环境锁定版本。

第八章:传输层详解------stdio 与 HTTP+SSE

8.1 日常开发:只关注 Stdio

对于本地开发,你只需要理解 stdio 模式。HTTP+SSE 模式主要用于云端多租户部署,初学者可以暂不深入。

Stdio 工作机制:

复制代码
Host 进程                            MCP Server 子进程
     |                                       |
     |  fork+exec 启动子进程                  |
     | ----------------------------------->  |
     |  建立 stdin/stdout 管道                |
     |                                       |
     |  向 stdin 写入 JSON-RPC 请求           |
     | ----------------------------------->  | 读取 stdin
     |                                       | 处理请求
     |  从 stdout 读取 JSON-RPC 响应          |
     | <-----------------------------------  | 写入 stdout

Stdio 模式的特点:

  • 消息以换行符 \n 分隔(见 3.1 节的分帧说明)
  • Server 可通过 stderr 输出调试日志
  • 进程生命周期由 Host 完全控制

适用场景:本地命令行工具、桌面应用集成、开发调试。

8.2 HTTP+SSE / Streamable HTTP(简要了解)

工作机制:

  1. Server 作为独立 HTTP 服务运行
  2. Client 建立 SSE 连接接收 Server 消息
  3. Client 通过 HTTP POST 发送 JSON-RPC 请求
  4. Server 通过 SSE 推送响应和通知

适用场景:云端部署、多租户服务、需要同时服务多个 Client 的场景。

建议:日常本地开发只需掌握 stdio 模式。当你需要将 MCP Server 部署为云端服务时,再深入研究 HTTP+SSE 的细节。

第九章:动手实战------编写一个最简 Python MCP Server

9.1 环境准备

bash 复制代码
# 使用 uv 管理项目
uv init mcp-demo
cd mcp-demo
uv add "mcp[cli]"

9.2 编写 MCP Server

创建 server.py:

python 复制代码
import asyncio
import json
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio

server = Server("demo-server")

# ============ 注册 Tool ============
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    """声明 Server 提供的所有工具"""
    return [
        types.Tool(
            name="add",
            description="将两个数字相加",
            inputSchema={
                "type": "object",
                "properties": {
                    "a": {"type": "number", "description": "第一个数字"},
                    "b": {"type": "number", "description": "第二个数字"},
                },
                "required": ["a", "b"],
            },
        )
    ]

@server.call_tool()
async def handle_call_tool(
    name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
    """处理工具调用请求"""
    # 工具返回值可以是文本、图片或嵌入式资源的混合列表,本示例仅返回纯文本
    if name == "add":
        a = arguments.get("a")
        b = arguments.get("b")
        if a is None or b is None:
            raise ValueError("缺少参数 a 或 b")
        return [types.TextContent(type="text", text=str(a + b))]
    raise ValueError(f"未知工具: {name}")

# ============ 注册 Resource ============
@server.list_resources()
async def handle_list_resources() -> list[types.Resource]:
    """声明 Server 提供的所有资源"""
    return [
        types.Resource(
            uri="demo://config",
            name="配置信息",
            description="示例配置数据",
            mimeType="application/json",
        )
    ]

@server.read_resource()
async def handle_read_resource(uri: str) -> str:
    """处理资源读取请求"""
    if uri == "demo://config":
        return json.dumps({"version": "1.0", "name": "MCP Demo"})
    raise ValueError(f"未知资源: {uri}")

# ============ 主入口 ============
async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="demo-server",
                server_version="1.0.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

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

9.3 方式一:命令行独立运行

bash 复制代码
# 直接启动(会卡住等待 stdin)
python server.py

# 在另一个终端发送请求(模拟 Host)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python server.py

预期输出:

json 复制代码
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"add","description":"将两个数字相加","inputSchema":{"type":"object","properties":{"a":{"type":"number","description":"第一个数字"},"b":{"type":"number","description":"第二个数字"}},"required":["a","b"]}}]}}

9.4 方式二:通过 Client 托管运行

创建 client.py:

python 复制代码
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    # 配置 Server 参数------等同于 Host 配置文件中的 "command" + "args"
    server_params = StdioServerParameters(
        command="python",   # 等同于 "command"
        args=["server.py"], # 等同于 "args"
    )

    # 建立连接------Client 会自动 fork+exec 启动子进程
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 初始化会话(内部自动完成 initialize 请求 → 响应 → initialized 通知的完整握手)
            # 对应第三章 3.1 节抓包视图中的 [1][2][3] 步
            await session.initialize()

            # 1. 列出所有工具
            tools = await session.list_tools()
            print("可用工具:")
            for tool in tools.tools:
                print(f"  - {tool.name}: {tool.description}")

            # 2. 调用工具
            result = await session.call_tool("add", {"a": 3, "b": 5})
            print(f"\nadd(3, 5) = {result.content[0].text}")

            # 3. 列出所有资源
            resources = await session.list_resources()
            print("\n可用资源:")
            for resource in resources.resources:
                print(f"  - {resource.name}: {resource.uri}")

            # 4. 读取资源
            content = await session.read_resource("demo://config")
            print(f"\nconfig 内容: {content.contents[0].text}")

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

运行:

bash 复制代码
python client.py

预期输出:

复制代码
可用工具:
  - add: 将两个数字相加

add(3, 5) = 8

可用资源:
  - 配置信息: demo://config

config 内容: {"version": "1.0", "name": "MCP Demo"}

9.5 方式三:配置到 Claude Desktop

在 claude_desktop_config.json 中添加:

json 复制代码
{
  "mcpServers": {
    "demo": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}

重启 Claude Desktop 即可。

9.6 三种方式的本质统一

注意这三种方式的关系:

  • 方式一(命令行独立运行) :证明了 Server 的协议独立性------它只是一个处理 stdin/stdout 的进程。
  • 方式二(自定义 Client) :展示了 Host 如何通过代码启动子进程并建立管道。
  • 方式三(外部 Host) :则是生产环境中用户最常面对的配置形态。

三者的底层机制完全一致 ------都是 fork+exec 创建子进程,通过管道读写 JSON-RPC。

9.7 常见问题排查(Troubleshooting)

问题 1:Windows 下 python 命令找不到

在 Windows 中,python 可能不在 PATH 中,或者指向的是 Microsoft Store 版本。

解决方案:

  • 使用 py 代替 python(Windows 的 Python Launcher)
  • 或使用绝对路径:"command": "C:\\Python312\\python.exe"
json 复制代码
{
  "mcpServers": {
    "demo": {
      "command": "py",
      "args": ["C:\\path\\to\\server.py"]
    }
  }
}

问题 2:uvx 包下载失败或版本不兼容

uvx 依赖网络,且默认拉取最新版本。生产环境建议锁定版本并本地安装:

bash 复制代码
# 查看可用版本
uv pip install mcp-server-fetch==  # 会列出所有版本

# 安装到项目虚拟环境
uv add mcp-server-fetch==1.2.0

问题 3:Server 启动后卡住不退出

在命令行独立运行时,Server 会一直等待 stdin 输入。按 Ctrl+C 发送 SIGINT 信号终止进程。

在 Host 托管模式下,Host 关闭时会自动终止子进程。如果僵尸进程残留,可以手动 kill:

bash 复制代码
# Linux/macOS
ps aux | grep mcp
kill -9 <PID>

# Windows (PowerShell)
Get-Process python* | Where-Object { $_.Path -like "*mcp*" } | Stop-Process -Force

问题 4:stderr 日志不显示

Server 的 stderr 输出在 Host 托管模式下可能被重定向到 Host 的日志系统。

  • Claude Desktop:查看 ~/Library/Logs/Claude/(macOS)或 %APPDATA%\Claude\logs\(Windows)
  • 自定义 Client:stderr 默认输出到终端,可直接查看

问题 5:路径中包含空格导致启动失败

如果 server.py 的路径包含空格(如 My Documents),需要用引号包裹:

json 复制代码
{
  "command": "python",
  "args": ["\"C:\\My Documents\\server.py\""]
}

或使用短路径名(Windows 的 PROGRA~1 风格)。

问题 6:类型注解在旧版 Python 中报错

如果你使用的是 Python 3.8 或更早版本,联合类型语法 X | Y 不被支持。

解决方案:使用 typing.Union:

python 复制代码
from typing import Union

async def handle_call_tool(
    name: str, arguments: dict | None
) -> list[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:
    # ...

或在 Python 3.10+ 环境中运行(推荐)。

结语

MCP 的本质是一个 标准化的上下文接入层协议,它让 AI 应用与外部世界的连接从"点对点的定制集成"转变为"基于统一协议的即插即用"。

对于开发者而言,掌握 MCP 需要建立两个关键认知:

  1. 横向看分层:MCP 不定义 LLM 如何被调用(那是模型推理层的职责),而是定义 LLM 周围的环境(工具、数据、提示)如何被接入。它锚定的是南向接口,是 LLM 应用架构中的"外设驱动层"。

  2. 纵向看进程 :MCP Server 只是一个读写 stdin/stdout 的普通进程。无论你在终端用 echo 管道测试,还是由 Host 通过 fork+exec 托管,Server 本身不感知调用者的差异。uvx 是包管理工具的行为,command 是操作系统的进程启动行为,而 MCP 协议仅定义进程启动后,stdin/stdout 中流淌的数据格式。

从 2024 年 11 月发布至今,MCP 已经从 Anthropic 的内部项目成长为 Linux Foundation 治理下的行业标准。理解其协议设计背后的分层哲学和进程模型,是深入参与这一生态的基石。

相关推荐
Csvn5 小时前
第 28 章 案例四 多智能体协作系统
人工智能·aigc·agent
IT_陈寒5 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
Thneonl5 小时前
Celery 生产踩坑:1000 任务积压与 acks_late 双重执行
后端·python
清桔5 小时前
模型的调用
python
吴佳浩5 小时前
Agent 怎么做自动化评测?构建端到端的 Agent Evaluation 体系
人工智能·agent·ai编程
火山引擎开发者社区5 小时前
火山引擎云数据库 TiDB 版公测开启,MySQL 架构升级的一站式选择
人工智能
代码方舟5 小时前
Java数据工程:利用天远全网运营商三要素优化线上实名认证合规体验
java·人工智能
小小张说故事5 小时前
Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手
后端·python
Csvn5 小时前
第 27 章 案例三 自动化工作流 Agent
人工智能·aigc·agent
知几蜗牛5 小时前
AI眼镜把记忆放上云,怎样证明云端也看不见?
人工智能