面向工程师与技术决策者的入门指南。从概念到实战,配套 Python SDK 示例。
一、为什么会有 MCP?
在 MCP 出现之前,把一个 AI 应用接入外部系统大致是这样一种状态:
- 想让模型查数据库?写一套 function calling 的 schema、写 handler、写权限校验。
- 想让模型读文件?再写一遍。
- 同一个"查 GitHub Issue"的能力,Claude 桌面端、Cursor、VS Code、自研 Agent,各写一遍。
- 工具多了之后,prompt 维护成本急剧上升,跨应用复用几乎为零。
这就是典型的 M × N 集成问题:M 个 AI 应用 × N 个数据源/工具 = M × N 套对接代码。
MCP(Model Context Protocol,模型上下文协议) 是 Anthropic 牵头、目前已成为事实标准的开放协议,目标是把这个 M × N 收敛为 M + N:
- 数据源/工具方只需要实现一次 MCP 服务端;
- AI 应用方只需要实现一次 MCP 客户端;
- 任意客户端都能即插即用地连接任意服务端。
官方一句话比喻:MCP 之于 AI 应用,相当于 USB-C 之于电子设备——一个统一的接口,连接万物。
二、一图总览
┌─ Tools (模型调用,有副作用)
Server ─────┼─ Resources (应用读取,只读上下文)
/ └─ Prompts (用户触发,工作流模板)
/
Client
/
Host
\ ┌─ Sampling (Server 反向请求 LLM)
Client ───────────┼─ Elicitation (Server 反向问用户)
\ ├─ Roots (Client 告知工作目录)
\ └─ Logging (Server 输出日志)
Server
数据层:JSON-RPC 2.0
传输层:stdio(本地) / Streamable HTTP(远程,支持 OAuth)- 传输层:Client 与 Server 之间用 JSON-RPC 2.0 通信,承载在 stdio 或 Streamable HTTP 之上。
- Server 原语:Server 主动暴露给 Host 使用的能力,分别面向模型(Tools)、应用(Resources)、用户(Prompts)三类不同的"决策者"。
- Client 原语:Client 暴露给 Server 反向调用的能力,让 Server 在不直接依赖 LLM/UI 的前提下也能完成 agentic 工作流。
三、三个最容易混淆的角色:Host / Client / Server
这是入门 MCP 时最容易绕晕的地方。它们三者的关系是这样的:
注意右下角 Server C 同时连接了两个 Client——远程 Server 通常服务多个 Client,而本地 stdio Server 一般是 1:1。
| 角色 | 角色定位 | 举例 |
|---|---|---|
| Host(宿主) | 用户直接交互的 AI 应用,负责协调多个 Client | Claude Desktop、Cursor、VS Code、ChatGPT、你自研的 Agent |
| Client(客户端) | Host 内部的一个组件,每个 Client 与一个 Server 维持 1:1 的专用连接 | Host 启动时为每个配置的 Server 各创建一个 Client |
| Server(服务端) | 提供工具/数据/模板的程序,本地或远程均可 | filesystem、postgres、github、sentry、Slack 等 |
一个常见误解:以为 "Server" 一定是远程服务。错。Server 指的是协议角色,不是部署形态。 通过 stdio 启动的本地子进程同样是 Server。
四、协议的两层:数据层 + 传输层
MCP 的协议设计干净地分成了两层,便于在不同场景复用。
1. 数据层(Data Layer)
基于 JSON-RPC 2.0,定义消息的结构与语义。包含:
- 生命周期管理:连接建立、能力协商、连接关闭
- 服务端能力:tools、resources、prompts
- 客户端能力:sampling、elicitation、logging、roots
- 通知机制:列表变更、进度更新等
2. 传输层(Transport Layer)
负责把数据层的 JSON 消息从一端送到另一端。MCP 目前定义了两种官方传输:
| 传输方式 | 适用场景 | 特点 |
|---|---|---|
| stdio | 本地 Server | Host 把 Server 作为子进程启动,通过标准输入/输出通信。零网络开销,最适合本地工具(文件、Git、本地 DB)。 |
| Streamable HTTP | 远程 Server | 客户端用 HTTP POST 发请求,服务端可选用 Server-Sent Events (SSE) 推送流式响应。支持标准 HTTP 鉴权(Bearer Token、OAuth),适合 SaaS 化的 MCP Server。 |
旧版协议中还有一个独立的 SSE 传输,新规范已合并到 Streamable HTTP,新写代码请直接选 Streamable HTTP。
五、服务端三大原语:Tools / Resources / Prompts
这是日常开发中接触最频繁的部分。三者非常容易混淆,但它们的设计意图完全不同——关键差异在于"谁来决定调用"。
| 原语 | 控制权 | 类比 | 典型用途 |
|---|---|---|---|
| Tools(工具) | 模型控制 | 类似 REST 的 POST |
模型自主决定调用,会产生副作用:写数据库、发消息、调 API |
| Resources(资源) | 应用控制 | 类似 REST 的 GET |
被动数据源,只读。由宿主应用选择什么时机把哪些上下文塞给模型 |
| Prompts(提示) | 用户控制 | 类似 Slash Command | 预制的工作流模板,用户主动选择调用(如 /plan-vacation) |
我喜欢这样记忆:
Tools 是动词,Resources 是名词,Prompts 是工作流。
Tools:模型主动调用
# 模型看到用户说"查下旧金山天气",自己决定调用这个工具
@mcp.tool()
def get_weather(city: str) -> str:
"""获取某个城市的当前天气"""
return fetch_weather_api(city)相关方法:tools/list(发现)、tools/call(执行)。
Resources:应用按需读取
# 应用决定是否要把这段 README 塞给模型作为上下文
@mcp.resource("file:///{path}")
def read_file(path: str) -> str:
return open(path).read()每个资源有一个 URI(如 file:///README.md、postgres://mydb/schema),支持模板化(weather://forecast/{city})。相关方法:resources/list、resources/templates/list、resources/read、resources/subscribe。
Tools vs Resources:完整链路与选型
光看"控制权"不够直观,对比一下两者的完整数据流:
Tools 的链路(模型回合内触发):
1. Host 启动 → 调 tools/list 拿 schema
2. Host 把 schema 注入 LLM 调用的 tools 字段
3. 用户发消息 → LLM 推理 → 输出 tool_use 块
4. Host 截获 → 路由到对应 Server → 发 tools/call
5. Server 执行(跑 SQL、调 API)→ 返回 content
6. Host 把 content 作为 tool_result 拼回对话
7. LLM 继续生成(可能再触发下一轮,循环 3-6)Resources 的链路(对话回合之外注入):
1. Host 启动 → 调 resources/list 和 resources/templates/list
2. Host 把资源在 UI 上呈现(文件树 / @ 补全 / 自动建议)
3. 资源进入对话上下文的三种触发方式:
a. 用户手动 @ 引用
b. 应用启发式自动塞入
c. 桥接为 Tool 让模型自主调用(下面详述)
4. Host 调 resources/read(uri) → Server 返回 contents
5. Host 把 contents 作为 user 消息的一部分发给 LLM
6. LLM 收到时是"已经在上下文里"的事实一句话区分:
Tools 在 LLM 回合内被模型主动拉取;Resources 在 LLM 回合外被应用主动推入。
"数据但需要计算"选哪个? 判断顺序:
- 有副作用? 有 → 必须 Tool(哪怕只是埋点)。
- 要让模型自主决定调用? → Tool。
- 要让用户/应用预先选好作为上下文塞入? → Resource Template。
- 客户端生态考量:现状是 Tools 在所有 Host 上都支持,Resources 的 UI 支持差异较大。很多 Server 会把同一能力同时暴露为 Tool 和 Resource——Tool 兜底兼容,Resource 给支持的 Host 用更优雅的呈现。
Resources 也能由 LLM 驱动吗? 协议层并未禁止——"应用控制"是设计意图而非技术约束。常见的做法是 Resource-as-Tool 桥接:Host 在 LLM 的工具列表里额外注入两个合成工具:
{ "name": "list_resources", "description": "列出所有可用 MCP 资源", ... }
{ "name": "read_resource", "description": "按 URI 读取资源", ... }LLM 决定调用 read_resource(uri="postgres://schema/users") → Host 转发为 resources/read。形式是 Tool,本质是 LLM 在驱动 Resource。Claude Code 默认就这么做。
"应用启发式塞入"具体是什么? 几个真实场景:
- IDE 类 Host 的上下文注入:每次发消息时自动塞入当前光标所在文件、最近编辑过的几个文件、
.cursorrules/CLAUDE.md等约定配置。 - 项目初始化自动加载:打开项目时自动读
README.md、package.json、.env.example作为 system context。 - 语义检索自动召回:对 user message 做 embedding,对所有 resources 做向量检索,Top-K 命中自动拼入。
- 订阅式实时同步:用户改了文件 → Server 推
notifications/resources/updated→ Host 把新内容刷进下一轮上下文。
这些都不需要用户和模型显式发起,由 Host 用一套规则决定。
Prompts:用户显式触发
@mcp.prompt()
def plan_vacation(destination: str, days: int) -> str:
"""生成一次度假规划"""
return f"请帮我规划一次为期 {days} 天的 {destination} 之旅..."在 Claude Desktop、Claude Code、Cursor 这类应用中,Prompt 通常表现为 / 命令或命令面板里的快捷入口。
关于 Prompts 有三个常被混淆的点需要澄清:
1. "只能由用户触发"是 UX 约定,不是协议铁律。 协议层没限制谁调 prompts/get,任何持有 session 的一方都能触发。"user-controlled" 是规范给 Host 的 UX 建议,意在让用户感知到自己在主动发起,避免模型偷偷调用模板。自研 Agent 完全可以在某个 workflow 节点上自动 prompts/get,技术上没人会拦。
2. 返回值不只是字符串。 prompts/get 真正返回的是一个 messages 数组:
{
"messages": [
{ "role": "user", "content": { "type": "text", "text": "..." } },
{ "role": "assistant", "content": { "type": "text", "text": "..." } },
{ "role": "user", "content": { "type": "image", "data": "base64...", "mimeType": "image/png" } },
{ "role": "user", "content": { "type": "resource", "resource": { "uri": "...", "text": "..." } } }
]
}可以构造多轮 few-shot 示例、包含图片/音频的多模态 prompt、嵌入资源引用。FastMCP 里 @mcp.prompt() 返回字符串只是便利封装——它会被自动包装成单条 user 消息。要更复杂的结构,显式返回 list[Message]:
from mcp.server.fastmcp.prompts import base
@mcp.prompt()
def debug_session(error: str) -> list[base.Message]:
return [
base.UserMessage("我遇到了这个错误:"),
base.UserMessage(error),
base.AssistantMessage("我来帮你分析。先确认几个问题..."),
]3. Prompts ≠ 简化版 Skills。 这两者经常被混为一谈,但解决的是完全不同维度的问题:
| 维度 | MCP Prompts | Claude Skills |
|---|---|---|
| 核心问题 | 让用户把已知任务用模板化方式发起 | 让模型在合适的时候自主获得一项能力 |
| 触发者 | 用户(UI 主动选) | 模型(基于 SKILL.md frontmatter 自主判断) |
| 形态 | 协议消息,通过 JSON-RPC 取 | 文件系统上的 markdown bundle(含可选脚本) |
| 内容 | 返回 messages 数组喂给 LLM | markdown 指令 + 可执行代码 + 资源文件 |
| 执行 | 纯模板替换,不执行代码 | Agent 可执行其中的脚本 |
| 控制平面 | 服务端原语 | 模型侧能力 |
更准确的类比:
Prompts 像 IDE 的 Code Snippet 或 Notion 的 Template:用户选 → 自动填模板。 Skills 像 Unix 的 man page + 可执行脚本:模型自己读说明书 → 决定要不要用 → 可能还执行其中代码。
一句话:Prompts 是"用户能用的模板",Skills 是"模型能学的技能"。
六、客户端原语:让 Server 反过来"调用"Client
服务端不只是被动响应,它也可以主动向客户端请求某些能力。客户端原语有四个:
| 客户端原语 | 用途 | 经典场景 |
|---|---|---|
| Sampling | Server 请求 Host 的 LLM 帮自己做一次推理 | Server 想分析 47 个航班选项但不想内置 LLM SDK,于是让 Client 代为完成 |
| Elicitation | Server 请求用户提供信息或确认操作 | "请确认这笔 $3000 的订单",配 JSON Schema 描述需要哪些字段 |
| Roots | Client 告诉 Server "你只能在这些目录下工作" | IDE 把当前打开的项目目录暴露给 Server |
| Logging | Server 向 Client 发送日志,便于调试与可观测 | tool 执行中打印 debug 信息 |
Sampling 的精妙之处:Server 想用 LLM 但不想自己花钱、不想绑定模型供应商,于是把"调用 LLM"的工作反向托管给 Client。Client 已经有 LLM 访问能力(用户已经付费/授权了),所以让 Client 出钱出力,并且用户可在中途审核 prompt 和返回内容——这天然就是 human-in-the-loop。
七、生命周期:一次完整的握手
理解了角色和原语,我们用 JSON-RPC 看一次最小完整流程,便于将来抓包调试时不抓瞎。
Step 1. 初始化握手
// Client → Server
{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": { "elicitation": {} },
"clientInfo": { "name": "example-client", "version": "1.0.0" }
}
}// Server → Client
{
"jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": {}
},
"serverInfo": { "name": "example-server", "version": "1.0.0" }
}
}// Client → Server(通知,无 id 无返回值)
{ "jsonrpc": "2.0", "method": "notifications/initialized" }这里有几个关键点:
- 协议版本协商:双方必须使用兼容的版本,否则连接终止。
- 能力声明:Server 在
capabilities里显式列出自己支持的原语——本例中只声明了tools和resources,意味着这个 Server 不支持 prompts,Client 也就不会调用prompts/*相关方法。声明了tools.listChanged: true的服务端,将来工具列表变更时会主动发notifications/tools/list_changed。 {}不是空对象:它表示"我支持这个能力,但没有可配置选项"——是声明的最小形式。
Step 2. 发现工具
// Client → Server
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }Step 3. 调用工具
// Client → Server
{
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {
"name": "weather_current",
"arguments": { "location": "San Francisco", "units": "imperial" }
}
}Step 4. 服务端主动通知
// Server → Client(工具列表变更,无需响应)
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }八、Python SDK 实战
接下来全部用 Python 官方 SDK(mcp)演示。
1. 安装
推荐使用 uv:
uv init mcp-demo
cd mcp-demo
uv add "mcp[cli]"也可以用 pip:
pip install "mcp[cli]"2. 最简服务端:Tools + Resources + Prompts 全家桶
server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")
# ---- Tool:模型主动调用,会产生副作用 ----
@mcp.tool()
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
# ---- Resource:应用按需读取 ----
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""根据名字生成个性化问候"""
return f"Hello, {name}!"
# ---- Prompt:用户显式触发 ----
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
"""生成不同风格的问候 prompt 模板"""
styles = {
"friendly": "请写一段温暖、友好的问候",
"formal": "请写一段正式的问候",
"casual": "请写一段轻松随意的问候",
}
return f"{styles[style]},对象是名叫 {name} 的人。"
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输启动方式三选一:
# 调试:MCP Inspector 提供 Web UI
uv run mcp dev server.py
# 安装进 Claude Desktop
uv run mcp install server.py
# 切换为 HTTP 传输(生产推荐)
# 在代码里改成 mcp.run(transport="streamable-http")3. 结构化输出:用 Pydantic 自动生成 Schema
FastMCP 会从类型注解自动生成 JSON Schema,模型侧会更容易理解返回数据:
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Weather")
class WeatherData(BaseModel):
temperature: float = Field(description="摄氏度")
humidity: float = Field(description="百分比")
condition: str = Field(description="天气状况,如 sunny / cloudy")
@mcp.tool()
def get_weather(city: str) -> WeatherData:
"""获取指定城市的天气信息(结构化返回)"""
return WeatherData(temperature=22.5, humidity=45.0, condition="sunny")4. Context 注入:日志、进度、Sampling、Elicitation
向 tool 加一个 Context 参数,FastMCP 会自动注入。借助它可以做日志、进度、反向调用 LLM、向用户索取信息:
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import SamplingMessage, TextContent
mcp = FastMCP("Advanced")
# 进度上报 + 日志
@mcp.tool()
async def long_task(name: str, ctx: Context, steps: int = 5) -> str:
await ctx.info(f"开始执行任务:{name}")
for i in range(steps):
await ctx.report_progress(
progress=(i + 1) / steps,
total=1.0,
message=f"步骤 {i+1}/{steps}",
)
return f"任务 {name} 完成"
# Sampling:反向请求 Client 的 LLM
@mcp.tool()
async def summarize(topic: str, ctx: Context) -> str:
"""让 Host 的 LLM 帮我总结一下"""
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=f"请用一句话总结 {topic}"),
)
],
max_tokens=100,
)
return result.content.text if result.content.type == "text" else str(result.content)5. 生命周期管理:启动时建连接,关闭时清理
需要在启动时初始化数据库连接池等长生命周期资源时,使用 lifespan:
from dataclasses import dataclass
from contextlib import asynccontextmanager
from mcp.server.fastmcp import Context, FastMCP
@dataclass
class AppContext:
db: "Database"
@asynccontextmanager
async def app_lifespan(server: FastMCP):
db = await Database.connect()
try:
yield AppContext(db=db)
finally:
await db.close()
mcp = FastMCP("MyApp", lifespan=app_lifespan)
@mcp.tool()
def query(sql: str, ctx: Context) -> str:
app_ctx: AppContext = ctx.request_context.lifespan_context
return app_ctx.db.execute(sql)6. 客户端:stdio 连接本地 Server
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(
command="uv",
args=["run", "server.py"],
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("Tools:", [t.name for t in tools.tools])
result = await session.call_tool("add", arguments={"a": 5, "b": 3})
print("Result:", result.content[0].text)
asyncio.run(main())7. 客户端:Streamable HTTP 连接远程 Server
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
async with streamablehttp_client("http://localhost:8000/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("Tools:", [t.name for t in tools.tools])
asyncio.run(main())九、在 Claude Code 中的具体映射
理论讲完了,看一下三大原语在真实 Host 里到底长什么样。以 Claude Code 为例:
Tools → mcp__<server>__<tool> 工具
服务端的每个 tool 都被改名后注入模型的 tool 列表。例如装了 github MCP server,模型看到的工具叫 mcp__github__create_issue。
特殊机制 Tool Search(默认开启):tool 定义不是一次性塞进 context window,模型先看到一个 ToolSearch 工具,按需查找。所以装 20 个 Server 也不会撑爆上下文。可以用 alwaysLoad: true 把高频 Server 强制常驻。
Resources → @ 引用 + 桥接 Tool 双路并存
Claude Code 对 Resources 的支持正好印证上一节的讨论——应用驱动和模型驱动同时存在:
路径 A:用户主动 @ 引用
Can you analyze @github:issue://123 and suggest a fix?
Compare @postgres:schema://users with @docs:file://database/user-model输入 @ 触发自动补全,从所有连接的 Server 列出可用资源,支持模糊搜索。被引用的资源自动作为 attachment 拉进上下文。
路径 B:模型主动调用(桥接为 Tool)
官方文档原话:
"Claude Code automatically provides tools to list and read MCP resources when servers support them"
Claude Code 自动把 resources/list 和 resources/read 包装成内置工具暴露给模型——模型可以自主决定要不要去翻资源。
Prompts → /mcp__<server>__<prompt> Slash 命令
Server 定义的每个 prompt 都映射成一个 slash 命令:
/mcp__github__list_prs
/mcp__github__pr_review 456
/mcp__jira__create_issue "Bug in login flow" high- 参数按 prompt 的 schema 解析,空格分隔;
- 服务端和 prompt 名里的空格被规范化为下划线;
- Prompt 返回的 messages 数组直接拼进对话。
客户端原语支持情况
- Elicitation:自动弹交互对话框(form 模式或 URL 模式),可用 hook 自动应答。
- Roots:Server 启动时设置
CLAUDE_PROJECT_DIR环境变量,Server 也可以调roots/list查询当前工作目录。
对 Server 作者的启示
不同客户端对 MCP 原语的支持程度差异很大。如果你的 Server 想兼容性最大化:
- 必须支持 Tools——这是目前所有 MCP 客户端都支持的最大公约数。
- 重要能力建议双重暴露:既写成 Tool 兜底,也写成 Resource Template 给支持的 Host 用更优雅的方式呈现。比如查询类的接口,既提供
query_user(id)Tool,又暴露user://{id}Resource Template。 - Prompts 锦上添花:对于支持 Prompts 的 Host,它能显著提升用户在常见工作流上的体验。
十、MCP vs Function Calling vs 自研插件协议
这是公司内部分享时最常被问的问题之一。
| 维度 | Function Calling | 自研插件协议 | MCP |
|---|---|---|---|
| 标准化程度 | 各家 LLM 厂商各有定义 | 公司内部一套 | 跨厂商开放标准 |
| 生态复用 | 每个模型一套 schema | 几乎为零 | 一次实现,跨 Host 通用 |
| 上下文资源 | 通常只有"函数调用" | 自定义 | Tools / Resources / Prompts 三类原语 |
| 反向能力 | 不支持 | 一般不支持 | Sampling / Elicitation / Roots |
| 传输与鉴权 | 内嵌 API | 自定 | stdio / Streamable HTTP + OAuth |
| 适合场景 | 单一应用内部小范围 | 私有强定制 | 工具/数据要被多端复用、要标准化 |
一个粗略的判断准则:
- 能力只服务于一个 AI 应用、几乎不会被复用 → Function Calling 足够。
- 能力会被多个 AI 应用消费(团队多个产品 + 第三方工具如 Claude Desktop / Cursor)→ 优先选 MCP。
- 需要给用户/模型暴露资源和工作流模板,而不只是函数 → MCP 的 Resources / Prompts 是 Function Calling 没有的。
十一、什么时候不用 MCP
凡是建议,必有反例。下面几种情况,强行套 MCP 反而增加复杂度:
- 只是 Prompt 工程:没有外部系统交互,纯粹是 prompt 调优,不需要 MCP。
- 极致低延迟的同进程调用:MCP 即使是 stdio,也有 JSON-RPC 序列化开销,对延迟敏感的场景直接函数调用更划算。
- 能力跟应用强耦合、永远不会复用:自己造一套 function calling 更轻量。
- 跨语言、跨进程的 RPC 通用诉求:那不是 MCP 的目标,请用 gRPC / OpenAPI。
十二、生产实践中的几点建议
实际使用 MCP 时,下面几条是踩过坑后总结的:
1. 工具名要带命名空间
不要叫 search,叫 github_search_issues、jira_search_tickets。多个 Server 同时连接时,模型才能准确路由。
2. description 写给模型看,不是写给人看
工具描述会直接进入模型上下文,措辞会显著影响调用准确率。要写清楚:做什么、何时该用、何时不该用、输入约束。
3. 危险操作必须 Elicitation
写库、发消息、扣款……这类有副作用的操作,主动调用 elicitation/create 让用户确认,而不是相信"模型不会乱来"。
4. Streamable HTTP 配合 OAuth
远程 Server 不要再用 Bearer Token 写死在配置里。OAuth 是规范推荐的鉴权方式,配合 roots 限定 Server 的工作范围。
5. 善用 listChanged 通知
工具集随业务动态变化(如新接入一个数据源)时,发 notifications/tools/list_changed,Client 会立即刷新,无需重启。
6. 开发期用 MCP Inspector
uv run mcp dev server.py会启一个 Web UI,可视化地查看 tools/resources/prompts、手动调用、看 JSON-RPC 报文,比靠 print 调试快十倍。
十三、参考文档
- 官方文档首页:https://modelcontextprotocol.io/docs/
- 架构概览:https://modelcontextprotocol.io/docs/learn/architecture
- 服务端概念(Tools / Resources / Prompts):https://modelcontextprotocol.io/docs/learn/server-concepts
- 客户端概念(Sampling / Elicitation / Roots):https://modelcontextprotocol.io/docs/learn/client-concepts
- 最新规范:https://modelcontextprotocol.io/specification/latest
- Python SDK:https://github.com/modelcontextprotocol/python-sdk
- 官方参考 Server 仓库:https://github.com/modelcontextprotocol/servers
- MCP Inspector(调试利器):https://github.com/modelcontextprotocol/inspector
结语
MCP 在 2024 年底问世,到今天已经被 Claude、ChatGPT、VS Code、Cursor 等主流 AI 应用普遍支持,成为 AI 工具生态的事实标准。
它的设计哲学很简单:
协议归协议,模型归模型。 MCP 不规定你怎么用 LLM、怎么管理上下文,它只规定"AI 应用如何与外部系统通信"这一件事,并把它做到了足够通用与足够干净。
对工程师而言,理解 MCP 的回报很高:写一次 Server,便能被生态里的所有 Host 复用;写一次 Client,便能即插即用任意社区 Server。在 AI 应用碎片化加剧的今天,MCP 是少数能跨厂商、跨产品沉淀工程价值的协议层。