引言: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 建立连接时的完整对话。我们逐行拆解:

[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 无法处理某个请求时,必须返回标准格式的错误响应:

{
  "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 模式一:直接在命令行执行(独立模式)

# 直接启动 Server(会卡住等待 stdin)
uvx mcp-server-fetch

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

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

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

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

7.3 模式二:由 MCP Host 托管

在 Host 配置文件中定义:

{
  "mcpServers": {
    "fetch": {
      "command": "uvx",
      "args": ["mcp-server-fetch"]
    }
  }
}

Host 的行为:

  1. 执行 fork+exec 启动子进程
  2. 通过管道连接子进程的 stdinstdout
  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. 创建隔离虚拟环境,执行入口脚本

手动安装(生产推荐)

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

配置改为:

{
  "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 环境准备

# 使用 uv 管理项目
uv init mcp-demo
cd mcp-demo
uv add "mcp[cli]"

9.2 编写 MCP Server

创建 server.py

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 方式一:命令行独立运行

# 直接启动(会卡住等待 stdin)
python server.py

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

预期输出

{"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

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())

运行:

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 中添加:

{
  "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"
{
  "mcpServers": {
    "demo": {
      "command": "py",
      "args": ["C:\\path\\to\\server.py"]
    }
  }
}

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

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

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

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

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

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

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

# 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),需要用引号包裹:

{
  "command": "python",
  "args": ["\"C:\\My Documents\\server.py\""]
}

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

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

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

解决方案:使用 typing.Union

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 治理下的行业标准。理解其协议设计背后的分层哲学和进程模型,是深入参与这一生态的基石。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐