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 │
└───────────────┘ └───────────────┘
架构的四个核心约束:
- Client 与 Server 是 1:1 关系,一个 Client 实例只服务于一个 Server
- Host 与 Client 是 1:N 关系,一个 Host 可管理多个 Client
- Server 不感知其他 Server 的存在,保持隔离
- 所有协议消息都通过传输层(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 为什么需要能力协商?
协议版本和能力声明解决了三个问题:
- 向后兼容:新版本的 Host 可以连接旧版本的 Server,反之亦然
- 功能发现:双方无需预设对方支持什么,通过声明即可获知
- 按需启用: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 的行为:
- 执行
fork+exec启动子进程 - 通过管道连接子进程的
stdin和stdout - 自动完成完整的握手协商
- 管理进程生命周期
两种模式对比:
| 维度 | 命令行独立执行 | Host 托管执行 |
|---|---|---|
| 输入源 | 终端管道 | Host 内部的 stdin 管道 |
| 生命周期 | 随管道关闭终止 | 随 Host 生命周期 |
| 协议协商 | 需手动发送 initialize |
自动完成 |
| 消息路由 | 无 | Host 将 LLM 请求路由至此 |
7.4 解码 mcpServers 配置语义
| 字段 | 说明 |
|---|---|
mcpServers |
Host 保留字段,声明需启动的 Server 列表 |
"fetch" |
逻辑别名,用于日志和路由 |
"command" |
可执行文件(必须在 $PATH 中或为绝对路径) |
"args" |
透传参数数组 |
"env"(可选) |
注入环境变量,隔离敏感凭证 |
"cwd"(可选) |
设置子进程工作目录 |
7.5 安装机制:运行器 vs 手动安装
uvx / npx(运行器) :
- 检查缓存中是否存在包
- 若不存在,从 PyPI/NPM 下载到临时缓存
- 创建隔离虚拟环境,执行入口脚本
手动安装(生产推荐) :
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(简要了解)
工作机制:
- Server 作为独立 HTTP 服务运行
- Client 建立 SSE 连接接收 Server 消息
- Client 通过 HTTP POST 发送 JSON-RPC 请求
- 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 需要建立两个关键认知:
-
横向看分层:MCP 不定义 LLM 如何被调用(那是模型推理层的职责),而是定义 LLM 周围的环境(工具、数据、提示)如何被接入。它锚定的是南向接口,是 LLM 应用架构中的"外设驱动层"。
-
纵向看进程:MCP Server 只是一个读写 stdin/stdout 的普通进程。无论你在终端用
echo管道测试,还是由 Host 通过fork+exec托管,Server 本身不感知调用者的差异。uvx是包管理工具的行为,command是操作系统的进程启动行为,而 MCP 协议仅定义进程启动后,stdin/stdout 中流淌的数据格式。
从 2024 年 11 月发布至今,MCP 已经从 Anthropic 的内部项目成长为 Linux Foundation 治理下的行业标准。理解其协议设计背后的分层哲学和进程模型,是深入参与这一生态的基石。
更多推荐


所有评论(0)